Repository F# setup
open Reified
open Reified.Refinements
open Reified.Result
open Reified.Schema.JsonBuild, vary, and test structured data
This tutorial builds one customer value, derives requests from it, parses a JSON response, and checks the parts that matter to the test.
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.
For example:
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 result contains four cases named present, missing, blank, and wrong shape in that order. Their name values
are respectively "Ada", absent, "", and Data.List [ Data.Text "Ada" ].
nameCases |> List.map (fun case -> case.Name, Data.tryFindPath "name" case.Value)
// =>
// [ ("valid", 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
let response =
Reified.Schema.Json.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.
SchemaJsonReified.Schema.Json.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 example returns unit: all three expectations succeed even though the response contains the unmentioned id
field and the 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:
match Data.compare expected response with
| Ok () -> ()
| Error differences ->
differences
|> List.iter (fun difference ->
printfn "%s" (DataPath.toString difference.Path))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 (). If the actual plan is "free", it returns Error with a
DataDifference whose path is customer.plan, expected value is Data.Text "pro", and actual value is
Data.Text "free".
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.