Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultBuild, vary, and test structured data
Build a customer value, derive requests, parse a JSON response, and check selected fields.
Create the baseline
Open Reified.Data for the Data type and module. Open Reified.DataDSL for the concise literal, edit, and matching syntax:
open Reified
open Reified.DataDSL
let customer =
data [
"name" => "Ada"
"plan" => "free"
"nickname" ?=> (None: string option)
"deletedAt" => nil
"address" => [
"city" => "Adelaide"
"postcode" => 5000
]
"roles" => [ "author" ]
]
ReifiedReified.DataDSLConcise opt-in syntax for literals, immutable edits, cases, and matching.
customer: Datadata: DataField list -> DataBuilds an object from ordered field instructions. data [ "name" => "Ada"; "active" => true ] // Data.Object [ "name", Data.Text "Ada"; "active", Data.Bool true ]
(=>): string -> ^value -> DataFieldAssociates a field name with an exact value or recursive data pattern.
(?=>): string -> ^value option -> DataFieldAssociates a field name with an optional exact value, omitting None.
NoneThe representation of "No value"
stringAn abbreviation for the CLI type . Basic Types
optionThe type of optional values. When used from other CLI languages the empty option is the null value. Use the constructors Some and None to create values of this type. Use the values in the Option module to manipulate values of this type, or pattern match against the values directly. 'None' values will appear as the value null to other CLI languages. Instance methods on this type will appear as static methods to other CLI languages due to the use of null as a value representation. Options
nil: DataAn explicit structured null used by literals and edits.
Nested object fields use the same list syntax. ?=> None omits nickname; nil keeps deletedAt as a present null.
Render it with Data.render:
Data.render customer
// => "{ name: \"Ada\", plan: \"free\", deletedAt: null, address: { city: \"Adelaide\", postcode: 5000 }, roles: [\"author\"] }"
Reified.DataModulerender: Data -> stringRenders structured data in a compact, human-readable form. Data.render (data [ "name" => "Ada"; "active" => true ]) // { name: "Ada", active: true }
customer: DataNumbers follow these rules:
intandint64become base-10 digits, such as5000and-12.decimalalways uses.as its decimal separator, regardless of the machine's locale.floatuses enough digits to read back as the same finite value.NaNand infinity are rejected because JSON cannot represent them.numvalidates and keeps the token exactly as written. Use it when spelling matters, such as1.2300e+4.
let invoice =
data [
"amount" => 19.95m
"measurement" => num "1.2300e+4"
]
invoice: Datadata: DataField list -> DataBuilds an object from ordered field instructions. data [ "name" => "Ada"; "active" => true ] // Data.Object [ "name", Data.Text "Ada"; "active", Data.Bool true ]
(=>): string -> ^value -> DataFieldAssociates a field name with an exact value or recursive data pattern.
num: string -> DataConstructs an exact number from a validated portable JSON number token.
invoice contains Data.Number "19.95" for amount and the unchanged token
Data.Number "1.2300e+4" for measurement.
Data.lookupPath "amount" invoice
// => Data.Number "19.95"
Data.lookupPath "measurement" invoice
// => Data.Number "1.2300e+4"
Reified.DataModulelookupPath: string -> Data -> DataParses a path and finds its value or returns Null.
invoice: DataDerive a request
Apply strict edits instead of reconstructing the fixture:
let upgradeRequest =
customer
|> Data.patch [
replace "plan" "pro"
append "roles" "admin"
remove "deletedAt"
]
upgradeRequest: Datacustomer: Data(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Reified.DataModulepatch: DataEdit list -> Data -> DataApplies edits atomically or raises DataPatchException. Data.data [ Data.assoc "name" "Ada" ] |> Data.patch [ DataEdit.replace "name" "Grace" ] // Data.data [ Data.assoc "name" "Grace" ]
replace: string -> ^a -> DataEditReplaces an existing value.
append: string -> ^a -> DataEditAppends an item to an existing list.
remove: string -> DataEditRemoves an existing field or list item.
replace requires its target to exist. set can instead add a missing final object field. Edits run in order and the
complete patch is atomic.
Use Data.tryPatch when edits came from dynamic input and should return structured failures.
Render the changed request with Data.render:
Data.render upgradeRequest
// => "{ name: \"Ada\", plan: \"pro\", address: { city: \"Adelaide\", postcode: 5000 }, roles: [\"author\", \"admin\"] }"
Reified.DataModulerender: Data -> stringRenders structured data in a compact, human-readable form. Data.render (data [ "name" => "Ada"; "active" => true ]) // { name: "Ada", active: true }
upgradeRequest: DataThe original customer still contains plan: "free", one role, and the deletedAt field.
Derive named cases
let nameCases =
customer
|> variants [
variant "present" []
variant "missing" [ remove "name" ]
variant "blank" [ replace "name" "" ]
variant "wrong shape" [ replace "name" [ "Ada" ] ]
]
nameCases: DataCase listcustomer: Data(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
variants: DataVariation list -> Data -> DataCase listMaterializes named variations from one baseline. variants [ variant "inactive" [ replace "active" false ] ] (data [ "active" => true ]) // [ { Name = "inactive"; Value = data [ "active" => false ] } ]
variant: string -> DataEdit list -> DataVariationDeclares one named variation from a baseline.
remove: string -> DataEditRemoves an existing field or list item.
replace: string -> ^a -> DataEditReplaces an existing value.
Each DataCase is a record of Name and the materialized Value, so a failing test can report which case it was.
Declaration order is preserved. See Build variations and matrices for the case types.
The name field of each case is, in order, "Ada", absent, "", and Data.List [ Data.Text "Ada" ].
nameCases |> List.map (fun case -> case.Name, Data.tryFindPath "name" case.Value)
// =>
// [ ("present", Some (Data.Text "Ada"))
// ("missing", None)
// ("blank", Some (Data.Text ""))
// ("wrong shape", Some (Data.List [ Data.Text "Ada" ])) ]
nameCases: DataCase list(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Microsoft.FSharp.Collections.ListModuleContains operations for working with values of type . Operations for collections such as lists, arrays, sets, maps and sequences. See also F# Collection Types in the F# Language Guide.
map: ('T -> 'U) -> 'T list -> 'U listBuilds a new collection whose elements are the results of applying the given function to each of the elements of the collection. The function to transform elements from the input list. The input list. The list of transformed elements. let inputs = [ "a"; "bbb"; "cc" ] inputs |> List.map (fun x -> x.Length) Evaluates to [ 1; 3; 2 ]
case: DataCaseName: stringThe deterministic case name.
Reified.DataModuletryFindPath: string -> Data -> Data optionAttempts to parse a path and find its value.
Value: DataThe materialized value.
Parse JSON output
Json.parseData lives in Reified.Schema, not in the Data-only install shown at the top of this section. Add the
package for this step:
dotnet add package Reified.Schema
let response =
Reified.Json.parseData
"""{
"customer": {
"id": "c-123",
"name": "Ada",
"plan": "pro",
"roles": ["author", "admin"]
}
}"""
response: DataReifiedparseData: string -> DataParses one JSON value into source-neutral structured data. Preserves object field order, duplicate field names, and the original spelling of number tokens. This parser is available on .NET and Fable. It does not apply a model schema; use deserialize with a compiled codec when decoding directly to a schema-described model. Json.parseData "{\"name\":\"Ada\"}" // Data.Object [ "name", Data.Text "Ada" ] Thrown when is null. Thrown when the input is not one complete JSON value.
Reified.JsonFunctions for compiling and running JSON codecs over built model schemas.
The parser is portable across .NET and Fable and returns a fully owned Data tree.
Data.lookupPath "customer.id" response returns Data.Text "c-123". Rendering the response produces a stable JSON
value with the same field order and number tokens as the parsed tree.
Data.lookupPath "customer.id" response
// => Data.Text "c-123"
Reified.DataModulelookupPath: string -> Data -> DataParses a path and finds its value or returns Null.
response: DataCheck the behavior
Use paths for individual checks and containing when several checks belong to the same object:
response
|> matching [
at "customer.id" anyText
at "customer" (
containing [
"name" => "Ada"
"plan" => "pro"
"roles" => containingItems [ "admin" ]
])
absent "error"
]
response: Data(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
matching: DataExpectation list -> Data -> unitChecks expectations or raises DataMatchException. open Reified.DataDSL let actual = data [ "user" => data [ "name" => "Ada" ] ] matching [ at "user.name" "Ada"; absent "error" ] actual // returns unit when both expectations hold; otherwise raises DataMatchException
at: string -> ^a -> DataExpectationRequires a path to contain an exact value or recursive pattern.
anyText: DataPatternMatches any text value.
containing: DataField list -> DataPatternCreates a partial object pattern from required fields. Data.tryMatch [ at "" (containing [ "id" => 42 ]) ] (data [ "id" => 42; "extra" => true ]) // Ok ()
(=>): string -> ^value -> DataFieldAssociates a field name with an exact value or recursive data pattern.
containingItems: ^a list -> DataPatternMatches expected items as an unordered consumed subset. open Reified.DataDSL let actual = data [ "items" => [ "Ada"; "Grace"; "Mallory" ] ] Data.tryMatch [ at "items" (containingItems [ "Ada"; "Grace" ]) ] actual // Ok () when both values occur, in either order
absent: string -> DataExpectationRequires a path to be absent.
Unmentioned object fields are allowed by containing. Literal values inside the pattern remain exact.
matching raises DataMatchException when a test fails. Use Data.tryMatch when the mismatches should be returned as
a value.
The match returns unit despite the unmentioned id field and additional author role.
Changing the expected plan to "free" produces a mismatch at customer.plan. Adding absent "customer.id" produces
another mismatch because that path contains Data.Text "c-123".
Data.tryMatch [ at "customer.plan" "free"; absent "customer.id" ] response
// => Error [
// { ExpectationIndex = 0; Path = DataPath.parse "customer.plan"; Actual = Some (Data.Text "pro"); ... }
// { ExpectationIndex = 1; Path = DataPath.parse "customer.id"; Actual = Some (Data.Text "c-123"); ... }
// ]
Reified.DataModuletryMatch: DataExpectation list -> Data -> Result<unit,DataMismatch list>Checks path-based expectations and accumulates structured mismatches. Data.tryMatch [ at "name" "Ada" ] (data [ "name" => "Grace" ]) // Error [ mismatch at path "name": expected "Ada", found "Grace" ]
at: string -> ^a -> DataExpectationRequires a path to contain an exact value or recursive pattern.
absent: string -> DataExpectationRequires a path to be absent.
response: DataCompare the complete result
Use exact comparison when every field is part of the contract:
let expected =
data [ "customer" => [ "plan" => "free" ] ]
match Data.compare expected response with
| Ok () -> ()
| Error differences ->
differences
|> List.iter (fun difference ->
printfn "%s" (DataPath.toString difference.Path))
expected: Datadata: DataField list -> DataBuilds an object from ordered field instructions. data [ "name" => "Ada"; "active" => true ] // Data.Object [ "name", Data.Text "Ada"; "active", Data.Bool true ]
(=>): string -> ^value -> DataFieldAssociates a field name with an exact value or recursive data pattern.
Reified.DataModulecompare: Data -> Data -> Result<unit,DataDifference list>Compares complete values and returns every structural difference. Data.compare (data [ "name" => "Ada" ]) (data [ "name" => "Ada" ]) // Ok ()
response: DataOkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
ErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
differences: DataDifference list(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Microsoft.FSharp.Collections.ListModuleContains operations for working with values of type . Operations for collections such as lists, arrays, sets, maps and sequences. See also F# Collection Types in the F# Language Guide.
iter: ('T -> unit) -> 'T list -> unitApplies the given function to each element of the collection. The function to apply to elements from the input list. The input list. let inputs = [ "a"; "b"; "c" ] inputs |> List.iter (printfn "%s") Evaluates to unit and prints a b c in the console.
difference: DataDifferenceprintfn: Printf.TextWriterFormat<'T> -> 'TPrint to stdout using the given format, and add a newline. The formatter. The formatted result. See Printf.printfn (link: ) for examples.
Reified.DataPathModuleHelpers for constructing, parsing, and rendering structured data paths.
toString: DataPath -> stringRenders a structured data path using names, dot separators, and bracketed indexes.
Path: DataPathThe location of the difference.
Data.diff returns the same differences directly. Exact comparison observes number tokens, object order, duplicate
fields, list length, and list order.
For equal trees, Data.compare returns Ok (). In the match above, expected supplies plan "free" while
response supplies plan "pro", so Data.compare expected response reports a DataDifference at customer.plan
with expected value Data.Text "free" and actual value Data.Text "pro". Swapping the two arguments reports the
same path with the expected and actual values reversed:
Data.compare
(data [ "customer" => [ "plan" => "pro" ] ])
(data [ "customer" => [ "plan" => "free" ] ])
// => Error [
// { Path = DataPath.parse "customer.plan"
// Expected = Some (Data.Text "pro")
// Actual = Some (Data.Text "free")
// Cause = DataDifferenceCause.DifferentValue }
// ]
Reified.DataModulecompare: Data -> Data -> Result<unit,DataDifference list>Compares complete values and returns every structural difference. Data.compare (data [ "name" => "Ada" ]) (data [ "name" => "Ada" ]) // Ok ()
data: DataField list -> DataBuilds an object from ordered field instructions. data [ "name" => "Ada"; "active" => true ] // Data.Object [ "name", Data.Text "Ada"; "active", Data.Bool true ]
(=>): string -> ^value -> DataFieldAssociates a field name with an exact value or recursive data pattern.