diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Primitives/ScmKnownParameters.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Primitives/ScmKnownParameters.cs index e07006acf43..336fc35dff0 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Primitives/ScmKnownParameters.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Primitives/ScmKnownParameters.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.ClientModel; using System.ClientModel.Primitives; using System.Text.Json; using System.Threading; @@ -66,5 +67,8 @@ public static ParameterProvider ClientOptions(CSharpType clientOptionsType) public static readonly ParameterProvider NextPage = new ParameterProvider("nextPage", $"The url of the next page of responses.", typeof(Uri)); + + public static readonly ParameterProvider KeyCredential = new("credential", FormattableStringHelpers.Empty, typeof(ApiKeyCredential)); + public static readonly ParameterProvider TokenCredential = new("credential", FormattableStringHelpers.Empty, typeof(AuthenticationTokenProvider)); } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/Samples/ExampleParameterValue.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/Samples/ExampleParameterValue.cs new file mode 100644 index 00000000000..5ff6aa62201 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/Samples/ExampleParameterValue.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.TypeSpec.Generator.Expressions; +using Microsoft.TypeSpec.Generator.Input; +using Microsoft.TypeSpec.Generator.Primitives; + +namespace Microsoft.TypeSpec.Generator.ClientModel.Providers.Samples +{ + /// + /// Represents a parameter value in a sample. Supports two modes: + /// - : raw example data that will be converted to a C# expression later + /// - : a pre-built C# expression (used for known parameters like credentials, endpoints) + /// + public class ExampleParameterValue + { + public ExampleParameterValue(string name, CSharpType type, InputExampleValue value) + { + Name = name; + Type = type; + Value = value; + } + + public ExampleParameterValue(string name, CSharpType type, ValueExpression expression) + { + Name = name; + Type = type; + Expression = expression; + } + + /// + /// The parameter name. + /// + public string Name { get; } + + /// + /// The C# type of the parameter. + /// + public CSharpType Type { get; } + + /// + /// Raw example data from the spec or mock builder. Will be converted to a + /// via . + /// Mutually exclusive with . + /// + public InputExampleValue? Value { get; } + + /// + /// A pre-built C# expression. Used for known parameters (credentials, endpoints) + /// where the expression is fixed regardless of example data. + /// Mutually exclusive with . + /// + public ValueExpression? Expression { get; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/Samples/ExampleValueExpressionBuilder.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/Samples/ExampleValueExpressionBuilder.cs new file mode 100644 index 00000000000..a2c906c9080 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/Samples/ExampleValueExpressionBuilder.cs @@ -0,0 +1,379 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; +using Microsoft.TypeSpec.Generator.Expressions; +using Microsoft.TypeSpec.Generator.Input; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; +using static Microsoft.TypeSpec.Generator.Snippets.Snippet; + +namespace Microsoft.TypeSpec.Generator.ClientModel.Providers.Samples +{ + /// + /// Converts + into (C# AST nodes). + /// This is the bridge between raw mock/spec example data and generated C# code. + /// + public static class ExampleValueExpressionBuilder + { + /// + /// Converts an to a C# expression. + /// If the parameter has a pre-built expression, returns it directly. + /// Otherwise, converts the raw using type information. + /// + public static ValueExpression GetExpression(ExampleParameterValue parameterValue, SerializationFormat format = SerializationFormat.Default) + { + if (parameterValue.Expression != null) + return parameterValue.Expression; + + if (parameterValue.Value != null) + return GetExpression(parameterValue.Type, parameterValue.Value, format); + + return Default; + } + + /// + /// Converts an to a C# expression based on the target . + /// + public static ValueExpression GetExpression(CSharpType type, InputExampleValue exampleValue, SerializationFormat format = SerializationFormat.Default) + { + if (type.IsList) + return GetExpressionForList(type, exampleValue); + if (type.IsDictionary) + return GetExpressionForDictionary(type, exampleValue); + if (type.IsEnum) + return GetExpressionForEnum(type, exampleValue); + if (type is { IsFrameworkType: true }) + return GetExpressionForFrameworkType(type.FrameworkType, exampleValue, format); + + // For model types, fall back to default + return GetExpressionForModel(type, exampleValue); + } + + private static ValueExpression GetExpressionForFrameworkType(Type frameworkType, InputExampleValue exampleValue, SerializationFormat format = SerializationFormat.Default) + { + var rawValue = GetRawValue(exampleValue); + + // String + if (frameworkType == typeof(string)) + { + return rawValue is string s ? Literal(s) : Null; + } + + // Boolean + if (frameworkType == typeof(bool)) + { + return rawValue is bool b ? Literal(b) : Default; + } + + // Integer types + if (frameworkType == typeof(int)) + { + return rawValue != null ? Literal(Convert.ToInt32(rawValue)) : Default; + } + if (frameworkType == typeof(long)) + { + return rawValue != null ? Literal(Convert.ToInt64(rawValue)) : Default; + } + if (frameworkType == typeof(short)) + { + return rawValue != null ? new CastExpression(Literal(Convert.ToInt16(rawValue)), frameworkType) : Default; + } + if (frameworkType == typeof(sbyte)) + { + return rawValue != null ? new CastExpression(Literal(Convert.ToSByte(rawValue)), frameworkType) : Default; + } + if (frameworkType == typeof(byte)) + { + return rawValue != null ? new CastExpression(Literal(Convert.ToByte(rawValue)), frameworkType) : Default; + } + if (frameworkType == typeof(ushort)) + { + return rawValue != null ? new CastExpression(Literal(Convert.ToUInt16(rawValue)), frameworkType) : Default; + } + if (frameworkType == typeof(uint)) + { + return rawValue != null ? new CastExpression(Literal(Convert.ToUInt32(rawValue)), frameworkType) : Default; + } + if (frameworkType == typeof(ulong)) + { + return rawValue != null ? new CastExpression(Literal(Convert.ToUInt64(rawValue)), frameworkType) : Default; + } + + // Float types + if (frameworkType == typeof(float)) + { + return rawValue != null ? Literal(Convert.ToSingle(rawValue)) : Default; + } + if (frameworkType == typeof(double)) + { + return rawValue != null ? Literal(Convert.ToDouble(rawValue)) : Default; + } + if (frameworkType == typeof(decimal)) + { + return rawValue != null ? Literal(Convert.ToDecimal(rawValue)) : Default; + } + + // Guid + if (frameworkType == typeof(Guid)) + { + if (rawValue is string s) + return Static(typeof(Guid)).Invoke(nameof(Guid.Parse), Literal(s)); + return Default; + } + + // Uri + if (frameworkType == typeof(Uri)) + { + if (rawValue is string s) + return New.Instance(typeof(Uri), Literal(s)); + return Null; + } + + // DateTimeOffset + if (frameworkType == typeof(DateTimeOffset)) + { + if (format == SerializationFormat.DateTime_Unix) + { + var unixValue = rawValue is string us ? Convert.ToInt64(us) : rawValue is int or long ? Convert.ToInt64(rawValue) : 0L; + return Static(typeof(DateTimeOffset)).Invoke(nameof(DateTimeOffset.FromUnixTimeSeconds), Literal(unixValue)); + } + if (rawValue is string s) + return Static(typeof(DateTimeOffset)).Invoke(nameof(DateTimeOffset.Parse), Literal(s)); + if (rawValue is int or long) + return Static(typeof(DateTimeOffset)).Invoke(nameof(DateTimeOffset.FromUnixTimeSeconds), Literal(Convert.ToInt64(rawValue))); + return Default; + } + + // TimeSpan + if (frameworkType == typeof(TimeSpan)) + { + if (format is SerializationFormat.Duration_Seconds or SerializationFormat.Duration_Seconds_Float or SerializationFormat.Duration_Milliseconds) + { + if (rawValue is string ds) + return Static(typeof(TimeSpan)).Invoke(nameof(TimeSpan.FromSeconds), Literal(Convert.ToDouble(ds))); + if (rawValue is int or float or double) + return Static(typeof(TimeSpan)).Invoke(nameof(TimeSpan.FromSeconds), Literal(Convert.ToDouble(rawValue))); + } + if (rawValue is string s) + return Static(typeof(XmlConvert)).Invoke(nameof(XmlConvert.ToTimeSpan), Literal(s)); + if (rawValue is int or float or double) + return Static(typeof(TimeSpan)).Invoke(nameof(TimeSpan.FromSeconds), Literal(Convert.ToDouble(rawValue))); + return Default; + } + + // BinaryData + if (frameworkType == typeof(BinaryData)) + { + if (rawValue == null && exampleValue is not InputExampleValue) + return Null; + return GetExpressionForBinaryData(exampleValue); + } + + // byte[] + if (frameworkType == typeof(byte[])) + { + if (rawValue is string s) + return Static(typeof(Encoding)).Property(nameof(Encoding.UTF8)) + .Invoke(nameof(Encoding.GetBytes), Literal(s)); + return Null; + } + + // Stream + if (frameworkType == typeof(Stream)) + { + if (exampleValue is InputExampleStreamValue streamValue) + return Static(typeof(File)).Invoke(nameof(File.OpenRead), Literal(streamValue.Filename)); + return Null; + } + + // Fallback + return frameworkType.IsValueType ? Default : Null; + } + + private static ValueExpression GetExpressionForList(CSharpType listType, InputExampleValue exampleValue) + { + var elementType = listType.ElementType; + var items = new List(); + + if (exampleValue is InputExampleListValue listValue) + { + foreach (var itemValue in listValue.Values) + { + items.Add(GetExpression(elementType, itemValue)); + } + } + + return New.Array(elementType, items.ToArray()); + } + + private static ValueExpression GetExpressionForDictionary(CSharpType dictionaryType, InputExampleValue exampleValue) + { + var keyType = dictionaryType.Arguments[0]; + var valueType = dictionaryType.Arguments[1]; + var entries = new Dictionary(); + + if (exampleValue is InputExampleObjectValue objectValue) + { + foreach (var (key, value) in objectValue.Values) + { + var keyExpr = GetExpression(keyType, InputExampleValue.Value(new InputPrimitiveType(InputPrimitiveTypeKind.String, "string", "TypeSpec.string"), key)); + var valueExpr = GetExpression(valueType, value); + entries[keyExpr] = valueExpr; + } + } + + return New.Dictionary(keyType, valueType, entries); + } + + private static ValueExpression GetExpressionForEnum(CSharpType enumType, InputExampleValue exampleValue) + { + var rawValue = GetRawValue(exampleValue); + if (rawValue == null) + return Default; + + // Access the enum member by name using the type reference + var rawString = rawValue.ToString()!; + // Use the type name as a static access point: EnumType.MemberName + return new MemberExpression(Static(enumType), rawString); + } + + private static ValueExpression GetExpressionForModel(CSharpType type, InputExampleValue exampleValue) + { + if (type.IsValueType) + return Default; + + // Try to resolve the model's TypeProvider to get constructor parameters + if (exampleValue is InputExampleObjectValue objectValue && + CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap.TryGetValue(type, out var typeProvider) && + typeProvider is ModelProvider modelProvider) + { + // Find the public constructor with the most parameters + ConstructorProvider? bestCtor = null; + foreach (var ctor in modelProvider.Constructors) + { + if (ctor.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)) + { + if (bestCtor == null || ctor.Signature.Parameters.Count > bestCtor.Signature.Parameters.Count) + bestCtor = ctor; + } + } + + if (bestCtor != null && bestCtor.Signature.Parameters.Count > 0) + { + var arguments = new List(); + foreach (var param in bestCtor.Signature.Parameters) + { + InputExampleValue? matchedValue = null; + + // Try matching by parameter name + objectValue.Values.TryGetValue(param.Name, out matchedValue); + + // Try matching by wire serialized name + if (matchedValue == null && param.Property?.WireInfo?.SerializedName != null) + objectValue.Values.TryGetValue(param.Property.WireInfo.SerializedName, out matchedValue); + + // Try matching by property name + if (matchedValue == null && param.Property != null) + objectValue.Values.TryGetValue(param.Property.Name, out matchedValue); + + if (matchedValue != null) + { + arguments.Add(GetExpression(param.Type, matchedValue)); + } + else if (param.DefaultValue != null) + { + arguments.Add(param.DefaultValue); + } + else + { + arguments.Add(param.Type.IsValueType ? Default : Null); + } + } + + return New.Instance(type, [.. arguments]); + } + } + + return New.Instance(type); + } + + private static ValueExpression GetExpressionForBinaryData(InputExampleValue exampleValue) + { + // Build an anonymous object from the example value and wrap in BinaryData.FromObjectAsJson + var anonymousObj = GetExpressionForAnonymousObject(exampleValue); + return Static(typeof(BinaryData)).Invoke(nameof(BinaryData.FromObjectAsJson), anonymousObj); + } + + /// + /// Converts an example value to an anonymous object expression for use in + /// BinaryData.FromObjectAsJson() or BinaryContent.Create(). + /// + internal static ValueExpression GetExpressionForAnonymousObject(InputExampleValue exampleValue) + { + if (exampleValue is InputExampleObjectValue objectValue) + { + var properties = new Dictionary(); + foreach (var (key, value) in objectValue.Values) + { + var rawVal = GetRawValue(value); + // Skip null properties in anonymous objects (causes compilation errors) + if (rawVal == null && value is InputExampleRawValue) + continue; + + var valueExpr = GetExpressionForAnonymousObject(value); + properties[Identifier(key)] = valueExpr; + } + return properties.Count > 0 ? New.Anonymous(properties) : New.Instance(typeof(object)); + } + + if (exampleValue is InputExampleListValue listValue) + { + var items = new List(); + foreach (var item in listValue.Values) + { + items.Add(GetExpressionForAnonymousObject(item)); + } + return New.Array(new CSharpType(typeof(object)), items.ToArray()); + } + + if (exampleValue is InputExampleStreamValue streamValue) + { + return Static(typeof(File)).Invoke(nameof(File.OpenRead), Literal(streamValue.Filename)); + } + + // Raw value — convert to literal + var raw = GetRawValue(exampleValue); + if (raw == null) + return Null; + + return raw switch + { + string s => Literal(s), + bool b => Literal(b), + int i => Literal(i), + long l => Literal(l), + float f => Literal(f), + double d => Literal(d), + decimal m => Literal(m), + _ => Literal(raw.ToString()!) + }; + } + + /// + /// Extracts the raw value from an if it's a raw (primitive) value. + /// Returns null for non-raw values (lists, objects, streams). + /// + private static object? GetRawValue(InputExampleValue exampleValue) + { + if (exampleValue is InputExampleRawValue rawValue) + return rawValue.RawValue; + return null; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/Samples/OperationSample.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/Samples/OperationSample.cs new file mode 100644 index 00000000000..292a1882acc --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/Samples/OperationSample.cs @@ -0,0 +1,755 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using Microsoft.TypeSpec.Generator.ClientModel.Primitives; +using Microsoft.TypeSpec.Generator.Expressions; +using Microsoft.TypeSpec.Generator.Input; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; +using Microsoft.TypeSpec.Generator.Statements; +using static Microsoft.TypeSpec.Generator.Snippets.Snippet; + +namespace Microsoft.TypeSpec.Generator.ClientModel.Providers.Samples +{ + /// + /// Represents a single sample for an operation — the bridge between an + /// and the generated C# sample method. + /// Resolves client construction chains, parameter value mappings, and method invocation details. + /// Modeled after the autorest DpgOperationSample pattern. + /// + [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] + public class OperationSample + { + private readonly ClientProvider _client; + private readonly ScmMethodProviderCollection _methodCollection; + private readonly InputServiceMethod _serviceMethod; + private readonly InputOperationExample _example; + private readonly MethodSignature _operationMethodSignature; + + private IReadOnlyList? _clientInvocationChain; + private Dictionary? _parameterValueMapping; + private InputType? _resultType; + + public OperationSample( + ClientProvider client, + ScmMethodProviderCollection methodCollection, + InputServiceMethod serviceMethod, + InputOperationExample example, + bool isConvenienceSample, + string exampleKey) + { + _client = client; + _methodCollection = methodCollection; + _serviceMethod = serviceMethod; + _example = example; + IsConvenienceSample = isConvenienceSample; + ExampleKey = exampleKey; + IsAllParametersUsed = exampleKey == "AllParameters"; + _operationMethodSignature = ResolveOperationSignature(); + } + + // ------------------------------------------------------------------- + // Core identity + // ------------------------------------------------------------------- + + /// + /// The example key, e.g. "ShortVersion" or "AllParameters". + /// + public string ExampleKey { get; } + + /// + /// Whether this is a convenience method sample (true) or protocol method sample (false). + /// + public bool IsConvenienceSample { get; } + + /// + /// Whether this sample uses all parameters (including optional). + /// + public bool IsAllParametersUsed { get; } + + // ------------------------------------------------------------------- + // Method info + // ------------------------------------------------------------------- + + /// + /// The input operation name. + /// + public string InputOperationName => _serviceMethod.Operation.Name; + + /// + /// The resource name from the operation, or the client name as fallback. + /// Used for method naming. + /// + public string? ResourceName => _serviceMethod.Operation.ResourceName ?? _client.InputClient.Name; + + /// + /// Whether the operation uses paging. + /// + public bool IsPageable => _serviceMethod is InputPagingServiceMethod; + + /// + /// Whether the operation is long-running. + /// + public bool IsLongRunning => _serviceMethod is InputLongRunningServiceMethod; + + /// + /// The method signature for the operation being sampled (protocol or convenience). + /// + public MethodSignature OperationMethodSignature => _operationMethodSignature; + + /// + /// Whether there is a response body on the operation. + /// + public bool HasResponseBody => _serviceMethod.Response?.Type != null; + + /// + /// Whether the response is a stream. + /// + public bool IsResponseStream => + _serviceMethod.Response?.Type is InputPrimitiveType { Kind: InputPrimitiveTypeKind.Stream }; + + /// + /// The effective result type of the operation. + /// For paging, this is the item type; for all others this is the response type. + /// + public InputType? ResultType => _resultType ??= GetEffectiveResponseType(); + + private MethodSignature ResolveOperationSignature() + { + var kind = IsConvenienceSample ? ScmMethodKind.Convenience : ScmMethodKind.Protocol; + var method = _methodCollection.MethodProviders + .FirstOrDefault(m => m.Kind == kind && !m.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Async)); + return method?.Signature ?? _methodCollection.MethodProviders.First().Signature; + } + + // ------------------------------------------------------------------- + // Client invocation chain + // ------------------------------------------------------------------- + + /// + /// Ordered list of method signatures to invoke to construct the client: + /// [root ctor, .GetSubClient(), .GetLeafClient()]. + /// + public IReadOnlyList ClientInvocationChain => + _clientInvocationChain ??= BuildClientInvocationChain(); + + /// + /// Walks from the current client up to the root, collecting factory methods, + /// then pushes the root constructor. Returns the chain in top-down order. + /// + private IReadOnlyList BuildClientInvocationChain() + { + var callChain = new Stack(); + + // Walk from the current client up to root, collecting factory methods. + // For each client that has a parent, find the factory method on the parent + // that returns it, and push that onto the chain. + var currentInputClient = _client.InputClient; + while (currentInputClient.Parent != null) + { + var parentProvider = ResolveClientProvider(currentInputClient.Parent); + if (parentProvider != null) + { + var factoryMethod = FindSubClientFactoryMethod(parentProvider, currentInputClient.Name); + if (factoryMethod != null) + { + callChain.Push(factoryMethod); + } + } + currentInputClient = currentInputClient.Parent; + } + + // At the root, push the primary public constructor + var rootProvider = ResolveClientProvider(currentInputClient); + if (rootProvider != null) + { + var ctor = GetPrimaryPublicConstructor(rootProvider); + if (ctor != null) + { + callChain.Push(ctor); + } + } + + return callChain.ToArray(); + } + + private static ClientProvider? ResolveClientProvider(InputClient inputClient) + { + return ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(inputClient); + } + + private static MethodSignature? FindSubClientFactoryMethod(ClientProvider parentProvider, string subClientName) + { + // Match the naming convention used by ClientProvider: + // If name ends with "Client" → "Get{name}", otherwise → "Get{name}Client" + var expectedMethodName = subClientName.EndsWith("Client", StringComparison.OrdinalIgnoreCase) + ? $"Get{subClientName}" + : $"Get{subClientName}Client"; + + foreach (var method in parentProvider.Methods) + { + if (method.Signature.Name.Equals(expectedMethodName, StringComparison.OrdinalIgnoreCase) && + !method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Async)) + { + return method.Signature; + } + } + return null; + } + + private static ConstructorSignature? GetPrimaryPublicConstructor(ClientProvider clientProvider) + { + ConstructorSignature? best = null; + foreach (var ctor in clientProvider.Constructors) + { + if (ctor.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)) + { + if (best == null || ctor.Signature.Parameters.Count > best.Parameters.Count) + { + best = ctor.Signature; + } + } + } + return best; + } + + // ------------------------------------------------------------------- + // Parameter value mapping + // ------------------------------------------------------------------- + + /// + /// Maps parameter names to their example values (either pre-built expressions or raw data). + /// + public IReadOnlyDictionary ParameterValueMapping => + _parameterValueMapping ??= EnsureParameterValueMapping(); + + private Dictionary EnsureParameterValueMapping() + { + var result = new Dictionary(); + var parameters = GetAllParameters(); + var parameterExamples = GetAllParameterExamples(); + + foreach (var parameter in parameters) + { + if (result.ContainsKey(parameter.Name)) + continue; + + if (TryProcessKnownParameter(result, parameter)) + continue; + + // Find the corresponding example value + var exampleValue = FindExampleValueByName(parameterExamples, parameter.Name); + + if (exampleValue == null && parameter.WireInfo?.SerializedName != null) + { + exampleValue = FindExampleValueByName(parameterExamples, parameter.WireInfo.SerializedName); + } + + if (exampleValue != null) + { + result.Add(parameter.Name, new ExampleParameterValue(parameter.Name, parameter.Type, exampleValue)); + } + else if (parameter.DefaultValue == null) + { + // Required parameter with no example — use default + var expression = DefaultOf(parameter.Type); + result.Add(parameter.Name, new ExampleParameterValue(parameter.Name, parameter.Type, expression)); + } + // Optional parameters with no value are intentionally omitted + } + + return result; + } + + /// + /// Known parameters that receive special handling (endpoint, credentials, cancellation, etc). + /// + private bool TryProcessKnownParameter(Dictionary result, ParameterProvider parameter) + { + var type = parameter.Type; + + // WaitUntil — prefer Completed to make long-running samples deterministic. + if (type.Name == "WaitUntil") + { + result[parameter.Name] = new ExampleParameterValue( + parameter.Name, + type, + Static(type).Property("Completed")); + return true; + } + + // CancellationToken — skip entirely (we don't set it in samples) + if (parameter.Equals(ScmKnownParameters.CancellationToken)) + { + return true; + } + + // RequestOptions (required) — pass null explicitly to avoid ambiguity, similar + // to old RequestContextRequired handling in AutoRest. + if ((parameter.Equals(ScmKnownParameters.RequestOptions) || parameter.Equals(ScmKnownParameters.OptionalRequestOptions)) && + parameter.DefaultValue == null) + { + result[parameter.Name] = new ExampleParameterValue( + parameter.Name, + type, + Null.CastTo(type)); + return true; + } + + // Endpoint (Uri) — use provided example value, otherwise a named placeholder like "" + if (type.Equals(typeof(Uri)) && parameter.InputParameter is InputEndpointParameter) + { + var endpointExpr = GetEndpointValue(parameter.Name); + result[parameter.Name] = new ExampleParameterValue(parameter.Name, type, endpointExpr); + return true; + } + + // Request content (BinaryContent) — handled specially via body parameter lookup + if (parameter.IsContentParameter) + { + result[parameter.Name] = new ExampleParameterValue(parameter.Name, type, GetBodyParameterValue()); + return true; + } + + // Request conditions / match conditions — default to null in samples. + if (type.Name == "RequestConditions" || + type.Name == "MatchConditions" || + type.Equals(ScmCodeModelGenerator.Instance.TypeFactory.MatchConditionsType)) + { + result[parameter.Name] = new ExampleParameterValue( + parameter.Name, + type, + Null.CastTo(type)); + return true; + } + + // ApiKeyCredential — produce `new ApiKeyCredential("")` + if (type.Equals(ScmKnownParameters.KeyCredential.Type) || + type.Name == ScmKnownParameters.KeyCredential.Type.Name || + type.Name == "AzureKeyCredential") + { + result[parameter.Name] = new ExampleParameterValue( + parameter.Name, type, New.Instance(type, Literal(""))); + return true; + } + + // TokenCredential — produce `new DefaultAzureCredential()` + // TokenCredential is abstract, so we must use a concrete type. + if (type.Equals(ScmKnownParameters.TokenCredential.Type) || + type.Name == ScmKnownParameters.TokenCredential.Type.Name || + type.Name == "TokenCredential") + { + // Use FormattableStringExpression because DefaultAzureCredential + // lives in Azure.Identity which the generator doesn't reference. + result[parameter.Name] = new ExampleParameterValue( + parameter.Name, type, new FormattableStringExpression("new DefaultAzureCredential()", [])); + return true; + } + + // ClientOptions — skip (optional, not needed in sample) + if (parameter.Name.EndsWith("Options", StringComparison.OrdinalIgnoreCase) && parameter.DefaultValue != null) + { + return true; + } + + return false; + } + + /// + /// Returns all the parameters that should be used in this sample. + /// Only required parameters are included if is false. + /// + private IEnumerable GetAllParameters() + { + // Parameters from the client invocation chain (ctor + factory methods) + foreach (var method in ClientInvocationChain) + { + foreach (var parameter in method.Parameters) + yield return parameter; + } + + // Parameters from the operation method itself + var operationParams = IsAllParametersUsed + ? _operationMethodSignature.Parameters + : _operationMethodSignature.Parameters.Where(p => p.DefaultValue == null); + + foreach (var parameter in operationParams) + yield return parameter; + } + + /// + /// Returns all parameter examples, handling spread parameters by extracting + /// individual properties from the model type. + /// + private IEnumerable GetAllParameterExamples() + { + foreach (var parameterExample in _example.Parameters) + { + var inputParameter = parameterExample.Parameter; + + // For spread parameters, the example value contains properties of the model + if (inputParameter is InputMethodParameter { Scope: InputParameterScope.Spread } && + inputParameter.Type is InputModelType modelType && + parameterExample.ExampleValue is InputExampleObjectValue objectValue) + { + var properties = objectValue.Values; + foreach (var modelOrBase in GetSelfAndBaseModels(modelType)) + { + foreach (var property in modelOrBase.Properties) + { + if (properties.TryGetValue(property.SerializedName, out var propValue)) + { + // Create a synthetic parameter example for each spread property + var syntheticParam = new InputMethodParameter( + property.Name, + null, // summary + property.Doc, + property.Type, + property.IsRequired, + property.IsReadOnly, + null, // access + property.SerializedName, + false, // isApiVersion + null, // defaultValue + InputParameterScope.Method, + InputRequestLocation.Body); + yield return new InputParameterExample(syntheticParam, propValue); + } + } + } + } + else + { + yield return parameterExample; + } + } + } + + /// + /// Searches for an example value by parameter name (case-sensitive). + /// + private static InputExampleValue? FindExampleValueByName( + IEnumerable parameterExamples, + string name) + { + foreach (var parameterExample in parameterExamples) + { + if (parameterExample.Parameter.Name == name) + { + return parameterExample.ExampleValue; + } + } + return null; + } + + /// + /// Gets the endpoint value, preferring the example value if available. + /// + private InputExampleValue GetEndpointValue(string parameterName) + { + // Try to find an endpoint value from the examples + var endpointExampleValue = _example.Parameters + .FirstOrDefault(e => e.Parameter is InputEndpointParameter)?.ExampleValue; + + if (endpointExampleValue != null) + return endpointExampleValue; + + // Fallback: placeholder + return InputExampleValue.Value(InputPrimitiveType.String, $"<{parameterName}>"); + } + + /// + /// Gets the body parameter value from the examples. + /// If there's a single body parameter example, use it. Otherwise search by type. + /// + private InputExampleValue GetBodyParameterValue() + { + var bodyParameters = _example.Parameters + .Where(e => e.Parameter is InputBodyParameter) + .ToArray(); + + if (bodyParameters.Length == 1) + { + return bodyParameters[0].ExampleValue; + } + + // Check for any body-located method parameter + var bodyMethodParam = _example.Parameters + .FirstOrDefault(e => e.Parameter is InputMethodParameter { Location: InputRequestLocation.Body }); + + if (bodyMethodParam != null) + { + return bodyMethodParam.ExampleValue; + } + + return InputExampleValue.Null(InputPrimitiveType.Any); + } + + /// + /// Walks the model and all its base models. + /// + private static IEnumerable GetSelfAndBaseModels(InputModelType model) + { + var current = model; + while (current != null) + { + yield return current; + current = current.BaseModel; + } + } + + // ------------------------------------------------------------------- + // Expression generation + // ------------------------------------------------------------------- + + /// + /// Converts the parameter value mapping entries to instances + /// for a specific set of parameters. Complex types are declared as out-of-line variables, + /// while simple types are inlined directly. + /// + public IEnumerable GetValueExpressionsForParameters( + IEnumerable parameters, + List variableDeclarationStatements) + { + foreach (var parameter in parameters) + { + ValueExpression parameterExpression; + + if (ParameterValueMapping.TryGetValue(parameter.Name, out var exampleValue)) + { + var format = parameter.WireInfo?.SerializationFormat ?? SerializationFormat.Default; + parameterExpression = ExampleValueExpressionBuilder.GetExpression(exampleValue, format); + } + else + { + // No example value — skip optional, use default for required + if (parameter.DefaultValue != null) + continue; + + parameterExpression = DefaultOf(parameter.Type); + } + + if (IsInlineParameter(parameter)) + { + yield return parameterExpression; + } + else + { + // Declare variable out-of-line + var varRef = new VariableExpression(parameter.Type, parameter.Name); + var declaration = NeedsDispose(parameter) + ? UsingDeclare(varRef, parameterExpression) + : Declare(varRef, parameterExpression); + variableDeclarationStatements.Add(declaration); + yield return varRef; + } + } + } + + /// + /// Determines whether a parameter value should be inlined directly in the method call + /// or declared as a separate variable. + /// + private static bool IsInlineParameter(ParameterProvider parameter) + { + var type = parameter.Type; + + // Content parameters (BinaryContent/RequestContent) → out-of-line + if (parameter.IsContentParameter) + return false; + + // Endpoint → out-of-line + if (type.Equals(typeof(Uri))) + return false; + + // Credentials → out-of-line + if (type.Equals(ScmKnownParameters.KeyCredential.Type) || + type.Name == ScmKnownParameters.KeyCredential.Type.Name || + type.Name == "AzureKeyCredential" || + type.Equals(ScmKnownParameters.TokenCredential.Type) || + type.Name == ScmKnownParameters.TokenCredential.Type.Name || + type.Name == "TokenCredential") + return false; + + // Model types (non-framework, non-enum, non-collection) → out-of-line + if (!type.IsFrameworkType && !type.IsEnum && !type.IsList && !type.IsDictionary) + return false; + + // Everything else (primitives, enums, collections) → inline + return true; + } + + /// + /// Determines whether a parameter needs a using declaration for disposal. + /// + private static bool NeedsDispose(ParameterProvider parameter) + { + return parameter.IsContentParameter; + } + + // ------------------------------------------------------------------- + // Response type resolution + // ------------------------------------------------------------------- + + /// + /// Returns the effective response type. + /// For paging operations, unwraps the item type from the response model. + /// For non-paging operations, returns the response type directly. + /// + private InputType? GetEffectiveResponseType() + { + var responseType = _serviceMethod.Response?.Type; + + if (_serviceMethod is not InputPagingServiceMethod pagingMethod) + return responseType; + + // For paging, try to unwrap the item type from the response model + var itemSegments = pagingMethod.PagingMetadata.ItemPropertySegments; + if (itemSegments.Count == 0 || responseType is not InputModelType responseModel) + return responseType; + + // Walk the item property path to find the items array + InputType currentType = responseModel; + foreach (var segment in itemSegments) + { + if (currentType is not InputModelType currentModel) + break; + + var property = currentModel.Properties + .FirstOrDefault(p => p.SerializedName == segment || p.Name == segment); + if (property == null) + break; + + currentType = property.Type; + } + + // If we found an array type at the end of the path, return the element type + if (currentType is InputArrayType arrayType) + return arrayType.ValueType; + + return responseType; + } + + // ------------------------------------------------------------------- + // Static helpers for sample generation decisions + // ------------------------------------------------------------------- + + /// + /// Determines whether samples should be generated for the given method. + /// + public static bool ShouldGenerateSample(ClientProvider client, MethodSignature protocolSignature) + { + if (!protocolSignature.Modifiers.HasFlag(MethodSignatureModifiers.Public)) + return false; + + // Check for obsolete + if (protocolSignature.Attributes.Any(a => a.Type.Equals(typeof(ObsoleteAttribute)))) + return false; + + // Subclients are always valid; root clients need a public constructor + bool isSubClient = client.InputClient.Parent != null; + if (isSubClient) + return true; + + return client.Constructors.Any(c => + c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)); + } + + /// + /// Determines whether the ShortVersion sample should be generated. + /// If protocol and convenience signatures are effectively identical, skip ShortVersion for protocol + /// to avoid duplicate samples. + /// + public static bool ShouldGenerateShortVersion(ScmMethodProviderCollection methodCollection) + { + var protocolMethod = methodCollection.MethodProviders + .FirstOrDefault(m => m.Kind == ScmMethodKind.Protocol && + !m.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Async)); + var convenienceMethod = methodCollection.MethodProviders + .FirstOrDefault(m => m.Kind == ScmMethodKind.Convenience && + !m.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Async)); + + if (protocolMethod == null || convenienceMethod == null) + return true; + + var protocolParams = protocolMethod.Signature.Parameters; + var convenienceParams = convenienceMethod.Signature.Parameters; + + // If the convenience method has one fewer parameter (e.g., no CancellationToken) + // and all overlapping parameter types match, skip — they're effectively identical. + if (convenienceParams.Count == protocolParams.Count - 1 && convenienceParams.Count > 0 && + !convenienceParams.Last().Type.Equals(typeof(CancellationToken))) + { + bool allEqual = true; + for (int i = 0; i < convenienceParams.Count; i++) + { + if (!convenienceParams[i].Type.Equals(protocolParams[i].Type)) + { + allEqual = false; + break; + } + } + + if (allEqual) + return false; + } + + return true; + } + + // ------------------------------------------------------------------- + // Sample information (human-readable descriptions) + // ------------------------------------------------------------------- + + /// + /// Gets a human-readable description of what the sample demonstrates. + /// + public string GetSampleInformation(bool isAsync) + { + var methodName = isAsync + ? _operationMethodSignature.Name + "Async" + : _operationMethodSignature.Name; + + return IsConvenienceSample + ? GetSampleInformationForConvenience(methodName) + : GetSampleInformationForProtocol(methodName); + } + + private string GetSampleInformationForConvenience(string methodName) + { + if (IsAllParametersUsed) + return $"This sample shows how to call {methodName} with all parameters."; + return $"This sample shows how to call {methodName}."; + } + + private string GetSampleInformationForProtocol(string methodName) + { + if (IsAllParametersUsed) + { + var desc = GenerateParameterAndRequestContentDescription(_operationMethodSignature.Parameters); + var parseResult = HasResponseBody ? " and parse the result" : ""; + return $"This sample shows how to call {methodName} with all {desc}{parseResult}."; + } + return $"This sample shows how to call {methodName}{(HasResponseBody ? " and parse the result" : "")}."; + } + + private static string GenerateParameterAndRequestContentDescription(IReadOnlyList parameters) + { + var hasNonBodyParameter = parameters.Any(p => + p.Location != ParameterLocation.Body && p.Name != "options"); + var hasBodyParameter = parameters.Any(p => p.Location == ParameterLocation.Body); + + if (hasNonBodyParameter) + return hasBodyParameter ? "parameters and request content" : "parameters"; + return "request content"; + } + + private string GetDebuggerDisplay() + => $"Sample (Client: {_client.Name}, Method: {_operationMethodSignature.Name})"; + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs index c85805768e9..add87324523 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; using System.Xml.Linq; using Microsoft.TypeSpec.Generator.ClientModel.Primitives; +using Microsoft.TypeSpec.Generator.ClientModel.Providers.Samples; using Microsoft.TypeSpec.Generator.ClientModel.Snippets; using Microsoft.TypeSpec.Generator.ClientModel.Utilities; using Microsoft.TypeSpec.Generator.EmitterRpc; @@ -40,12 +41,14 @@ public class ScmMethodProviderCollection : IReadOnlyList private IReadOnlyList? _convenienceMethodParameters; private readonly InputPagingServiceMethod? _pagingServiceMethod; private IReadOnlyList? _methods; + private IReadOnlyList? _samples; private readonly bool _generateConvenienceMethod; private ClientProvider Client { get; } protected InputServiceMethod ServiceMethod { get; } protected TypeProvider EnclosingType { get; } public IReadOnlyList MethodProviders => _methods ??= BuildMethods(); + public IReadOnlyList Samples => _samples ??= BuildSamples(); public ScmMethodProvider this[int index] { @@ -108,6 +111,47 @@ protected virtual IReadOnlyList BuildMethods() ]; } + protected virtual IReadOnlyList BuildSamples() + { + if (ServiceMethod.Operation.Examples.Count == 0) + { + return []; + } + + var protocolMethod = MethodProviders.FirstOrDefault(m => + m.Kind == ScmMethodKind.Protocol && + !m.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Async)); + + if (protocolMethod == null || !OperationSample.ShouldGenerateSample(Client, protocolMethod.Signature)) + { + return []; + } + + bool shouldGenerateShortVersion = OperationSample.ShouldGenerateShortVersion(this); + bool shouldGenerateConvenienceSamples = MethodProviders.Any(m => + m.Kind == ScmMethodKind.Convenience && + !m.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Async) && + m.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)); + + List samples = new(); + foreach (var example in ServiceMethod.Operation.Examples) + { + if (!shouldGenerateShortVersion && example.Name == "ShortVersion") + { + continue; + } + + samples.Add(new OperationSample(Client, this, ServiceMethod, example, false, example.Name)); + + if (shouldGenerateConvenienceSamples) + { + samples.Add(new OperationSample(Client, this, ServiceMethod, example, true, example.Name)); + } + } + + return samples; + } + private ScmMethodProvider BuildConvenienceMethod(MethodProvider protocolMethod, bool isAsync) { if (EnclosingType is not ClientProvider client) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Samples/ExampleValueExpressionBuilderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Samples/ExampleValueExpressionBuilderTests.cs new file mode 100644 index 00000000000..8daea57ac77 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Samples/ExampleValueExpressionBuilderTests.cs @@ -0,0 +1,624 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Xml; +using Microsoft.TypeSpec.Generator.ClientModel.Providers.Samples; +using Microsoft.TypeSpec.Generator.Expressions; +using Microsoft.TypeSpec.Generator.Input; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Tests.Common; +using NUnit.Framework; + +namespace Microsoft.TypeSpec.Generator.ClientModel.Tests.Providers.Samples +{ + public class ExampleValueExpressionBuilderTests + { + // ----------------------------------------------------------------------- + // ExampleParameterValue — dual mode + // ----------------------------------------------------------------------- + + [Test] + public void ParameterValue_WithExpression_ReturnsExpression() + { + var expr = new LiteralExpression(42); + var paramValue = new ExampleParameterValue("count", new CSharpType(typeof(int)), expr); + + var result = ExampleValueExpressionBuilder.GetExpression(paramValue); + + Assert.AreSame(expr, result); + } + + [Test] + public void ParameterValue_WithValue_ConvertsToExpression() + { + var inputValue = InputExampleValue.Value(InputPrimitiveType.Int32, 1234); + var paramValue = new ExampleParameterValue("count", new CSharpType(typeof(int)), inputValue); + + var result = ExampleValueExpressionBuilder.GetExpression(paramValue); + + Assert.IsNotNull(result); + } + + [Test] + public void ParameterValue_WithNeither_ReturnsDefault() + { + // Edge case: both Value and Expression are null (shouldn't happen in practice) + var paramValue = new ExampleParameterValue("x", new CSharpType(typeof(int)), + (InputExampleValue)null!); + + // This will go through GetExpression with null value — should return Default + var result = ExampleValueExpressionBuilder.GetExpression(paramValue); + Assert.IsNotNull(result); + } + + // ----------------------------------------------------------------------- + // Framework type conversions — primitives + // ----------------------------------------------------------------------- + + [Test] + public void String_FromRawValue() + { + var result = BuildExpression(typeof(string), InputExampleValue.Value(InputPrimitiveType.String, "hello")); + Assert.IsNotNull(result); + // Should not be null/default + Assert.IsNotInstanceOf(result); + } + + [Test] + public void String_FromNull() + { + var result = BuildExpression(typeof(string), InputExampleValue.Null(InputPrimitiveType.String)); + // Null string → Null keyword + Assert.IsNotNull(result); + } + + [Test] + public void Bool_True() + { + var result = BuildExpression(typeof(bool), InputExampleValue.Value(InputPrimitiveType.Boolean, true)); + Assert.IsNotNull(result); + } + + [Test] + public void Int32_FromRawValue() + { + var result = BuildExpression(typeof(int), InputExampleValue.Value(InputPrimitiveType.Int32, 1234)); + Assert.IsNotNull(result); + } + + [Test] + public void Int64_FromRawValue() + { + var result = BuildExpression(typeof(long), InputExampleValue.Value(InputPrimitiveType.Int64, 1234L)); + Assert.IsNotNull(result); + } + + [Test] + public void Float_FromRawValue() + { + var result = BuildExpression(typeof(float), InputExampleValue.Value(InputPrimitiveType.Float32, 123.45f)); + Assert.IsNotNull(result); + } + + [Test] + public void Double_FromRawValue() + { + var result = BuildExpression(typeof(double), InputExampleValue.Value(InputPrimitiveType.Float64, 123.45)); + Assert.IsNotNull(result); + } + + [Test] + public void Decimal_FromRawValue() + { + var result = BuildExpression(typeof(decimal), InputExampleValue.Value( + new InputPrimitiveType(InputPrimitiveTypeKind.Decimal, "decimal", "TypeSpec.decimal"), 123.45m)); + Assert.IsNotNull(result); + } + + // ----------------------------------------------------------------------- + // Framework type conversions — complex types + // ----------------------------------------------------------------------- + + [Test] + public void Guid_FromString() + { + var result = BuildExpression(typeof(Guid), + InputExampleValue.Value(InputPrimitiveType.String, "73f411fe-4f43-4b4b-9cbd-6828d8f4cf9a")); + + // Should produce Guid.Parse("...") + Assert.IsInstanceOf(result); + } + + [Test] + public void Uri_FromString() + { + var result = BuildExpression(typeof(Uri), + InputExampleValue.Value(InputPrimitiveType.Url, "http://localhost:3000")); + + // Should produce new Uri("...") + Assert.IsNotNull(result); + } + + [Test] + public void DateTimeOffset_FromString() + { + var result = BuildExpression(typeof(DateTimeOffset), + InputExampleValue.Value(InputPrimitiveType.String, "2022-05-10T18:57:31.2311892Z")); + + // Should produce DateTimeOffset.Parse("...") + Assert.IsInstanceOf(result); + } + + [Test] + public void DateTimeOffset_FromUnixTimestamp() + { + var result = BuildExpression(typeof(DateTimeOffset), + InputExampleValue.Value(InputPrimitiveType.Int64, 1652209051L)); + + // Should produce DateTimeOffset.FromUnixTimeSeconds(...) + Assert.IsInstanceOf(result); + } + + [Test] + public void TimeSpan_FromIso8601String() + { + var result = BuildExpression(typeof(TimeSpan), + InputExampleValue.Value(InputPrimitiveType.String, "PT1H23M45S")); + + // Should produce XmlConvert.ToTimeSpan("...") + Assert.IsInstanceOf(result); + } + + [Test] + public void TimeSpan_FromSeconds() + { + var result = BuildExpression(typeof(TimeSpan), + InputExampleValue.Value(InputPrimitiveType.Float64, 10.5)); + + // Should produce TimeSpan.FromSeconds(...) + Assert.IsInstanceOf(result); + } + + [Test] + public void BinaryData_FromObject() + { + var objValue = InputExampleValue.Object( + InputFactory.Model("TestModel"), + new Dictionary + { + ["name"] = InputExampleValue.Value(InputPrimitiveType.String, "test") + }); + + var result = BuildExpression(typeof(BinaryData), objValue); + + // Should produce BinaryData.FromObjectAsJson(new { name = "test" }) + Assert.IsInstanceOf(result); + } + + [Test] + public void ByteArray_FromString() + { + var result = BuildExpression(typeof(byte[]), + InputExampleValue.Value(InputPrimitiveType.String, "dGVzdA==")); + + // Should produce Encoding.UTF8.GetBytes("...") + Assert.IsInstanceOf(result); + } + + [Test] + public void Stream_FromStreamValue() + { + var result = BuildExpression(typeof(Stream), + InputExampleValue.Stream(InputPrimitiveType.String, "")); + + // Should produce File.OpenRead("...") + Assert.IsInstanceOf(result); + } + + // ----------------------------------------------------------------------- + // Collections + // ----------------------------------------------------------------------- + + [Test] + public void List_FromListValue() + { + var listType = new CSharpType(typeof(IList<>), new CSharpType(typeof(int))); + var listValue = InputExampleValue.List( + new InputArrayType("list", "TypeSpec.Array", InputPrimitiveType.Int32), + new[] { InputExampleValue.Value(InputPrimitiveType.Int32, 1234) }); + + var result = ExampleValueExpressionBuilder.GetExpression(listType, listValue); + + // New.Array returns IndexableExpression wrapping NewArrayExpression + Assert.IsNotNull(result); + } + + [Test] + public void Dictionary_FromObjectValue() + { + var dictType = new CSharpType(typeof(IDictionary<,>), new CSharpType(typeof(string)), new CSharpType(typeof(int))); + var dictValue = InputExampleValue.Object( + new InputDictionaryType("dict", InputPrimitiveType.String, InputPrimitiveType.Int32), + new Dictionary + { + ["key"] = InputExampleValue.Value(InputPrimitiveType.Int32, 42) + }); + + var result = ExampleValueExpressionBuilder.GetExpression(dictType, dictValue); + + // New.Dictionary returns DictionaryExpression wrapping NewInstanceExpression + Assert.IsNotNull(result); + } + + // ----------------------------------------------------------------------- + // Enum + // ----------------------------------------------------------------------- + + [Test] + public void Enum_ProducesMemberAccess() + { + // Use a real framework enum type for testing + var enumType = new CSharpType(typeof(DayOfWeek)); + var value = InputExampleValue.Value( + InputFactory.StringEnum("DayOfWeek", [("Monday", "Monday")]), "Monday"); + + var result = ExampleValueExpressionBuilder.GetExpression(enumType, value); + + // Should produce a member expression like DayOfWeek.Monday + Assert.IsInstanceOf(result); + } + + // ----------------------------------------------------------------------- + // Model (basic) + // ----------------------------------------------------------------------- + + [Test] + public void Model_ProducesNewInstance() + { + var modelType = new CSharpType(typeof(object)); + var value = InputExampleValue.Object( + InputFactory.Model("Widget"), + new Dictionary()); + + var result = ExampleValueExpressionBuilder.GetExpression(modelType, value); + + // Non-collection, non-enum framework type falls through to framework handler + Assert.IsNotNull(result); + } + + // ----------------------------------------------------------------------- + // Anonymous object (for BinaryContent) + // ----------------------------------------------------------------------- + + [Test] + public void AnonymousObject_FromNestedValues() + { + var objValue = InputExampleValue.Object( + InputFactory.Model("Request"), + new Dictionary + { + ["name"] = InputExampleValue.Value(InputPrimitiveType.String, "test"), + ["count"] = InputExampleValue.Value(InputPrimitiveType.Int32, 5) + }); + + var result = ExampleValueExpressionBuilder.GetExpressionForAnonymousObject(objValue); + + // Should produce new { name = "test", count = 5 } + Assert.IsNotNull(result); + } + + [Test] + public void AnonymousObject_SkipsNullValues() + { + var objValue = InputExampleValue.Object( + InputFactory.Model("Request"), + new Dictionary + { + ["name"] = InputExampleValue.Value(InputPrimitiveType.String, "test"), + ["nullable"] = InputExampleValue.Null(InputPrimitiveType.String) + }); + + var result = ExampleValueExpressionBuilder.GetExpressionForAnonymousObject(objValue); + + // Should produce new { name = "test" } — nullable skipped + Assert.IsNotNull(result); + } + + [Test] + public void AnonymousObject_EmptyObject() + { + var objValue = InputExampleValue.Object( + InputFactory.Model("Empty"), + new Dictionary()); + + var result = ExampleValueExpressionBuilder.GetExpressionForAnonymousObject(objValue); + + // Empty object → new object() (wrapped in ScopedApi) + Assert.IsNotNull(result); + } + + // ----------------------------------------------------------------------- + // Null / default fallbacks + // ----------------------------------------------------------------------- + + [Test] + public void Null_ValueType_ReturnsDefault() + { + var result = BuildExpression(typeof(int), InputExampleValue.Null(InputPrimitiveType.Int32)); + Assert.IsInstanceOf(result); + } + + [Test] + public void Null_ReferenceType_ReturnsNull() + { + var result = BuildExpression(typeof(string), InputExampleValue.Null(InputPrimitiveType.String)); + Assert.IsInstanceOf(result); + } + + // ----------------------------------------------------------------------- + // Missing primitive types — cast expressions + // ----------------------------------------------------------------------- + + [Test] + public void Short_FromRawValue() + { + var result = BuildExpression(typeof(short), InputExampleValue.Value( + new InputPrimitiveType(InputPrimitiveTypeKind.Int16, "int16", "TypeSpec.int16"), (short)1234)); + Assert.IsNotNull(result); + Assert.IsInstanceOf(result); + } + + [Test] + public void SByte_FromRawValue() + { + var result = BuildExpression(typeof(sbyte), InputExampleValue.Value( + new InputPrimitiveType(InputPrimitiveTypeKind.Int8, "int8", "TypeSpec.int8"), (sbyte)123)); + Assert.IsNotNull(result); + Assert.IsInstanceOf(result); + } + + [Test] + public void Byte_FromRawValue() + { + var result = BuildExpression(typeof(byte), InputExampleValue.Value( + new InputPrimitiveType(InputPrimitiveTypeKind.UInt8, "uint8", "TypeSpec.uint8"), (byte)123)); + Assert.IsNotNull(result); + Assert.IsInstanceOf(result); + } + + [Test] + public void UShort_FromRawValue() + { + var result = BuildExpression(typeof(ushort), InputExampleValue.Value( + new InputPrimitiveType(InputPrimitiveTypeKind.UInt16, "uint16", "TypeSpec.uint16"), (ushort)1234)); + Assert.IsNotNull(result); + Assert.IsInstanceOf(result); + } + + [Test] + public void UInt_FromRawValue() + { + var result = BuildExpression(typeof(uint), InputExampleValue.Value( + new InputPrimitiveType(InputPrimitiveTypeKind.UInt32, "uint32", "TypeSpec.uint32"), (uint)1234)); + Assert.IsNotNull(result); + Assert.IsInstanceOf(result); + } + + [Test] + public void ULong_FromRawValue() + { + var result = BuildExpression(typeof(ulong), InputExampleValue.Value( + new InputPrimitiveType(InputPrimitiveTypeKind.UInt64, "uint64", "TypeSpec.uint64"), (ulong)1234)); + Assert.IsNotNull(result); + Assert.IsInstanceOf(result); + } + + // ----------------------------------------------------------------------- + // Null paths for complex types + // ----------------------------------------------------------------------- + + [Test] + public void Bool_Null_ReturnsDefault() + { + var result = BuildExpression(typeof(bool), InputExampleValue.Null(InputPrimitiveType.Boolean)); + Assert.IsInstanceOf(result); + } + + [Test] + public void Guid_Null_ReturnsDefault() + { + var result = BuildExpression(typeof(Guid), InputExampleValue.Null(InputPrimitiveType.String)); + Assert.IsInstanceOf(result); + } + + [Test] + public void Uri_Null_ReturnsNull() + { + var result = BuildExpression(typeof(Uri), InputExampleValue.Null(InputPrimitiveType.String)); + Assert.IsNotNull(result); + } + + [Test] + public void DateTimeOffset_Null_ReturnsDefault() + { + var result = BuildExpression(typeof(DateTimeOffset), InputExampleValue.Null(InputPrimitiveType.String)); + Assert.IsInstanceOf(result); + } + + [Test] + public void TimeSpan_Null_ReturnsDefault() + { + var result = BuildExpression(typeof(TimeSpan), InputExampleValue.Null(InputPrimitiveType.String)); + Assert.IsInstanceOf(result); + } + + [Test] + public void ByteArray_Null_ReturnsNull() + { + var result = BuildExpression(typeof(byte[]), InputExampleValue.Null(InputPrimitiveType.String)); + Assert.IsNotNull(result); + } + + [Test] + public void Stream_NonStreamValue_ReturnsNull() + { + var result = BuildExpression(typeof(Stream), InputExampleValue.Value(InputPrimitiveType.String, "notastream")); + Assert.IsNotNull(result); + } + + // ----------------------------------------------------------------------- + // Enum edge case + // ----------------------------------------------------------------------- + + [Test] + public void Enum_Null_ReturnsDefault() + { + var enumType = new CSharpType(typeof(DayOfWeek)); + var value = InputExampleValue.Null(InputFactory.StringEnum("DayOfWeek", [("Monday", "Monday")])); + + var result = ExampleValueExpressionBuilder.GetExpression(enumType, value); + + Assert.IsInstanceOf(result); + } + + // ----------------------------------------------------------------------- + // Collection edge cases + // ----------------------------------------------------------------------- + + [Test] + public void List_FromNonListValue_ReturnsEmptyArray() + { + var listType = new CSharpType(typeof(IList<>), new CSharpType(typeof(int))); + var nonListValue = InputExampleValue.Value(InputPrimitiveType.Int32, 1234); + + var result = ExampleValueExpressionBuilder.GetExpression(listType, nonListValue); + + // Should produce an empty array + Assert.IsNotNull(result); + } + + [Test] + public void Dictionary_FromNonObjectValue_ReturnsEmptyDictionary() + { + var dictType = new CSharpType(typeof(IDictionary<,>), new CSharpType(typeof(string)), new CSharpType(typeof(int))); + var nonObjValue = InputExampleValue.Value(InputPrimitiveType.Int32, 1234); + + var result = ExampleValueExpressionBuilder.GetExpression(dictType, nonObjValue); + + // Should produce an empty dictionary + Assert.IsNotNull(result); + } + + // ----------------------------------------------------------------------- + // Model edge case + // ----------------------------------------------------------------------- + + [Test] + public void Model_ValueType_ReturnsDefault() + { + var valueType = new CSharpType(typeof(int)); // value type but not enum/collection/known + var value = InputExampleValue.Object( + InputFactory.Model("Widget"), + new Dictionary()); + + // int is a framework type, so it goes to framework handler, not model handler + var result = ExampleValueExpressionBuilder.GetExpression(valueType, value); + Assert.IsNotNull(result); + } + + // ----------------------------------------------------------------------- + // Anonymous object edge cases + // ----------------------------------------------------------------------- + + [Test] + public void AnonymousObject_WithNestedList() + { + var listValue = InputExampleValue.List( + new InputArrayType("list", "TypeSpec.Array", InputPrimitiveType.String), + new[] { InputExampleValue.Value(InputPrimitiveType.String, "item1") }); + + var objValue = InputExampleValue.Object( + InputFactory.Model("Request"), + new Dictionary + { + ["items"] = listValue + }); + + var result = ExampleValueExpressionBuilder.GetExpressionForAnonymousObject(objValue); + Assert.IsNotNull(result); + } + + [Test] + public void AnonymousObject_WithNestedObject() + { + var innerObj = InputExampleValue.Object( + InputFactory.Model("Inner"), + new Dictionary + { + ["innerProp"] = InputExampleValue.Value(InputPrimitiveType.Int32, 42) + }); + + var outerObj = InputExampleValue.Object( + InputFactory.Model("Outer"), + new Dictionary + { + ["nested"] = innerObj + }); + + var result = ExampleValueExpressionBuilder.GetExpressionForAnonymousObject(outerObj); + Assert.IsNotNull(result); + } + + [Test] + public void AnonymousObject_FromRawPrimitive() + { + var rawValue = InputExampleValue.Value(InputPrimitiveType.String, "hello"); + + var result = ExampleValueExpressionBuilder.GetExpressionForAnonymousObject(rawValue); + Assert.IsNotNull(result); + } + + [Test] + public void AnonymousObject_FromNullRaw() + { + var nullValue = InputExampleValue.Null(InputPrimitiveType.String); + + var result = ExampleValueExpressionBuilder.GetExpressionForAnonymousObject(nullValue); + // Should return Null keyword + Assert.IsNotNull(result); + } + + // ----------------------------------------------------------------------- + // Fallback for unknown framework types + // ----------------------------------------------------------------------- + + [Test] + public void UnknownValueType_ReturnsDefault() + { + // A value type that doesn't match any specific handler + var result = BuildExpression(typeof(DateTime), InputExampleValue.Null(InputPrimitiveType.String)); + Assert.IsInstanceOf(result); + } + + [Test] + public void UnknownReferenceType_ReturnsNull() + { + // A reference type that doesn't match any specific handler + var result = BuildExpression(typeof(System.Text.StringBuilder), InputExampleValue.Null(InputPrimitiveType.String)); + Assert.IsNotNull(result); + } + + // ----------------------------------------------------------------------- + // Helpers + // ----------------------------------------------------------------------- + + private static ValueExpression BuildExpression(Type frameworkType, InputExampleValue value) + { + return ExampleValueExpressionBuilder.GetExpression(new CSharpType(frameworkType), value); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Samples/OperationSampleTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Samples/OperationSampleTests.cs new file mode 100644 index 00000000000..dcec8a22bbf --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Samples/OperationSampleTests.cs @@ -0,0 +1,387 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using System; +using System.Reflection; +using Microsoft.TypeSpec.Generator.ClientModel.Providers; +using Microsoft.TypeSpec.Generator.ClientModel.Providers.Samples; +using Microsoft.TypeSpec.Generator.Expressions; +using Microsoft.TypeSpec.Generator.Input; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Snippets; +using Microsoft.TypeSpec.Generator.Tests.Common; +using NUnit.Framework; + +namespace Microsoft.TypeSpec.Generator.ClientModel.Tests.Providers.Samples +{ + public class OperationSampleTests + { + [SetUp] + public void SetUp() + { + MockHelpers.LoadMockGenerator(); + } + + // ------------------------------------------------------------------- + // TokenCredential → new DefaultAzureCredential() + // ------------------------------------------------------------------- + + [Test] + public void TokenCredential_ProducesDefaultAzureCredential() + { + var sample = CreateSampleWithClientParameterType("credential", new CSharpType(typeof(TokenCredential))); + var mapping = sample.ParameterValueMapping; + + Assert.IsTrue(mapping.ContainsKey("credential")); + + // The expression should be a FormattableStringExpression containing "new DefaultAzureCredential()" + // (not "new TokenCredential()" which would fail because TokenCredential is abstract) + var expr = ExampleValueExpressionBuilder.GetExpression(mapping["credential"]); + Assert.IsInstanceOf(expr); + + // Verify the format string is exactly "new DefaultAzureCredential()" via reflection + // (Format is private, but this is the key parity assertion) + var formatProp = typeof(FormattableStringExpression).GetProperty("Format", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.IsNotNull(formatProp, "Format property should exist on FormattableStringExpression"); + Assert.AreEqual("new DefaultAzureCredential()", formatProp!.GetValue(expr)); + } + + // ------------------------------------------------------------------- + // ApiKeyCredential → new AzureKeyCredential("") + // ------------------------------------------------------------------- + + [Test] + public void ApiKeyCredential_ProducesNewWithKeyPlaceholder() + { + var sample = CreateSampleWithClientParameterType("credential", new CSharpType(typeof(AzureKeyCredential))); + var mapping = sample.ParameterValueMapping; + + Assert.IsTrue(mapping.ContainsKey("credential")); + + var expr = Unwrap(UnwrapCast(ExampleValueExpressionBuilder.GetExpression(mapping["credential"]))); + Assert.IsInstanceOf(expr); + + var newExpr = (NewInstanceExpression)expr; + Assert.AreEqual(1, newExpr.Parameters.Count); + // The argument should be the literal "" + var argExpr = Unwrap(newExpr.Parameters[0]); + Assert.IsInstanceOf(argExpr); + Assert.AreEqual("", ((LiteralExpression)argExpr).Literal); + } + + // ------------------------------------------------------------------- + // Endpoint value resolution + // ------------------------------------------------------------------- + + [Test] + public void Endpoint_WithoutExample_UsesPlaceholder() + { + var sample = CreateBasicSample("ShortVersion"); + var endpointValue = InvokeGetEndpointValue(sample, "endpoint"); + Assert.AreEqual("", GetRawExampleValue(endpointValue)); + } + + [Test] + public void Endpoint_WithExample_UsesProvidedUrl() + { + const string url = "https://example.contoso.test"; + var sample = CreateSampleWithEndpointExample("ShortVersion", url); + var endpointValue = InvokeGetEndpointValue(sample, "endpoint"); + Assert.AreEqual(url, GetRawExampleValue(endpointValue)); + } + + // ------------------------------------------------------------------- + // Client invocation chain — root client + // ------------------------------------------------------------------- + + [Test] + public void RootClient_ChainHasConstructor() + { + var sample = CreateBasicSample("ShortVersion"); + var chain = sample.ClientInvocationChain; + + Assert.IsTrue(chain.Count >= 1); + Assert.IsInstanceOf(chain[0]); + } + + // ------------------------------------------------------------------- + // Client invocation chain — subclient + // ------------------------------------------------------------------- + + [Test] + public void SubClient_ChainHasConstructorThenFactory() + { + var (sample, _, _) = CreateSubClientSample("ShortVersion"); + var chain = sample.ClientInvocationChain; + + Assert.IsTrue(chain.Count >= 2); + Assert.IsInstanceOf(chain[0], "First element should be root ctor"); + Assert.IsInstanceOf(chain[1], "Second element should be factory method"); + } + + // ------------------------------------------------------------------- + // Parameter mapping + // ------------------------------------------------------------------- + + [Test] + public void ParameterMapping_ExampleValue_IsMapped() + { + var bodyParam = InputFactory.MethodParameter("message", InputPrimitiveType.String, isRequired: true); + var example = new InputOperationExample( + "ShortVersion", + null, + [new InputParameterExample(bodyParam, InputExampleValue.Value(InputPrimitiveType.String, "hello"))], + ""); + + var operation = InputFactory.Operation("SendMessage", + parameters: [InputFactory.BodyParameter("message", InputPrimitiveType.String, isRequired: true)]); + var serviceMethod = InputFactory.BasicServiceMethod("SendMessage", operation, parameters: [bodyParam]); + var inputClient = InputFactory.Client("MsgClient", methods: [serviceMethod]); + + MockHelpers.LoadMockGenerator( + createCSharpTypeCore: (inputType) => new CSharpType(typeof(string))); + + var client = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(inputClient)!; + var methodCollection = new ScmMethodProviderCollection(serviceMethod, client); + var sample = new OperationSample(client, methodCollection, serviceMethod, example, true, "ShortVersion"); + + var mapping = sample.ParameterValueMapping; + Assert.IsTrue(mapping.ContainsKey("message")); + Assert.IsNotNull(mapping["message"].Value); + } + + // ------------------------------------------------------------------- + // Optional params excluded from ShortVersion + // ------------------------------------------------------------------- + + [Test] + public void ParameterMapping_OptionalParam_ExcludedFromShortVersion() + { + var optionalParam = InputFactory.MethodParameter("tag", InputPrimitiveType.String, isRequired: false, + defaultValue: InputFactory.Constant.String("default")); + var example = new InputOperationExample("ShortVersion", null, [], ""); + + var operation = InputFactory.Operation("GetItem", + parameters: [InputFactory.QueryParameter("tag", InputPrimitiveType.String, isRequired: false)]); + var serviceMethod = InputFactory.BasicServiceMethod("GetItem", operation, parameters: [optionalParam]); + var inputClient = InputFactory.Client("ItemClient", methods: [serviceMethod]); + + MockHelpers.LoadMockGenerator( + createCSharpTypeCore: (inputType) => new CSharpType(typeof(string))); + + var client = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(inputClient)!; + var methodCollection = new ScmMethodProviderCollection(serviceMethod, client); + var sample = new OperationSample(client, methodCollection, serviceMethod, example, true, "ShortVersion"); + + Assert.IsFalse(sample.ParameterValueMapping.ContainsKey("tag")); + } + + // ------------------------------------------------------------------- + // WaitUntil → .Completed + // ------------------------------------------------------------------- + + [Test] + public void WaitUntil_ProducesCompletedMemberAccess() + { + var sample = CreateSampleWithClientParameterType("waitUntil", new CSharpType(typeof(WaitUntil))); + var mapping = sample.ParameterValueMapping; + + Assert.IsTrue(mapping.ContainsKey("waitUntil")); + var expr = UnwrapCast(ExampleValueExpressionBuilder.GetExpression(mapping["waitUntil"])); + // Should be a property access like WaitUntil.Completed + Assert.IsInstanceOf(expr); + } + + // ------------------------------------------------------------------- + // RequestOptions (required) → null + // ------------------------------------------------------------------- + + [Test] + public void RequiredRequestOptions_ProducesNull() + { + var sample = CreateSampleWithClientParameterType( + "options", + new CSharpType(typeof(System.ClientModel.Primitives.RequestOptions)), + isOptional: false, + defaultValue: null); + var mapping = sample.ParameterValueMapping; + + Assert.IsTrue(mapping.ContainsKey("options")); + var expr = UnwrapCast(ExampleValueExpressionBuilder.GetExpression(mapping["options"])); + Assert.IsInstanceOf(expr); + } + + // ------------------------------------------------------------------- + // MatchConditions → null + // ------------------------------------------------------------------- + + [Test] + public void MatchConditions_ProducesNull() + { + var sample = CreateSampleWithClientParameterType("conditions", new CSharpType(typeof(MatchConditions))); + var mapping = sample.ParameterValueMapping; + + Assert.IsTrue(mapping.ContainsKey("conditions")); + var expr = UnwrapCast(ExampleValueExpressionBuilder.GetExpression(mapping["conditions"])); + Assert.IsInstanceOf(expr); + } + + // ------------------------------------------------------------------- + // ShouldGenerateShortVersion + // ------------------------------------------------------------------- + + [Test] + public void ShouldGenerateShortVersion_TrueWhenNoConvenience() + { + var operation = InputFactory.Operation("TestOp", generateConvenienceMethod: false); + var serviceMethod = InputFactory.BasicServiceMethod("TestOp", operation); + var inputClient = InputFactory.Client("TestClient", methods: [serviceMethod]); + + var client = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(inputClient)!; + var methodCollection = new ScmMethodProviderCollection(serviceMethod, client); + + Assert.IsTrue(OperationSample.ShouldGenerateShortVersion(methodCollection)); + } + + // ------------------------------------------------------------------- + // Sample metadata + // ------------------------------------------------------------------- + + [Test] + public void SampleInformation_ContainsMethodName() + { + var sample = CreateBasicSample("ShortVersion", isConvenience: true); + var info = sample.GetSampleInformation(false); + Assert.IsTrue(info.Contains("how to call")); + } + + [Test] + public void SampleInformation_AsyncAppendsAsync() + { + var sample = CreateBasicSample("ShortVersion", isConvenience: true); + var info = sample.GetSampleInformation(true); + Assert.IsTrue(info.Contains("Async")); + } + + // ------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------- + + private static OperationSample CreateBasicSample(string exampleKey, bool isConvenience = false) + { + var operation = InputFactory.Operation("TestOperation"); + var serviceMethod = InputFactory.BasicServiceMethod("TestOperation", operation); + var inputClient = InputFactory.Client("TestClient", methods: [serviceMethod]); + + var client = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(inputClient)!; + var methodCollection = new ScmMethodProviderCollection(serviceMethod, client); + var example = new InputOperationExample(exampleKey, null, [], ""); + + return new OperationSample(client, methodCollection, serviceMethod, example, isConvenience, exampleKey); + } + + private static (OperationSample sample, ClientProvider rootClient, ClientProvider subClient) CreateSubClientSample(string exampleKey) + { + var endpointParam = InputFactory.EndpointParameter("endpoint", InputPrimitiveType.Url, isRequired: true); + var parentClient = InputFactory.Client("RootClient", parameters: [endpointParam]); + var subOperation = InputFactory.Operation("DoWork"); + var subServiceMethod = InputFactory.BasicServiceMethod("DoWork", subOperation); + var subClient = InputFactory.Client("SubClient", parent: parentClient, + methods: [subServiceMethod], + initializedBy: InputClientInitializedBy.Parent); + + MockHelpers.LoadMockGenerator(clients: () => [parentClient, subClient]); + + var rootProvider = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(parentClient)!; + var subProvider = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(subClient)!; + var methodCollection = new ScmMethodProviderCollection(subServiceMethod, subProvider); + var example = new InputOperationExample(exampleKey, null, [], ""); + + var sample = new OperationSample(subProvider, methodCollection, subServiceMethod, example, false, exampleKey); + return (sample, rootProvider, subProvider); + } + + private static OperationSample CreateSampleWithEndpointExample(string exampleKey, string endpointExample) + { + var endpointParam = InputFactory.EndpointParameter("endpoint", InputPrimitiveType.Url, isRequired: true); + var operation = InputFactory.Operation("TestOperation"); + var serviceMethod = InputFactory.BasicServiceMethod("TestOperation", operation); + var inputClient = InputFactory.Client("TestClient", methods: [serviceMethod]); + + MockHelpers.LoadMockGenerator(); + + var client = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(inputClient)!; + var methodCollection = new ScmMethodProviderCollection(serviceMethod, client); + var example = new InputOperationExample( + exampleKey, + null, + [new InputParameterExample(endpointParam, InputExampleValue.Value(InputPrimitiveType.Url, endpointExample))], + ""); + + return new OperationSample(client, methodCollection, serviceMethod, example, false, exampleKey); + } + + private static OperationSample CreateSampleWithClientParameterType( + string parameterName, + CSharpType mappedType, + bool isOptional = false, + InputConstant? defaultValue = null) + { + var clientParameter = InputFactory.QueryParameter( + parameterName, + InputPrimitiveType.String, + isRequired: !isOptional, + defaultValue: defaultValue); + var operation = InputFactory.Operation("DoWork"); + var serviceMethod = InputFactory.BasicServiceMethod("DoWork", operation); + var inputClient = InputFactory.Client("TypedClient", parameters: [clientParameter], methods: [serviceMethod]); + + MockHelpers.LoadMockGenerator( + clients: () => [inputClient], + createCSharpTypeCore: _ => mappedType); + + var client = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(inputClient)!; + var methodCollection = new ScmMethodProviderCollection(serviceMethod, client); + var example = new InputOperationExample("ShortVersion", null, [], ""); + + return new OperationSample(client, methodCollection, serviceMethod, example, false, "ShortVersion"); + } + + // Stub types for credential/LRO tests (matched by type name in TryProcessKnownParameter) + private sealed class TokenCredential { } + private sealed class AzureKeyCredential { } + private sealed class WaitUntil + { + public static WaitUntil Completed { get; } = new WaitUntil(); + } + private sealed class MatchConditions { } + + private static object? GetRawExampleValue(InputExampleValue value) + { + var rawValueProperty = value.GetType().GetProperty("RawValue"); + return rawValueProperty?.GetValue(value); + } + + private static ValueExpression UnwrapCast(ValueExpression expression) + { + while (expression is CastExpression castExpression) + expression = castExpression.Inner; + return expression; + } + + /// Unwrap ScopedApi wrappers to get the underlying expression. + private static ValueExpression Unwrap(ValueExpression expr) + => expr is ScopedApi scoped ? scoped.Original : expr; + + private static InputExampleValue InvokeGetEndpointValue(OperationSample sample, string parameterName) + { + var method = typeof(OperationSample).GetMethod("GetEndpointValue", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.IsNotNull(method); + var value = method!.Invoke(sample, [parameterName]); + Assert.IsNotNull(value); + return (InputExampleValue)value!; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmMethodProviderCollectionTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmMethodProviderCollectionTests.cs index b54ece36bd0..d23513876fe 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmMethodProviderCollectionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmMethodProviderCollectionTests.cs @@ -6,10 +6,12 @@ using System.ClientModel.Primitives; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.TypeSpec.Generator.ClientModel.Providers; +using Microsoft.TypeSpec.Generator.ClientModel.Providers.Samples; using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Input.Extensions; using Microsoft.TypeSpec.Generator.Primitives; @@ -1996,5 +1998,82 @@ public void ConvenienceMethod_JsonListBody_DoesNotUseXmlFromEnumerable() Assert.IsFalse(methodBody.Contains("rootNameHint")); Assert.IsFalse(methodBody.Contains("childNameHint")); } + + [Test] + public void Samples_AttachesProtocolAndConvenienceSamplesForEachExample() + { + var shortVersion = new InputOperationExample("ShortVersion", null, [], ""); + var allParameters = new InputOperationExample("AllParameters", null, [], ""); + var operation = InputFactory.Operation("GetWidget"); + SetOperationExamples(operation, [shortVersion, allParameters]); + var serviceMethod = InputFactory.BasicServiceMethod("GetWidget", operation); + var inputClient = InputFactory.Client("TestClient", methods: [serviceMethod]); + + MockHelpers.LoadMockGenerator(clients: () => [inputClient]); + + var client = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(inputClient)!; + var methodCollection = new ScmMethodProviderCollection(serviceMethod, client); + + var expectedCount = OperationSample.ShouldGenerateShortVersion(methodCollection) ? 4 : 3; + Assert.AreEqual(expectedCount, methodCollection.Samples.Count); + Assert.IsTrue(methodCollection.Samples.Any(s => !s.IsConvenienceSample && s.ExampleKey == "AllParameters")); + Assert.IsTrue(methodCollection.Samples.Any(s => s.IsConvenienceSample && s.ExampleKey == "AllParameters")); + Assert.IsTrue(methodCollection.Samples.Any(s => s.IsConvenienceSample && s.ExampleKey == "ShortVersion") || + !OperationSample.ShouldGenerateShortVersion(methodCollection)); + + if (OperationSample.ShouldGenerateShortVersion(methodCollection)) + { + Assert.IsTrue(methodCollection.Samples.Any(s => !s.IsConvenienceSample && s.ExampleKey == "ShortVersion")); + } + else + { + Assert.IsFalse(methodCollection.Samples.Any(s => !s.IsConvenienceSample && s.ExampleKey == "ShortVersion")); + } + } + + [Test] + public void Samples_DoesNotAttachConvenienceSamplesWhenConvenienceMethodIsDisabled() + { + var operation = InputFactory.Operation("GetWidget", generateConvenienceMethod: false); + SetOperationExamples( + operation, + [ + new InputOperationExample("ShortVersion", null, [], ""), + new InputOperationExample("AllParameters", null, [], "") + ]); + var serviceMethod = InputFactory.BasicServiceMethod("GetWidget", operation); + var inputClient = InputFactory.Client("TestClient", methods: [serviceMethod]); + + MockHelpers.LoadMockGenerator(clients: () => [inputClient]); + + var client = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(inputClient)!; + var methodCollection = new ScmMethodProviderCollection(serviceMethod, client); + + Assert.AreEqual(2, methodCollection.Samples.Count); + Assert.IsTrue(methodCollection.Samples.All(s => !s.IsConvenienceSample)); + } + + [Test] + public void Samples_WithoutExamples_ReturnsEmpty() + { + var operation = InputFactory.Operation("GetWidget"); + var serviceMethod = InputFactory.BasicServiceMethod("GetWidget", operation); + var inputClient = InputFactory.Client("TestClient", methods: [serviceMethod]); + + MockHelpers.LoadMockGenerator(clients: () => [inputClient]); + + var client = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(inputClient)!; + var methodCollection = new ScmMethodProviderCollection(serviceMethod, client); + + Assert.IsEmpty(methodCollection.Samples); + } + + private static void SetOperationExamples(InputOperation operation, IReadOnlyList examples) + { + var setter = typeof(InputOperation) + .GetProperty(nameof(InputOperation.Examples), BindingFlags.Instance | BindingFlags.Public)! + .GetSetMethod(nonPublic: true)!; + setter.Invoke(operation, [examples]); + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ScmKnownParametersTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ScmKnownParametersTests.cs index 40e7c9e92bd..06b74024358 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ScmKnownParametersTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ScmKnownParametersTests.cs @@ -4,6 +4,7 @@ using System; using Microsoft.TypeSpec.Generator.ClientModel.Primitives; using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.Providers; using NUnit.Framework; using static Microsoft.TypeSpec.Generator.Snippets.Snippet; @@ -40,5 +41,17 @@ public void RepeatabilityFirstSentParamHasDefaultValue() var expectedDefaultValue = Static(typeof(DateTimeOffset)).Property(nameof(DateTimeOffset.Now)); Assert.AreEqual(expectedDefaultValue, parameter.DefaultValue); } + + [Test] + public void KeyCredentialHasExpectedType() + { + Assert.AreEqual("ApiKeyCredential", ScmKnownParameters.KeyCredential.Type.Name); + } + + [Test] + public void TokenCredentialHasExpectedType() + { + Assert.AreEqual("AuthenticationTokenProvider", ScmKnownParameters.TokenCredential.Type.Name); + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/Properties/AssemblyInfo.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/Properties/AssemblyInfo.cs index f3131262a05..53b57a107a3 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/Properties/AssemblyInfo.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Input/src/Properties/AssemblyInfo.cs @@ -6,3 +6,5 @@ [assembly: InternalsVisibleTo("Microsoft.TypeSpec.Generator.Input.Tests.Perf, PublicKey=002400000480000094000000060200000024000052534131000400000100010041df4fe80c5af6ff9a410db5a173b0ce24ad68764c623e308b1584a88b1d1d82277f746c1cccba48997e13db3366d5ed676576ffd293293baf42c643f008ba2e8a556e25e529c0407a38506555340749559f5100e6fd78cc935bb6c82d2af303beb0d3c6563400659610759b4ed5cb2e0faf36b17e6842f04cdc544c74e051ba")] [assembly: InternalsVisibleTo("Microsoft.TypeSpec.Generator.Input.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010041df4fe80c5af6ff9a410db5a173b0ce24ad68764c623e308b1584a88b1d1d82277f746c1cccba48997e13db3366d5ed676576ffd293293baf42c643f008ba2e8a556e25e529c0407a38506555340749559f5100e6fd78cc935bb6c82d2af303beb0d3c6563400659610759b4ed5cb2e0faf36b17e6842f04cdc544c74e051ba")] [assembly: InternalsVisibleTo("Microsoft.TypeSpec.Generator.Tests.Common, PublicKey=002400000480000094000000060200000024000052534131000400000100010041df4fe80c5af6ff9a410db5a173b0ce24ad68764c623e308b1584a88b1d1d82277f746c1cccba48997e13db3366d5ed676576ffd293293baf42c643f008ba2e8a556e25e529c0407a38506555340749559f5100e6fd78cc935bb6c82d2af303beb0d3c6563400659610759b4ed5cb2e0faf36b17e6842f04cdc544c74e051ba")] +[assembly: InternalsVisibleTo("Microsoft.TypeSpec.Generator, PublicKey=002400000480000094000000060200000024000052534131000400000100010041df4fe80c5af6ff9a410db5a173b0ce24ad68764c623e308b1584a88b1d1d82277f746c1cccba48997e13db3366d5ed676576ffd293293baf42c643f008ba2e8a556e25e529c0407a38506555340749559f5100e6fd78cc935bb6c82d2af303beb0d3c6563400659610759b4ed5cb2e0faf36b17e6842f04cdc544c74e051ba")] +[assembly: InternalsVisibleTo("Microsoft.TypeSpec.Generator.ClientModel, PublicKey=002400000480000094000000060200000024000052534131000400000100010041df4fe80c5af6ff9a410db5a173b0ce24ad68764c623e308b1584a88b1d1d82277f746c1cccba48997e13db3366d5ed676576ffd293293baf42c643f008ba2e8a556e25e529c0407a38506555340749559f5100e6fd78cc935bb6c82d2af303beb0d3c6563400659610759b4ed5cb2e0faf36b17e6842f04cdc544c74e051ba")]