GithubHelp home page GithubHelp logo

allykzam / amazingant.fsharp.typeexpansion.templates Goto Github PK

View Code? Open in Web Editor NEW
0.0 2.0 2.0 227 KB

Templates for use with the Amazingant.FSharp.TypeExpansion tool

License: MIT License

Batchfile 0.20% F# 99.80%

amazingant.fsharp.typeexpansion.templates's Introduction

Amazingant.FSharp.TypeExpansion.Templates

This package contains templates for use with the Amazingant.FSharp.TypeExpansion type provider.

Templates

Lenses

This template provides two types for lensing, StaticLens<'a,'b> and Lens<'a,'b>, to use for creating lenses for nested record types.

The type expansion template provides a static member of type StaticLens<'a,'b> for each field (of type 'b) in the processed record of type 'a, and a non-static member of type Lens<'a,'b> for the same. The template additionally composes the StaticLens<'a,'b> members for nested types, creating a static member of type StaticLens<'a,'c> for each field of type 'c in the nested record of type 'b which is, in turn, a field in the record type 'a.

Example:

[<Lens; ExpandableType([| "Lenses" |])>]
type Coordinate = { X : int; Y : int; Z : int; }

[<Lens; ExpandableType([| "Lenses" |])>]
type Player =
    {
        Name : string;
        Position : Coordinate;
    }

The expansion of these two types causes individual lenses to be created for each axis of a Player's Position, such that a Player can be moved like so:

let MoveRight (p : Player) (distance : int) =
    p.Position_X_Lens.Map ((+) distance)

This MoveRight function now takes a Player and moves them to the right by the specified distance, and returns the new Player value that reflects this.

While this lensing template is not as feature-complete as some lensing tools are, and the lens names can be very long depending on the nesting depth and field names, the benefit that this template provides is that lenses are created automatically via the type expansion system. These lenses do not require manually creating lenses as some tools do, and the alternative in vanilla F# is much longer:

let MoveRight (p : Player) (distance : int) =
    { p with Position = { p.Position with X = (p.Position.X + distance) } }

And of course with deeper nesting, the length of vanilla F# record updates grows much faster than the lenses provided by this template. In the event that a more feature-rich lensing library is needed, feel free to use this template as a reference to build a template for that lensing library, if doing so will improve its usefulness and/or usability.

FromXml

This template provides a FromXmlNode extension method for any type provided to it. Types which have the XmlNode attribute will additionally receive two FromXmlDoc extension methods, the first of which takes a System.Xml.XmlDocument object, and the second of which takes a string and loads it as an XmlDocument object before calling the first function.

These extension methods serve to load a record type and any nested types from an XML document. These methods are intended to build F# record types only, and are not likely to work with custom classes, and the XML processing done is very simplistic. If more advanced processing is needed, the XML type provider from from F# Data should be considered instead.

A basic example:

[<XmlNode("Item_Data"); ExpandableType([| "FromXml" |])>]
type ItemData =
    {
        ItemId : string;
        PromotionStart : DateTimeOffset;
        [<XmlAttr("free_shipping")>]
        HasFreeShipping : bool;
    }
    [<Validation>]
    member x.ReasonableStartTime() = if x.PromotionStart.Year < 2000 then failwith "Promotion start date is too far in the past."

Note that the above example makes use of both the XmlNode and XmlAttr attributes; the XmlNode attribute can be used on both the record type, and individual fields, whereas the XmlAttr attribute can only be used on the record's fields. Either node indicates to this template what part of the XML to process. The result is that the following XML can be passed to ItemData.FromXmlDoc, and a valid ItemData value will be produced:

<ITEM_DATA FREE_SHIPPING="true">
    <ITEM_ID>123abc</ITEM_ID>
    <PROMOTION_START>2016-01-01 12:34:56 +00:00</PROMOTION_START>
</ITEM_DATA>

Also note that the F# code above includes a method named ReasonableStartTime, which checks the year of the PromotionStart field, throwing an exception if the start time is determined to be too far in the past. Such validation methods can be marked with the Validation attribute to indicate that they need to be validated after processing XML; the FromXmlNode and FromXmlDoc methods generated by the FromXml template will automatically call any of these validation methods it finds. Validation methods are accepted as either member methods which take no parameters, or as static methods that take an instance of their parent type. Likewise, validation methods can either return unit (()), or a ValidationResult. If returning unit, feel free to throw an exception, as shown in the example above; if returning a ValidationResult, please put any error message(s) into the ValidationResult.Invalid case.

In addition to the basic information provided below, more advanced processing can be done. Individual fields in the processed record type can be optional, or one of the three main collection types used in F# code (arrays, F#'s list, and seq). These can be combined in a handful of ways, and the template will provide an error if it cannot process the combination provided.

If a record field is of a type that also has an XmlNode attribute on it, the FromXmlNode method for that type will be used to process it. Nested types like this improve the levels of Option<'T> and the collection types which can be used. As an example, the above ItemData type could be modified to contain a Promotions field:

[<XmlNode("Sales_Promotion"); ExpandableType([| "FromXml" |])>]
type Promotion =
    {
        ...
    }

[<XmlNode("Item_Data"); ExpandableType([| "FromXml" |])>]
type ItemData =
    {
        ...
        [<XmlNode("Promotions")>]
        Promotions : (Promotion list) option;
    }

If the Promotions node was empty or not present during processing, the Promotions field would be set to None. But if the Promotions node was present and contained one or more Sales_Promotion nodes, each Sales_Promotion node would be processed into a Promotion value and stored in the resulting list.

The rules around nesting levels of list and option are a bit flexible, so feel free to play around with them; however, be sure to test the result with sample XML documents to ensure that the result matches the expectations.

An additional point of note, when specifying XmlNode("Item_Data") or XmlAttr("free_shipping"), the name specified is case-insensitive. The specified name is free to be all lowercase while your data source provides uppercased XML nodes, or visa versa. However, when neither XmlNode nor XmlAttr are used for a field, the processing code will additionally strip out underscores in the XML node and attribute names while processing. This means that in the initial example, the ItemId field in the record type will successfully match XML nodes or attributes named e.g. ITEM_ID, itemid, or even I_t_E_m_I_d. Of course, one should not take this as a suggestion to go crazy with mixed upper and lower case letters or underscores.

For cases where XML nodes are nested in containers such as the following example, the XPath attribute can be used with an XPath specifier.

<ITEM_DATA FREE_SHIPPING="true">
    <ITEM_ID>123abc</ITEM_ID>
    <PROMOTIONS>
        <PROMOTION>Free Shipping</PROMOTION>
        <PROMOTION>10% Off</PROMOTION>
    </PROMOTIONS>
</ITEM_DATA>

In such a case, creating a Promotions type just to access the PROMOTION nodes is needlessly required. An XPath specifier can be used to avoid this:

[<XmlNode("Item_Data"); ExpandableType([| "FromXml" |])>]
type ItemData =
    {
        ...
        [<XPath("PROMOTIONS/PROMOTION")>]
        Promotions : string list;
    }

Points of note with the XPath attribute:

  • Fields that are tagged with the XPath attribute currently cannot be of another type with an XmlNode attribute
  • Any valid XPath specifier can be used, but those which contain double-quotes (") will cause a compiler error after expansion is complete. Prefer single-quotes (') within the XPath string to avoid this.
  • Escaped characters will need to be prefixed with two extra backslashes to account for the fact that the string is going to be dumped into an F# source file and compiled again (fun, right?)

License

This project is Copyright © 2016-2017 Anthony Perez a.k.a. amazingant, and is licensed under the MIT license. See the LICENSE file for more details.

amazingant.fsharp.typeexpansion.templates's People

Contributors

allykzam avatar forki avatar

Watchers

 avatar  avatar

Forkers

forki amazingant

amazingant.fsharp.typeexpansion.templates's Issues

Add basic testing?

Should be easy enough to add some basic sample code for each template. Initial expectation is that for each template, there should be:

  • Source types that test each of the expected valid cases
  • A copy of what the expanded results should look like
  • Any needed input files (XML files for the FromXml template)
  • A script that:
    • Uses the expansion library on the source types
    • Uses any input files and validates the results
  • One or more scripts that use invalid source types

With the above established, it should be possible to add a build target for testing which iterates through the test directories for each template and:

  • Deletes the expanded source file if it exists
  • Runs the good script and validates that it was happy with the expansion results
  • Checks to see that a new expanded source file has been created
  • Compares the new expanded source file to the sample expansion
  • Runs the invalid scripts and validates that they are each unhappy

[FromXml] Allow specifying nested node/attribute names

Would be nice to use the following XML and source type:

<Person>
    <PhoneNumbers>
        <Phone>123456789</Phone>
        <Phone>987654321</Phone>
    </PhoneNumbers>
    ...
</Person>
...
type Person =
    {
        [<XmlNode("PhoneNumbers/Phone")>]
        PhoneNumbers : string list;
...

It may be worth creating a new attribute that can be used to just specify an XPath to follow?

Add a test build target

For each sub-directory under the tests directory, the test build target should:

  • Delete the expanded source file if it exists
  • Run the good script and validate that it was happy with the expansion results
  • Check to see that a new expanded source file has been created
  • Compare the new expanded source file to the sample expansion
  • Run the invalid scripts and validate that they are each unhappy

[ImmutableViewModel] Add new template

Had a crazy idea quite some time ago for a template that builds a view model suitable for use with WPF bindings, and bases it on an immutable data model. Creating an issue here so that the current code can be placed in an appropriate branch.

[FromXml] Add support for sum types?

Still just playing with an idea, documenting it here so I can come back and read this later to see if it sounds insane or not.

Initial thought, given the following sum type:

type SumType =
    | OneOption of Value : int
    | OtherOption of Text : string

Any of the following could be processed as OneOption(4):

<Thing>
    <OneOption>4</OneOption>
</Thing>
<Thing>
    <OneOption>
        <Value>4</Value>
    </OneOption>
</Thing>
<Thing>4</Thing>

And any of the following as OtherOption("Test"):

<Thing>
    <OtherOption>Test</OtherOption>
</Thing>
<Thing>
    <OtherOption>
        <Text>Test</Text>
    </OtherOption>
</Thing>
<Thing>Test</Thing>

The first XML option in either set assumes that sum type cases will only ever contain a single field, and therefore the matching node's contents can be used. The sum type case names can then be matched as a node or attribute name as is currently done for product type fields with no attributes.

The second option is a bit strict, but would easily allow any number of fields so long as they had names. If the fields do not have names, perhaps we could fall back to the order of the fields and the order of the XML nodes? But that seems dangerous; it would be safer to disallow FromXml expansion on sum type cases with unnamed fields.

As with the first option, the final option also assumes that the sum type cases will only contain a single field. Instead of going by field name, this option would select the appropriate case based on which field can be successfully parsed. This also seems dangerous; what happens when one case contains a single integer field and the other contains a string? Putting a case with a string field near the beginning of the sum type declaration would cause that case to match before any of the others. Seems like this kind of support is better done by reminding myself that I can just add a static TryParse function on sum types, although it would be nice to provide an easy way to allow "nested" FromXml-expanded types as fields in a sum type's cases.

[FromXml] Some field names that are valid uppercased are not valid lowercased

Noticed that I can build a record with a field named In and another named Out and that compiles fine, but in the generated code, the lower-cased names for these two fields, in and out respectively, are F# keywords and cannot be used directly. Temporary solution is to turn off the type provider and gratuitously sprinkle back-ticks everywhere until the compiler errors go away.

[FromXml] Add testing of input XML

Need to work up some sample XML files and test against them so that I can start playing with ideas for simplifying the generated code without breaking anything.

[FromXml] Nested type in a list option results in malformed expansion

Including a (Thing list) option results in expanded code like this:

let ``field`` =
    let xs = findAllNodes children "nodename"
    |> Seq.toArray
    if xs.Length = 0 then None
    else xs |> Array.map Some.Type.Here.FromXmlNode |> Array.toList

When it should result in this:

let ``field`` =
    let xs = findAllNodes children "nodename" |> Seq.toArray
    if xs.Length = 0 then None
    else xs |> Array.map Some.Type.Here.FromXmlNode |> Array.toList |> Some

Or, more aesthetically-pleasing:

let ``field`` =
    let xs = Seq.toArray (findAllNodes children "nodename")
    if xs.Length = 0
    then None
    else
        xs
        |> Array.map Some.Type.Here.FromXmlNode
        |> Array.toList
        |> Some

This appears to be the source.

[FromXml] Improve handling of bad XML

Although it eventually leads to a useful point in the expanded code, the exceptions thrown when a given XML document is not valid are...less than helpful? Would be nice to add some validation code to improve the messages that come out.

[FromXml] Adjust optional fields to populate as `None` if the target node(s) are empty

Testing with some data at work, and finding that an exception is thrown when trying to parse out an int option from an empty node. Except this is exactly why I marked the field as optional. Need to adjust the behavior so that if the InnerText property for a node is an empty string, the value is automatically None if populating an optional field, otherwise I'll just end up marking these fields as string and writing more code that this template is supposed to take away from me.

[Lenses] Expanded results do not compile

Need to open Amazingant.FSharp.TypeExpansion.Templates.Lenses for each module, and need to update the calls to makeStaticLens -> MakeStaticLens and compose -> Compose.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.