diff --git a/tests/FSharp.Data.Core.Tests/FSharp.Data.Core.Tests.fsproj b/tests/FSharp.Data.Core.Tests/FSharp.Data.Core.Tests.fsproj
index b85fa7803..94f0b2e9b 100644
--- a/tests/FSharp.Data.Core.Tests/FSharp.Data.Core.Tests.fsproj
+++ b/tests/FSharp.Data.Core.Tests/FSharp.Data.Core.Tests.fsproj
@@ -38,6 +38,7 @@
+
@@ -63,6 +64,10 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
all
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
diff --git a/tests/FSharp.Data.Core.Tests/StructuralTypes.fs b/tests/FSharp.Data.Core.Tests/StructuralTypes.fs
new file mode 100644
index 000000000..7451b96f0
--- /dev/null
+++ b/tests/FSharp.Data.Core.Tests/StructuralTypes.fs
@@ -0,0 +1,338 @@
+module FSharp.Data.Tests.StructuralTypes
+
+open NUnit.Framework
+open FsUnit
+open System
+open FSharp.Data.Runtime.StructuralTypes
+
+[]
+type InferedPropertyTests() =
+
+ []
+ member _.``InferedProperty can be created with name and type``() =
+ let inferredType = InferedType.Primitive(typeof, None, false, false)
+ let property = { Name = "TestProperty"; Type = inferredType }
+
+ property.Name |> should equal "TestProperty"
+ property.Type |> should equal inferredType
+
+ []
+ member _.``InferedProperty Type field is mutable``() =
+ let initialType = InferedType.Primitive(typeof, None, false, false)
+ let newType = InferedType.Primitive(typeof, None, false, false)
+ let property = { Name = "TestProperty"; Type = initialType }
+
+ property.Type <- newType
+ property.Type |> should equal newType
+
+ []
+ member _.``InferedProperty ToString produces meaningful output``() =
+ let inferredType = InferedType.Primitive(typeof, None, false, false)
+ let property = { Name = "TestProperty"; Type = inferredType }
+
+ let result = property.ToString()
+ result |> should contain "TestProperty"
+ result |> should contain "Primitive"
+
+ []
+ member _.``InferedProperty with complex type structure``() =
+ let recordProperties = [
+ { Name = "Field1"; Type = InferedType.Primitive(typeof, None, false, false) }
+ { Name = "Field2"; Type = InferedType.Primitive(typeof, None, false, false) }
+ ]
+ let recordType = InferedType.Record(Some("TestRecord"), recordProperties, false)
+ let property = { Name = "ComplexProperty"; Type = recordType }
+
+ property.Name |> should equal "ComplexProperty"
+ match property.Type with
+ | InferedType.Record(Some("TestRecord"), fields, false) ->
+ fields |> should haveLength 2
+ fields.[0].Name |> should equal "Field1"
+ fields.[1].Name |> should equal "Field2"
+ | _ -> failwith "Expected Record type"
+
+[]
+type InferedTypeTagTests() =
+
+ []
+ member _.``InferedTypeTag NiceName returns correct values``() =
+ InferedTypeTag.Number.NiceName |> should equal "Number"
+ InferedTypeTag.Boolean.NiceName |> should equal "Boolean"
+ InferedTypeTag.String.NiceName |> should equal "String"
+ InferedTypeTag.DateTime.NiceName |> should equal "DateTime"
+ InferedTypeTag.TimeSpan.NiceName |> should equal "TimeSpan"
+ InferedTypeTag.DateTimeOffset.NiceName |> should equal "DateTimeOffset"
+ InferedTypeTag.Guid.NiceName |> should equal "Guid"
+ InferedTypeTag.Collection.NiceName |> should equal "Array"
+ InferedTypeTag.Heterogeneous.NiceName |> should equal "Choice"
+ InferedTypeTag.Json.NiceName |> should equal "Json"
+ (InferedTypeTag.Record None).NiceName |> should equal "Record"
+
+ []
+ member _.``InferedTypeTag NiceName for named record``() =
+ (InferedTypeTag.Record (Some "TestRecord")).NiceName |> should equal "TestRecord"
+
+ []
+ member _.``InferedTypeTag Code returns correct values``() =
+ InferedTypeTag.Number.Code |> should equal "Number"
+ InferedTypeTag.Boolean.Code |> should equal "Boolean"
+ InferedTypeTag.String.Code |> should equal "String"
+ InferedTypeTag.Collection.Code |> should equal "Array"
+ (InferedTypeTag.Record None).Code |> should equal "Record"
+ (InferedTypeTag.Record (Some "TestRecord")).Code |> should equal "Record@TestRecord"
+
+ []
+ member _.``InferedTypeTag ParseCode correctly parses code values``() =
+ InferedTypeTag.ParseCode("Number") |> should equal InferedTypeTag.Number
+ InferedTypeTag.ParseCode("Boolean") |> should equal InferedTypeTag.Boolean
+ InferedTypeTag.ParseCode("String") |> should equal InferedTypeTag.String
+ InferedTypeTag.ParseCode("DateTime") |> should equal InferedTypeTag.DateTime
+ InferedTypeTag.ParseCode("TimeSpan") |> should equal InferedTypeTag.TimeSpan
+ InferedTypeTag.ParseCode("DateTimeOffset") |> should equal InferedTypeTag.DateTimeOffset
+ InferedTypeTag.ParseCode("Guid") |> should equal InferedTypeTag.Guid
+ InferedTypeTag.ParseCode("Array") |> should equal InferedTypeTag.Collection
+ InferedTypeTag.ParseCode("Choice") |> should equal InferedTypeTag.Heterogeneous
+ InferedTypeTag.ParseCode("Json") |> should equal InferedTypeTag.Json
+ InferedTypeTag.ParseCode("Record") |> should equal (InferedTypeTag.Record None)
+ InferedTypeTag.ParseCode("Record@TestRecord") |> should equal (InferedTypeTag.Record (Some "TestRecord"))
+
+ []
+ member _.``InferedTypeTag ParseCode throws for Null``() =
+ (fun () -> InferedTypeTag.ParseCode("Null") |> ignore)
+ |> should throw typeof
+
+ []
+ member _.``InferedTypeTag ParseCode throws for invalid code``() =
+ (fun () -> InferedTypeTag.ParseCode("InvalidCode") |> ignore)
+ |> should throw typeof
+
+ []
+ member _.``InferedTypeTag NiceName throws for Null``() =
+ (fun () -> InferedTypeTag.Null.NiceName |> ignore)
+ |> should throw typeof
+
+[]
+type InferedMultiplicityTests() =
+
+ []
+ member _.``InferedMultiplicity struct values work correctly``() =
+ let single = InferedMultiplicity.Single
+ let optionalSingle = InferedMultiplicity.OptionalSingle
+ let multiple = InferedMultiplicity.Multiple
+
+ single |> should not' (equal optionalSingle)
+ single |> should not' (equal multiple)
+ optionalSingle |> should not' (equal multiple)
+
+[]
+type InferedTypeTests() =
+
+ []
+ member _.``InferedType IsOptional returns correct values``() =
+ let primitiveOptional = InferedType.Primitive(typeof, None, true, false)
+ let primitiveRequired = InferedType.Primitive(typeof, None, false, false)
+ let recordOptional = InferedType.Record(None, [], true)
+ let recordRequired = InferedType.Record(None, [], false)
+ let jsonOptional = InferedType.Json(InferedType.Primitive(typeof, None, false, false), true)
+ let jsonRequired = InferedType.Json(InferedType.Primitive(typeof, None, false, false), false)
+
+ primitiveOptional.IsOptional |> should equal true
+ primitiveRequired.IsOptional |> should equal false
+ recordOptional.IsOptional |> should equal true
+ recordRequired.IsOptional |> should equal false
+ jsonOptional.IsOptional |> should equal true
+ jsonRequired.IsOptional |> should equal false
+ InferedType.Null.IsOptional |> should equal false
+ InferedType.Top.IsOptional |> should equal false
+
+ []
+ member _.``InferedType CanHaveEmptyValues works correctly``() =
+ InferedType.CanHaveEmptyValues(typeof) |> should equal true
+ InferedType.CanHaveEmptyValues(typeof) |> should equal true
+ InferedType.CanHaveEmptyValues(typeof) |> should equal false
+ InferedType.CanHaveEmptyValues(typeof) |> should equal false
+
+ []
+ member _.``InferedType EnsuresHandlesMissingValues with allowEmptyValues true``() =
+ let primitiveString = InferedType.Primitive(typeof, None, false, false)
+ let primitiveInt = InferedType.Primitive(typeof, None, false, false)
+
+ let result1 = primitiveString.EnsuresHandlesMissingValues(true)
+ let result2 = primitiveInt.EnsuresHandlesMissingValues(true)
+
+ result1 |> should equal primitiveString // string can have empty values, so no change
+ match result2 with
+ | InferedType.Primitive(typ, _, true, _) when typ = typeof -> ()
+ | _ -> failwith "Expected optional int primitive"
+
+ []
+ member _.``InferedType EnsuresHandlesMissingValues with allowEmptyValues false``() =
+ let primitiveString = InferedType.Primitive(typeof, None, false, false)
+
+ let result = primitiveString.EnsuresHandlesMissingValues(false)
+
+ match result with
+ | InferedType.Primitive(typ, _, true, _) when typ = typeof -> ()
+ | _ -> failwith "Expected optional string primitive"
+
+ []
+ member _.``InferedType EnsuresHandlesMissingValues handles already optional types``() =
+ let primitiveOptional = InferedType.Primitive(typeof, None, true, false)
+ let recordOptional = InferedType.Record(None, [], true)
+ let jsonOptional = InferedType.Json(InferedType.Primitive(typeof, None, false, false), true)
+
+ primitiveOptional.EnsuresHandlesMissingValues(false) |> should equal primitiveOptional
+ recordOptional.EnsuresHandlesMissingValues(false) |> should equal recordOptional
+ jsonOptional.EnsuresHandlesMissingValues(false) |> should equal jsonOptional
+
+ []
+ member _.``InferedType EnsuresHandlesMissingValues handles Null and Top``() =
+ InferedType.Null.EnsuresHandlesMissingValues(false) |> should equal InferedType.Null
+
+ (fun () -> InferedType.Top.EnsuresHandlesMissingValues(false) |> ignore)
+ |> should throw typeof
+
+ []
+ member _.``InferedType GetDropOptionality returns correct values``() =
+ let primitiveOptional = InferedType.Primitive(typeof, None, true, false)
+ let primitiveRequired = InferedType.Primitive(typeof, None, false, false)
+
+ let result1, wasOptional1 = primitiveOptional.GetDropOptionality()
+ let result2, wasOptional2 = primitiveRequired.GetDropOptionality()
+
+ match result1 with
+ | InferedType.Primitive(typ, _, false, _) when typ = typeof -> ()
+ | _ -> failwith "Expected non-optional string primitive"
+ wasOptional1 |> should equal true
+
+ result2 |> should equal primitiveRequired
+ wasOptional2 |> should equal false
+
+ []
+ member _.``InferedType DropOptionality works correctly``() =
+ let primitiveOptional = InferedType.Primitive(typeof, None, true, false)
+
+ let result = primitiveOptional.DropOptionality()
+
+ match result with
+ | InferedType.Primitive(typ, _, false, _) when typ = typeof -> ()
+ | _ -> failwith "Expected non-optional string primitive"
+
+ []
+ member _.``InferedType Equals works with reference equality``() =
+ let type1 = InferedType.Primitive(typeof, None, false, false)
+ let type2 = type1 // same reference
+ let type3 = InferedType.Primitive(typeof, None, false, false) // different reference, same content
+
+ type1.Equals(type2) |> should equal true
+ type1.Equals(type3) |> should equal true
+
+ []
+ member _.``InferedType Equals works with structural equality``() =
+ let type1 = InferedType.Primitive(typeof, None, false, false)
+ let type2 = InferedType.Primitive(typeof, None, false, false)
+ let type3 = InferedType.Primitive(typeof, None, false, false)
+
+ type1.Equals(type2) |> should equal true
+ type1.Equals(type3) |> should equal false
+
+ []
+ member _.``InferedType Equals with different types returns false``() =
+ let inferredType = InferedType.Primitive(typeof, None, false, false)
+ let otherObject = "not an InferedType"
+
+ inferredType.Equals(otherObject) |> should equal false
+
+ []
+ member _.``InferedType ToString produces meaningful output``() =
+ let primitiveType = InferedType.Primitive(typeof, None, false, false)
+ let nullType = InferedType.Null
+ let topType = InferedType.Top
+
+ primitiveType.ToString() |> should contain "Primitive"
+ nullType.ToString() |> should contain "Null"
+ topType.ToString() |> should contain "Top"
+
+[]
+type PrimitiveInferedValueTests() =
+
+ []
+ member _.``PrimitiveInferedValue Create with TypeWrapper works correctly``() =
+ let result = PrimitiveInferedValue.Create(typeof, TypeWrapper.Option, Some(typeof))
+
+ result.InferedType |> should equal typeof
+ result.RuntimeType |> should equal typeof
+ result.UnitOfMeasure |> should equal (Some(typeof))
+ result.TypeWrapper |> should equal TypeWrapper.Option
+
+ []
+ member _.``PrimitiveInferedValue Create with optional boolean works correctly``() =
+ let result = PrimitiveInferedValue.Create(typeof, true, None)
+
+ result.InferedType |> should equal typeof
+ result.RuntimeType |> should equal typeof
+ result.UnitOfMeasure |> should equal None
+ result.TypeWrapper |> should equal TypeWrapper.Option
+
+ []
+ member _.``PrimitiveInferedValue Create handles Bit types correctly``() =
+ let bitResult = PrimitiveInferedValue.Create(typeof, TypeWrapper.None, None)
+ let bit0Result = PrimitiveInferedValue.Create(typeof, TypeWrapper.None, None)
+ let bit1Result = PrimitiveInferedValue.Create(typeof, TypeWrapper.None, None)
+
+ bitResult.InferedType |> should equal typeof
+ bitResult.RuntimeType |> should equal typeof
+
+ bit0Result.InferedType |> should equal typeof
+ bit0Result.RuntimeType |> should equal typeof
+
+ bit1Result.InferedType |> should equal typeof
+ bit1Result.RuntimeType |> should equal typeof
+
+[]
+type PrimitiveInferedPropertyTests() =
+
+ []
+ member _.``PrimitiveInferedProperty Create with TypeWrapper works correctly``() =
+ let result = PrimitiveInferedProperty.Create("TestProp", typeof, TypeWrapper.Nullable, Some(typeof))
+
+ result.Name |> should equal "TestProp"
+ result.Value.InferedType |> should equal typeof
+ result.Value.TypeWrapper |> should equal TypeWrapper.Nullable
+ result.Value.UnitOfMeasure |> should equal (Some(typeof))
+
+ []
+ member _.``PrimitiveInferedProperty Create with optional boolean works correctly``() =
+ let result = PrimitiveInferedProperty.Create("OptionalProp", typeof, false, None)
+
+ result.Name |> should equal "OptionalProp"
+ result.Value.InferedType |> should equal typeof
+ result.Value.TypeWrapper |> should equal TypeWrapper.None
+ result.Value.UnitOfMeasure |> should equal None
+
+[]
+type TypeWrapperTests() =
+
+ []
+ member _.``TypeWrapper FromOption returns correct values``() =
+ TypeWrapper.FromOption(true) |> should equal TypeWrapper.Option
+ TypeWrapper.FromOption(false) |> should equal TypeWrapper.None
+
+[]
+type BitTypesTests() =
+
+ []
+ member _.``Bit types are struct types``() =
+ typeof.IsValueType |> should equal true
+ typeof.IsValueType |> should equal true
+ typeof.IsValueType |> should equal true
+
+ []
+ member _.``Bit types equality works``() =
+ let bit = Bit.Bit
+ let bit0 = Bit0.Bit0
+ let bit1 = Bit1.Bit1
+
+ bit |> should equal Bit.Bit
+ bit0 |> should equal Bit0.Bit0
+ bit1 |> should equal Bit1.Bit1
\ No newline at end of file