diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Samples/ExampleMockValueBuilder.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Samples/ExampleMockValueBuilder.cs new file mode 100644 index 00000000000..fd592a24e94 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Samples/ExampleMockValueBuilder.cs @@ -0,0 +1,280 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.TypeSpec.Generator.Input; + +namespace Microsoft.TypeSpec.Generator.Samples +{ + /// + /// Generates mock instances for operations + /// that don't have examples provided in the spec. + /// + public static class ExampleMockValueBuilder + { + /// + /// Example key for the variant that includes only required parameters. + /// + public const string ShortVersionKey = "ShortVersion"; + + /// + /// Example key for the variant that includes all parameters. + /// + public const string AllParametersKey = "AllParameters"; + + private const string DefaultEndpointValue = "http://localhost:3000"; + + /// + /// Builds mock operation examples for the given operation. + /// Produces two variants: "ShortVersion" (required params only) and "AllParameters" (all params). + /// + public static IReadOnlyList BuildOperationExamples(InputOperation operation) + { + return new[] + { + BuildOperationExample(operation, ShortVersionKey, useAllParameters: false), + BuildOperationExample(operation, AllParametersKey, useAllParameters: true) + }; + } + + private static InputOperationExample BuildOperationExample(InputOperation operation, string name, bool useAllParameters) + { + var parameterExamples = new List(operation.Parameters.Count); + foreach (var parameter in operation.Parameters) + { + if (!useAllParameters && !parameter.IsRequired) + { + continue; + } + var parameterExample = BuildParameterExample(parameter, useAllParameters); + parameterExamples.Add(parameterExample); + } + + return new InputOperationExample(name, null, parameterExamples, string.Empty); + } + + /// + /// Builds a mock example value for a single parameter. + /// + internal static InputParameterExample BuildParameterExample(InputParameter parameter, bool useAllParameters) + { + // Constant parameters use their constant value directly + if (parameter.Scope == InputParameterScope.Constant) + { + var value = GetConstantValue(parameter); + return new InputParameterExample(parameter, value); + } + + // Endpoint parameters use a mock URL + if (parameter is InputEndpointParameter { IsEndpoint: true }) + { + var value = InputExampleValue.Value(parameter.Type, DefaultEndpointValue); + return new InputParameterExample(parameter, value); + } + + // Parameters with default values use those + if (parameter.DefaultValue != null) + { + var value = InputExampleValue.Value(parameter.Type, parameter.DefaultValue.Value); + return new InputParameterExample(parameter, value); + } + + // Everything else: generate a mock value based on the type + var exampleValue = BuildExampleValue(parameter.Type, parameter.Name, useAllParameters, new HashSet()); + return new InputParameterExample(parameter, exampleValue); + } + + private static InputExampleValue GetConstantValue(InputParameter parameter) + { + if (parameter.Type is InputLiteralType { Value: not null } literal) + { + return InputExampleValue.Value(parameter.Type, literal.Value); + } + if (parameter.DefaultValue != null) + { + return InputExampleValue.Value(parameter.Type, parameter.DefaultValue.Value); + } + if (parameter.Type is InputUnionType unionType && unionType.VariantTypes[0] is InputLiteralType literalVariant) + { + return InputExampleValue.Value(parameter.Type, literalVariant.Value); + } + if (parameter.Type is InputEnumType enumType && enumType.Values.Count > 0) + { + return InputExampleValue.Value(parameter.Type, enumType.Values[0].Value); + } + return InputExampleValue.Null(parameter.Type); + } + + /// + /// Builds a mock example value for the given input type. + /// This is the main dispatch method that handles all type kinds. + /// + internal static InputExampleValue BuildExampleValue(InputType type, string? hint, bool useAllParameters, HashSet visitedModels) => type switch + { + InputArrayType arrayType => BuildListExampleValue(arrayType, hint, useAllParameters, visitedModels), + InputDictionaryType dictType => BuildDictionaryExampleValue(dictType, hint, useAllParameters, visitedModels), + InputEnumType enumType => BuildEnumExampleValue(enumType), + InputPrimitiveType primitiveType => BuildPrimitiveExampleValue(primitiveType, hint), + InputLiteralType literalType => InputExampleValue.Value(literalType, literalType.Value), + InputModelType modelType => BuildModelExampleValue(modelType, useAllParameters, visitedModels), + InputUnionType unionType => BuildExampleValue(unionType.VariantTypes[0], hint, useAllParameters, visitedModels), + InputNullableType nullableType => BuildExampleValue(nullableType.Type, hint, useAllParameters, visitedModels), + InputDateTimeType dateTimeType => BuildDateTimeExampleValue(dateTimeType), + InputDurationType durationType => BuildDurationExampleValue(durationType), + _ => InputExampleValue.Object(type, new Dictionary()) + }; + + private static InputExampleValue BuildListExampleValue(InputArrayType arrayType, string? hint, bool useAllParameters, HashSet visitedModels) + { + var elementValue = BuildExampleValue(arrayType.ValueType, hint, useAllParameters, visitedModels); + return InputExampleValue.List(arrayType, new[] { elementValue }); + } + + private static InputExampleValue BuildDictionaryExampleValue(InputDictionaryType dictType, string? hint, bool useAllParameters, HashSet visitedModels) + { + var valueExample = BuildExampleValue(dictType.ValueType, hint, useAllParameters, visitedModels); + return InputExampleValue.Object(dictType, new Dictionary + { + ["key"] = valueExample + }); + } + + private static InputExampleValue BuildEnumExampleValue(InputEnumType enumType) + { + var firstValue = enumType.Values.FirstOrDefault(); + return firstValue != null + ? InputExampleValue.Value(enumType, firstValue.Value) + : InputExampleValue.Null(enumType); + } + + private static InputExampleValue BuildPrimitiveExampleValue(InputPrimitiveType primitiveType, string? hint) => primitiveType.Kind switch + { + InputPrimitiveTypeKind.Boolean => InputExampleValue.Value(primitiveType, true), + InputPrimitiveTypeKind.Int8 => InputExampleValue.Value(primitiveType, (sbyte)123), + InputPrimitiveTypeKind.UInt8 => InputExampleValue.Value(primitiveType, (byte)123), + InputPrimitiveTypeKind.Int16 => InputExampleValue.Value(primitiveType, (short)1234), + InputPrimitiveTypeKind.Int32 or InputPrimitiveTypeKind.Integer => InputExampleValue.Value(primitiveType, 1234), + InputPrimitiveTypeKind.Int64 or InputPrimitiveTypeKind.SafeInt => InputExampleValue.Value(primitiveType, 1234L), + InputPrimitiveTypeKind.UInt16 => InputExampleValue.Value(primitiveType, (ushort)1234), + InputPrimitiveTypeKind.UInt32 => InputExampleValue.Value(primitiveType, (uint)1234), + InputPrimitiveTypeKind.UInt64 => InputExampleValue.Value(primitiveType, (ulong)1234), + InputPrimitiveTypeKind.Float32 or InputPrimitiveTypeKind.Float => InputExampleValue.Value(primitiveType, 123.45f), + InputPrimitiveTypeKind.Float64 or InputPrimitiveTypeKind.Numeric => InputExampleValue.Value(primitiveType, 123.45), + InputPrimitiveTypeKind.Decimal or InputPrimitiveTypeKind.Decimal128 => InputExampleValue.Value(primitiveType, 123.45m), + InputPrimitiveTypeKind.String => BuildStringExampleValue(primitiveType, hint), + InputPrimitiveTypeKind.Url => InputExampleValue.Value(primitiveType, "http://localhost:3000"), + InputPrimitiveTypeKind.PlainDate => InputExampleValue.Value(primitiveType, "2022-05-10"), + InputPrimitiveTypeKind.PlainTime => InputExampleValue.Value(primitiveType, "01:23:45"), + InputPrimitiveTypeKind.Stream => InputExampleValue.Stream(primitiveType, ""), + InputPrimitiveTypeKind.Bytes => InputExampleValue.Value(primitiveType, "dGVzdA=="), // base64 for "test" + _ => InputExampleValue.Object(primitiveType, new Dictionary()) + }; + + private static InputExampleValue BuildStringExampleValue(InputPrimitiveType primitiveType, string? hint) + { + // UUID-typed strings get a mock UUID + if (primitiveType.Encode == "uuid") + { + return InputExampleValue.Value(primitiveType, "73f411fe-4f43-4b4b-9cbd-6828d8f4cf9a"); + } + + return string.IsNullOrWhiteSpace(hint) + ? InputExampleValue.Value(primitiveType, "") + : InputExampleValue.Value(primitiveType, $"<{hint}>"); + } + + private static InputExampleValue BuildDateTimeExampleValue(InputDateTimeType dateTimeType) + { + if (dateTimeType.Encode == DateTimeKnownEncoding.Rfc7231) + return InputExampleValue.Value(dateTimeType.WireType, "Tue, 10 May 2022 18:57:31 GMT"); + if (dateTimeType.Encode == DateTimeKnownEncoding.Rfc3339) + return InputExampleValue.Value(dateTimeType.WireType, "2022-05-10T18:57:31.2311892Z"); + if (dateTimeType.Encode == DateTimeKnownEncoding.UnixTimestamp) + return InputExampleValue.Value(dateTimeType.WireType, 1652209051); + + return InputExampleValue.Null(dateTimeType); + } + + private static InputExampleValue BuildDurationExampleValue(InputDurationType durationType) + { + if (durationType.Encode == DurationKnownEncoding.Iso8601) + return InputExampleValue.Value(durationType.WireType, "PT1H23M45S"); + + if (durationType.Encode == DurationKnownEncoding.Seconds) + { + return durationType.WireType.Kind switch + { + InputPrimitiveTypeKind.Int32 => InputExampleValue.Value(durationType.WireType, 10), + InputPrimitiveTypeKind.Float or InputPrimitiveTypeKind.Float32 => InputExampleValue.Value(durationType.WireType, 10f), + _ => InputExampleValue.Value(durationType.WireType, 3.141592) + }; + } + + return InputExampleValue.Null(durationType); + } + + private static InputExampleValue BuildModelExampleValue(InputModelType model, bool useAllParameters, HashSet visitedModels) + { + // Cycle detection: if we've already visited this model, return null to break the loop + if (visitedModels.Contains(model)) + return InputExampleValue.Null(model); + + var properties = new Dictionary(); + var result = InputExampleValue.Object(model, properties); + visitedModels.Add(model); + + // If this model has a discriminator, choose the first derived type + if (model.DiscriminatorProperty != null && model.DerivedModels.Count > 0) + { + var derived = model.DerivedModels.FirstOrDefault(m => !m.IsUnknownDiscriminatorModel); + if (derived != null) + { + model = derived; + } + else + { + return InputExampleValue.Null(model); + } + } + + // Iterate all properties from this model and its base models + foreach (var modelInChain in model.GetSelfAndBaseModels()) + { + foreach (var property in modelInChain.Properties) + { + // Skip read-only properties (they can't be set) + if (property.IsReadOnly) + continue; + + // In ShortVersion, skip optional properties + if (!useAllParameters && !property.IsRequired) + continue; + + // Skip duplicate properties (keep the one from the most-derived type) + if (properties.ContainsKey(property.SerializedName)) + continue; + + InputExampleValue exampleValue; + if (property.IsDiscriminator && model.DiscriminatorValue != null) + { + exampleValue = InputExampleValue.Value(property.Type, model.DiscriminatorValue); + } + else if (property.DefaultValue is { Value: not null } defaultValue) + { + exampleValue = InputExampleValue.Value(property.Type, defaultValue.Value); + } + else + { + exampleValue = BuildExampleValue(property.Type, property.SerializedName, useAllParameters, visitedModels); + } + + properties.Add(property.SerializedName, exampleValue); + } + } + + return result; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Samples/ExampleMockValueBuilderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Samples/ExampleMockValueBuilderTests.cs new file mode 100644 index 00000000000..232067ce99d --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Samples/ExampleMockValueBuilderTests.cs @@ -0,0 +1,536 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.TypeSpec.Generator.Input; +using Microsoft.TypeSpec.Generator.Samples; +using Microsoft.TypeSpec.Generator.Tests.Common; +using NUnit.Framework; + +namespace Microsoft.TypeSpec.Generator.Tests.Samples +{ + public class ExampleMockValueBuilderTests + { + // ----------------------------------------------------------------------- + // BuildOperationExamples — top-level behavior + // ----------------------------------------------------------------------- + + [Test] + public void BuildOperationExamples_ProducesTwoVariants() + { + var operation = InputFactory.Operation("TestOp", parameters: [ + InputFactory.PathParameter("id", InputPrimitiveType.String, isRequired: true) + ]); + + var examples = ExampleMockValueBuilder.BuildOperationExamples(operation); + + Assert.AreEqual(2, examples.Count); + Assert.AreEqual(ExampleMockValueBuilder.ShortVersionKey, examples[0].Name); + Assert.AreEqual(ExampleMockValueBuilder.AllParametersKey, examples[1].Name); + } + + [Test] + public void ShortVersion_SkipsOptionalParameters() + { + var operation = InputFactory.Operation("TestOp", parameters: [ + InputFactory.PathParameter("requiredId", InputPrimitiveType.String, isRequired: true), + InputFactory.QueryParameter("optionalFilter", InputPrimitiveType.String, isRequired: false) + ]); + + var examples = ExampleMockValueBuilder.BuildOperationExamples(operation); + var shortVersion = examples[0]; + var allParams = examples[1]; + + Assert.AreEqual(1, shortVersion.Parameters.Count); + Assert.AreEqual("requiredId", shortVersion.Parameters[0].Parameter.Name); + + Assert.AreEqual(2, allParams.Parameters.Count); + } + + [Test] + public void AllParameters_IncludesOptionalParameters() + { + var operation = InputFactory.Operation("TestOp", parameters: [ + InputFactory.PathParameter("requiredId", InputPrimitiveType.String, isRequired: true), + InputFactory.QueryParameter("optionalFilter", InputPrimitiveType.String, isRequired: false), + InputFactory.HeaderParameter("optionalHeader", InputPrimitiveType.Int32, isRequired: false) + ]); + + var examples = ExampleMockValueBuilder.BuildOperationExamples(operation); + var allParams = examples[1]; + + Assert.AreEqual(3, allParams.Parameters.Count); + } + + [Test] + public void EmptyFilePath_ForMockExamples() + { + var operation = InputFactory.Operation("TestOp", parameters: [ + InputFactory.PathParameter("id", InputPrimitiveType.String, isRequired: true) + ]); + + var examples = ExampleMockValueBuilder.BuildOperationExamples(operation); + + Assert.AreEqual(string.Empty, examples[0].FilePath); + Assert.AreEqual(string.Empty, examples[1].FilePath); + } + + // ----------------------------------------------------------------------- + // Primitive type mock values + // ----------------------------------------------------------------------- + + [Test] + public void PrimitiveType_Boolean() + { + var value = BuildExampleValueForType(InputPrimitiveType.Boolean); + AssertRawValue(value, true); + } + + [Test] + public void PrimitiveType_Int32() + { + var value = BuildExampleValueForType(InputPrimitiveType.Int32); + AssertRawValue(value, 1234); + } + + [Test] + public void PrimitiveType_Int64() + { + var value = BuildExampleValueForType(InputPrimitiveType.Int64); + AssertRawValue(value, 1234L); + } + + [Test] + public void PrimitiveType_Float32() + { + var value = BuildExampleValueForType(InputPrimitiveType.Float32); + AssertRawValue(value, 123.45f); + } + + [Test] + public void PrimitiveType_Float64() + { + var value = BuildExampleValueForType(InputPrimitiveType.Float64); + AssertRawValue(value, 123.45); + } + + [Test] + public void PrimitiveType_String_WithHint() + { + var value = ExampleMockValueBuilder.BuildExampleValue( + InputPrimitiveType.String, "myParam", false, new HashSet()); + + AssertRawValue(value, ""); + } + + [Test] + public void PrimitiveType_String_WithoutHint() + { + var value = ExampleMockValueBuilder.BuildExampleValue( + InputPrimitiveType.String, null, false, new HashSet()); + + AssertRawValue(value, ""); + } + + [Test] + public void PrimitiveType_Url() + { + var value = BuildExampleValueForType(InputPrimitiveType.Url); + AssertRawValue(value, "http://localhost:3000"); + } + + [Test] + public void PrimitiveType_PlainDate() + { + var value = BuildExampleValueForType(InputPrimitiveType.PlainDate); + AssertRawValue(value, "2022-05-10"); + } + + [Test] + public void PrimitiveType_PlainTime() + { + var value = BuildExampleValueForType(InputPrimitiveType.PlainTime); + AssertRawValue(value, "01:23:45"); + } + + [Test] + public void PrimitiveType_Bytes() + { + var value = BuildExampleValueForType(InputPrimitiveType.Base64); + AssertRawValue(value, "dGVzdA=="); + } + + [Test] + public void PrimitiveType_Uuid_String() + { + var uuidType = new InputPrimitiveType(InputPrimitiveTypeKind.String, "string", "TypeSpec.string", "uuid"); + var value = ExampleMockValueBuilder.BuildExampleValue( + uuidType, "id", false, new HashSet()); + + AssertRawValue(value, "73f411fe-4f43-4b4b-9cbd-6828d8f4cf9a"); + } + + // ----------------------------------------------------------------------- + // DateTime mock values + // ----------------------------------------------------------------------- + + [Test] + public void DateTime_Rfc3339() + { + var dateTimeType = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, "dateTime", "TypeSpec.utcDateTime", InputPrimitiveType.String); + var value = ExampleMockValueBuilder.BuildExampleValue( + dateTimeType, null, false, new HashSet()); + + AssertRawValue(value, "2022-05-10T18:57:31.2311892Z"); + } + + [Test] + public void DateTime_Rfc7231() + { + var dateTimeType = new InputDateTimeType( + DateTimeKnownEncoding.Rfc7231, "dateTime", "TypeSpec.utcDateTime", InputPrimitiveType.String); + var value = ExampleMockValueBuilder.BuildExampleValue( + dateTimeType, null, false, new HashSet()); + + AssertRawValue(value, "Tue, 10 May 2022 18:57:31 GMT"); + } + + [Test] + public void DateTime_UnixTimestamp() + { + var dateTimeType = new InputDateTimeType( + DateTimeKnownEncoding.UnixTimestamp, "dateTime", "TypeSpec.utcDateTime", InputPrimitiveType.Int64); + var value = ExampleMockValueBuilder.BuildExampleValue( + dateTimeType, null, false, new HashSet()); + + AssertRawValue(value, 1652209051); + } + + // ----------------------------------------------------------------------- + // Duration mock values + // ----------------------------------------------------------------------- + + [Test] + public void Duration_Iso8601() + { + var durationType = new InputDurationType( + DurationKnownEncoding.Iso8601, "duration", "TypeSpec.duration", InputPrimitiveType.String, null); + var value = ExampleMockValueBuilder.BuildExampleValue( + durationType, null, false, new HashSet()); + + AssertRawValue(value, "PT1H23M45S"); + } + + [Test] + public void Duration_Seconds_Int32() + { + var durationType = new InputDurationType( + DurationKnownEncoding.Seconds, "duration", "TypeSpec.duration", InputPrimitiveType.Int32, null); + var value = ExampleMockValueBuilder.BuildExampleValue( + durationType, null, false, new HashSet()); + + AssertRawValue(value, 10); + } + + // ----------------------------------------------------------------------- + // Collection mock values + // ----------------------------------------------------------------------- + + [Test] + public void Array_ProducesSingleElementList() + { + var arrayType = new InputArrayType("list", "TypeSpec.Array", InputPrimitiveType.Int32); + var value = ExampleMockValueBuilder.BuildExampleValue( + arrayType, null, false, new HashSet()); + + // The value should be a list type — verify via the Type property + Assert.AreEqual(arrayType, value.Type); + // We can't access internal Values directly, but we can verify it's not null/raw + AssertIsNotRawNull(value); + } + + [Test] + public void Dictionary_ProducesSingleKeyEntry() + { + var dictType = new InputDictionaryType("dict", InputPrimitiveType.String, InputPrimitiveType.Int32); + var value = ExampleMockValueBuilder.BuildExampleValue( + dictType, null, false, new HashSet()); + + Assert.AreEqual(dictType, value.Type); + AssertIsNotRawNull(value); + } + + // ----------------------------------------------------------------------- + // Enum mock values + // ----------------------------------------------------------------------- + + [Test] + public void Enum_ReturnsFirstEnumValue() + { + var enumType = InputFactory.StringEnum("Color", [("Red", "red"), ("Blue", "blue")]); + var value = ExampleMockValueBuilder.BuildExampleValue( + enumType, null, false, new HashSet()); + + AssertRawValue(value, "red"); + } + + [Test] + public void Enum_Int32_ReturnsFirstValue() + { + var enumType = InputFactory.Int32Enum("Status", [("Active", 1), ("Inactive", 0)]); + var value = ExampleMockValueBuilder.BuildExampleValue( + enumType, null, false, new HashSet()); + + AssertRawValue(value, 1); + } + + // ----------------------------------------------------------------------- + // Literal type + // ----------------------------------------------------------------------- + + [Test] + public void LiteralType_ReturnsLiteralValue() + { + var literalType = InputFactory.Literal.String("fixedValue"); + var value = ExampleMockValueBuilder.BuildExampleValue( + literalType, null, false, new HashSet()); + + AssertRawValue(value, "fixedValue"); + } + + // ----------------------------------------------------------------------- + // Union type + // ----------------------------------------------------------------------- + + [Test] + public void UnionType_ReturnsFirstVariant() + { + var unionType = new InputUnionType("myUnion", [InputPrimitiveType.String, InputPrimitiveType.Int32]); + var value = ExampleMockValueBuilder.BuildExampleValue( + unionType, "field", false, new HashSet()); + + // Should pick the first variant (String) and mock it + AssertRawValue(value, ""); + } + + // ----------------------------------------------------------------------- + // Nullable type + // ----------------------------------------------------------------------- + + [Test] + public void NullableType_UnwrapsAndMocksInner() + { + var nullableType = new InputNullableType(InputPrimitiveType.Int32); + var value = ExampleMockValueBuilder.BuildExampleValue( + nullableType, null, false, new HashSet()); + + AssertRawValue(value, 1234); + } + + // ----------------------------------------------------------------------- + // Model mock values + // ----------------------------------------------------------------------- + + [Test] + public void Model_PopulatesRequiredProperties() + { + var model = InputFactory.Model("Widget", properties: [ + InputFactory.Property("name", InputPrimitiveType.String, isRequired: true), + InputFactory.Property("optionalColor", InputPrimitiveType.String, isRequired: false) + ]); + + var value = ExampleMockValueBuilder.BuildExampleValue( + model, null, false, new HashSet()); + + // Should be an object value (not null/raw) + Assert.AreEqual(model, value.Type); + AssertIsNotRawNull(value); + } + + [Test] + public void Model_PopulatesAllProperties_WhenUseAll() + { + var model = InputFactory.Model("Widget", properties: [ + InputFactory.Property("name", InputPrimitiveType.String, isRequired: true), + InputFactory.Property("optionalColor", InputPrimitiveType.String, isRequired: false) + ]); + + var value = ExampleMockValueBuilder.BuildExampleValue( + model, null, true, new HashSet()); + + Assert.AreEqual(model, value.Type); + AssertIsNotRawNull(value); + } + + [Test] + public void Model_SkipsReadOnlyProperties() + { + var model = InputFactory.Model("Widget", properties: [ + InputFactory.Property("name", InputPrimitiveType.String, isRequired: true), + InputFactory.Property("id", InputPrimitiveType.String, isRequired: true, isReadOnly: true) + ]); + + var value = ExampleMockValueBuilder.BuildExampleValue( + model, null, true, new HashSet()); + + Assert.AreEqual(model, value.Type); + AssertIsNotRawNull(value); + } + + [Test] + public void Model_IncludesBaseModelProperties() + { + var baseModel = InputFactory.Model("BaseWidget", properties: [ + InputFactory.Property("baseField", InputPrimitiveType.String, isRequired: true) + ]); + var derivedModel = InputFactory.Model("DerivedWidget", baseModel: baseModel, properties: [ + InputFactory.Property("derivedField", InputPrimitiveType.Int32, isRequired: true) + ]); + + var value = ExampleMockValueBuilder.BuildExampleValue( + derivedModel, null, true, new HashSet()); + + AssertIsNotRawNull(value); + } + + [Test] + public void Model_CircularReference_ReturnsNull() + { + var selfRefModel = InputFactory.Model("Node", properties: [ + InputFactory.Property("name", InputPrimitiveType.String, isRequired: true) + ]); + + // Pre-add to visited set to simulate circular reference + var visited = new HashSet { selfRefModel }; + var value = ExampleMockValueBuilder.BuildExampleValue( + selfRefModel, null, true, visited); + + // Circular reference should produce a null value + Assert.IsNotNull(value); + Assert.AreEqual(selfRefModel, value.Type); + } + + [Test] + public void Model_WithDiscriminator_ChoosesDerivedType() + { + var baseModel = InputFactory.Model("Pet", + properties: [ + InputFactory.Property("kind", InputPrimitiveType.String, isRequired: true, isDiscriminator: true) + ], + discriminatedModels: new Dictionary()); + + var catModel = InputFactory.Model("Cat", + discriminatedKind: "cat", + baseModel: baseModel, + properties: [ + InputFactory.Property("meow", InputPrimitiveType.Boolean, isRequired: true) + ]); + + var value = ExampleMockValueBuilder.BuildExampleValue( + baseModel, null, true, new HashSet()); + + // Should produce a non-null object value (derived type selected) + Assert.IsNotNull(value); + AssertIsNotRawNull(value); + } + + // ----------------------------------------------------------------------- + // Parameter handling + // ----------------------------------------------------------------------- + + [Test] + public void Parameter_Endpoint_UsesMockUrl() + { + var endpointParam = InputFactory.EndpointParameter("endpoint", InputPrimitiveType.Url, isRequired: true); + var operation = InputFactory.Operation("TestOp", parameters: [endpointParam]); + + var examples = ExampleMockValueBuilder.BuildOperationExamples(operation); + var paramExample = examples[0].Parameters[0]; + + AssertRawValue(paramExample.ExampleValue, "http://localhost:3000"); + } + + [Test] + public void Parameter_Constant_UsesConstantValue() + { + var constParam = InputFactory.HeaderParameter("apiVersion", InputFactory.Literal.String("2024-01-01"), + isRequired: true, scope: InputParameterScope.Constant, + defaultValue: InputFactory.Constant.String("2024-01-01")); + + var operation = InputFactory.Operation("TestOp", parameters: [constParam]); + + var examples = ExampleMockValueBuilder.BuildOperationExamples(operation); + var paramExample = examples[0].Parameters[0]; + + AssertRawValue(paramExample.ExampleValue, "2024-01-01"); + } + + [Test] + public void Parameter_WithDefaultValue_UsesDefault() + { + var paramWithDefault = InputFactory.QueryParameter("count", InputPrimitiveType.Int32, + isRequired: true, defaultValue: new InputConstant(10, InputPrimitiveType.Int32)); + + var operation = InputFactory.Operation("TestOp", parameters: [paramWithDefault]); + + var examples = ExampleMockValueBuilder.BuildOperationExamples(operation); + var paramExample = examples[0].Parameters[0]; + + AssertRawValue(paramExample.ExampleValue, 10); + } + + [Test] + public void OperationWithNoParameters_ProducesEmptyExamples() + { + var operation = InputFactory.Operation("EmptyOp", parameters: []); + + var examples = ExampleMockValueBuilder.BuildOperationExamples(operation); + + Assert.AreEqual(2, examples.Count); + Assert.AreEqual(0, examples[0].Parameters.Count); + Assert.AreEqual(0, examples[1].Parameters.Count); + } + + // ----------------------------------------------------------------------- + // Helpers + // ----------------------------------------------------------------------- + + private static InputExampleValue BuildExampleValueForType(InputType type) + { + return ExampleMockValueBuilder.BuildExampleValue(type, null, false, new HashSet()); + } + + /// + /// Asserts that the value is a raw example value matching the expected object. + /// Since InputExampleRawValue is internal, we use reflection to access RawValue. + /// + private static void AssertRawValue(InputExampleValue value, object expected) + { + var expectedValue = InputExampleValue.Value(value.Type, expected); + Assert.AreEqual(expectedValue.GetType(), value.GetType(), + $"Expected raw value type but got {value.GetType().Name}"); + var rawValueProp = value.GetType().GetProperty("RawValue"); + Assert.IsNotNull(rawValueProp, "Expected value to have RawValue property"); + var actualRaw = rawValueProp!.GetValue(value); + Assert.AreEqual(expected, actualRaw); + } + + /// + /// Asserts the value is not a null raw value — it should be a composite type (list, object). + /// + private static void AssertIsNotRawNull(InputExampleValue value) + { + var rawValueProp = value.GetType().GetProperty("RawValue"); + if (rawValueProp != null) + { + // It's a raw value — it should not be null (meaning it was a real value, not a cycle) + var rawVal = rawValueProp.GetValue(value); + Assert.IsNotNull(rawVal, "Expected a non-null value but got null (possible circular reference issue)"); + } + // If no RawValue property, it's a list/object/stream — that's fine + } + } +}