< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.OpenApi.OpenApiDocDescriptor
Assembly: Kestrun
File(s): File 1: /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_AnnotatedFunctions.cs
File 2: /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_BuildPath.cs
File 3: /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_BuildSchema.cs
File 4: /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Callbacks.cs
File 5: /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Examples.cs
File 6: /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Headers.cs
File 7: /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Helper.cs
File 8: /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Info.cs
File 9: /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Inline.cs
File 10: /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Links.cs
File 11: /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_MergeAttributes.cs
File 12: /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Parameter.cs
File 13: /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_PathOperation.cs
File 14: /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_RequestBody.cs
File 15: /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Response.cs
File 16: /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Schema.cs
File 17: /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Security.cs
File 18: /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Tags.cs
File 19: /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Webhook.cs
File 20: /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor.cs
Tag: Kestrun/Kestrun@eeafbe813231ed23417e7b339e170e307b2c86f9
Line coverage
42%
Covered lines: 1081
Uncovered lines: 1486
Coverable lines: 2567
Total lines: 7433
Line coverage: 42.1%
Branch coverage
34%
Covered branches: 673
Total branches: 1968
Branch coverage: 34.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 12/12/2025 - 17:27:19 Line coverage: 37% (507/1370) Branch coverage: 30.7% (347/1130) Total lines: 3659 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/14/2025 - 20:04:52 Line coverage: 36.9% (507/1371) Branch coverage: 30.7% (347/1130) Total lines: 3661 Tag: Kestrun/Kestrun@a05ac8de57c6207e227b92ba360e9d58869ac80a12/15/2025 - 18:44:50 Line coverage: 36.9% (507/1371) Branch coverage: 30.7% (347/1130) Total lines: 3660 Tag: Kestrun/Kestrun@6b9e56ea2de904fc3597033ef0f9bc7839d5d61812/18/2025 - 21:41:58 Line coverage: 36.9% (507/1371) Branch coverage: 30.7% (347/1128) Total lines: 3660 Tag: Kestrun/Kestrun@0d738bf294e6281b936d031e1979d928007495ff12/21/2025 - 06:07:10 Line coverage: 37.2% (512/1376) Branch coverage: 30.9% (348/1126) Total lines: 3679 Tag: Kestrun/Kestrun@8cf7f77e55fd1fd046ea4e5413eb9ef96e49fe6a12/23/2025 - 19:23:04 Line coverage: 34% (513/1507) Branch coverage: 28% (348/1242) Total lines: 4083 Tag: Kestrun/Kestrun@d062f281460e6c123c372aef61f8d957bbb6c90112/25/2025 - 19:20:44 Line coverage: 27.4% (427/1556) Branch coverage: 23.4% (297/1264) Total lines: 4242 Tag: Kestrun/Kestrun@5251f12f253e29f8a1dfb77edc2ef50b90a4f26f12/26/2025 - 18:43:06 Line coverage: 26.7% (429/1604) Branch coverage: 22.7% (297/1306) Total lines: 4366 Tag: Kestrun/Kestrun@66a9a3a4461391825b9a1ffc8190f76adb1bb67f12/27/2025 - 20:05:22 Line coverage: 25.1% (430/1707) Branch coverage: 21.5% (297/1376) Total lines: 4738 Tag: Kestrun/Kestrun@dec745d62965b14e1ed62c0f3ec815e60e53366f01/02/2026 - 00:16:25 Line coverage: 25% (430/1714) Branch coverage: 21.4% (297/1386) Total lines: 4746 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d001/02/2026 - 21:56:10 Line coverage: 25.1% (432/1719) Branch coverage: 21.4% (299/1396) Total lines: 4772 Tag: Kestrun/Kestrun@f60326065ebb24cf70b241e459b37baf142e6ed601/08/2026 - 02:20:28 Line coverage: 31.3% (546/1741) Branch coverage: 26.3% (383/1452) Total lines: 4900 Tag: Kestrun/Kestrun@4bc17b7e465c315de6386907c417e44fcb0fd3eb01/08/2026 - 08:19:25 Line coverage: 35.8% (624/1741) Branch coverage: 30% (436/1452) Total lines: 4901 Tag: Kestrun/Kestrun@6ab94ca7560634c2ac58b36c2b98e2a9b1bf305d01/09/2026 - 06:56:42 Line coverage: 32.1% (554/1725) Branch coverage: 26.3% (381/1448) Total lines: 4893 Tag: Kestrun/Kestrun@94f8107dc592fa7eaec45c0dd5f9fffbd41bc14501/11/2026 - 19:55:44 Line coverage: 34.3% (610/1778) Branch coverage: 28% (417/1488) Total lines: 5099 Tag: Kestrun/Kestrun@53c97a4806941d5aa8d4dcc6779071adf1ae537601/12/2026 - 18:03:06 Line coverage: 41.5% (740/1781) Branch coverage: 33% (492/1490) Total lines: 5112 Tag: Kestrun/Kestrun@956332ccc921363590dccd99d5707fb20b50966b01/14/2026 - 07:55:07 Line coverage: 43% (775/1800) Branch coverage: 31.7% (454/1430) Total lines: 5241 Tag: Kestrun/Kestrun@13bd81d8920e7e63e39aafdd188e7d766641ad3501/17/2026 - 04:33:35 Line coverage: 38.7% (742/1917) Branch coverage: 28.1% (428/1522) Total lines: 5619 Tag: Kestrun/Kestrun@aca34ea8d284564e2f9f6616dc937668dce926ba01/17/2026 - 18:18:02 Line coverage: 39% (753/1928) Branch coverage: 28.4% (434/1528) Total lines: 5667 Tag: Kestrun/Kestrun@8dd16f7908c0e15b594d16bb49be0240e2c7c01801/18/2026 - 06:40:41 Line coverage: 39.1% (763/1949) Branch coverage: 28.4% (439/1542) Total lines: 5726 Tag: Kestrun/Kestrun@99e92690d0fd95f6f4896f3410d2c024350a979401/18/2026 - 21:37:07 Line coverage: 37.2% (767/2060) Branch coverage: 27.6% (442/1600) Total lines: 6109 Tag: Kestrun/Kestrun@99c4ae445e8e5afc8b7080e01d5d9cdf39f972b801/19/2026 - 18:47:02 Line coverage: 36.9% (759/2054) Branch coverage: 27.3% (437/1598) Total lines: 6096 Tag: Kestrun/Kestrun@716db6917075bf04d6f8ae45a1bad48ca5cfacfe01/21/2026 - 17:07:46 Line coverage: 38.4% (819/2128) Branch coverage: 29.1% (480/1644) Total lines: 6261 Tag: Kestrun/Kestrun@3f6f61710c7ef7d5953cab578fe699c1e5e01a3601/21/2026 - 23:00:42 Line coverage: 38.1% (807/2116) Branch coverage: 29.1% (480/1644) Total lines: 6249 Tag: Kestrun/Kestrun@14e8864e34955316f20616ecfbeb1640fd06c40901/23/2026 - 00:12:18 Line coverage: 38% (823/2162) Branch coverage: 29.1% (487/1668) Total lines: 6369 Tag: Kestrun/Kestrun@67ed8a99376189d7ed94adba1b1854518edd75d902/05/2026 - 00:28:18 Line coverage: 39.2% (939/2392) Branch coverage: 31.1% (573/1842) Total lines: 6996 Tag: Kestrun/Kestrun@d9261bd752e45afa789d10bc0c82b7d5724d958902/18/2026 - 08:33:07 Line coverage: 42% (1078/2564) Branch coverage: 34.1% (670/1964) Total lines: 7418 Tag: Kestrun/Kestrun@bf8a937cfb7e8936c225b9df4608f8ddd85558b102/19/2026 - 11:34:19 Line coverage: 42.1% (1081/2567) Branch coverage: 34.1% (673/1968) Total lines: 7433 Tag: Kestrun/Kestrun@8aa46e1988031758b311143cd39bf5749fbcd39e 12/12/2025 - 17:27:19 Line coverage: 37% (507/1370) Branch coverage: 30.7% (347/1130) Total lines: 3659 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/14/2025 - 20:04:52 Line coverage: 36.9% (507/1371) Branch coverage: 30.7% (347/1130) Total lines: 3661 Tag: Kestrun/Kestrun@a05ac8de57c6207e227b92ba360e9d58869ac80a12/15/2025 - 18:44:50 Line coverage: 36.9% (507/1371) Branch coverage: 30.7% (347/1130) Total lines: 3660 Tag: Kestrun/Kestrun@6b9e56ea2de904fc3597033ef0f9bc7839d5d61812/18/2025 - 21:41:58 Line coverage: 36.9% (507/1371) Branch coverage: 30.7% (347/1128) Total lines: 3660 Tag: Kestrun/Kestrun@0d738bf294e6281b936d031e1979d928007495ff12/21/2025 - 06:07:10 Line coverage: 37.2% (512/1376) Branch coverage: 30.9% (348/1126) Total lines: 3679 Tag: Kestrun/Kestrun@8cf7f77e55fd1fd046ea4e5413eb9ef96e49fe6a12/23/2025 - 19:23:04 Line coverage: 34% (513/1507) Branch coverage: 28% (348/1242) Total lines: 4083 Tag: Kestrun/Kestrun@d062f281460e6c123c372aef61f8d957bbb6c90112/25/2025 - 19:20:44 Line coverage: 27.4% (427/1556) Branch coverage: 23.4% (297/1264) Total lines: 4242 Tag: Kestrun/Kestrun@5251f12f253e29f8a1dfb77edc2ef50b90a4f26f12/26/2025 - 18:43:06 Line coverage: 26.7% (429/1604) Branch coverage: 22.7% (297/1306) Total lines: 4366 Tag: Kestrun/Kestrun@66a9a3a4461391825b9a1ffc8190f76adb1bb67f12/27/2025 - 20:05:22 Line coverage: 25.1% (430/1707) Branch coverage: 21.5% (297/1376) Total lines: 4738 Tag: Kestrun/Kestrun@dec745d62965b14e1ed62c0f3ec815e60e53366f01/02/2026 - 00:16:25 Line coverage: 25% (430/1714) Branch coverage: 21.4% (297/1386) Total lines: 4746 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d001/02/2026 - 21:56:10 Line coverage: 25.1% (432/1719) Branch coverage: 21.4% (299/1396) Total lines: 4772 Tag: Kestrun/Kestrun@f60326065ebb24cf70b241e459b37baf142e6ed601/08/2026 - 02:20:28 Line coverage: 31.3% (546/1741) Branch coverage: 26.3% (383/1452) Total lines: 4900 Tag: Kestrun/Kestrun@4bc17b7e465c315de6386907c417e44fcb0fd3eb01/08/2026 - 08:19:25 Line coverage: 35.8% (624/1741) Branch coverage: 30% (436/1452) Total lines: 4901 Tag: Kestrun/Kestrun@6ab94ca7560634c2ac58b36c2b98e2a9b1bf305d01/09/2026 - 06:56:42 Line coverage: 32.1% (554/1725) Branch coverage: 26.3% (381/1448) Total lines: 4893 Tag: Kestrun/Kestrun@94f8107dc592fa7eaec45c0dd5f9fffbd41bc14501/11/2026 - 19:55:44 Line coverage: 34.3% (610/1778) Branch coverage: 28% (417/1488) Total lines: 5099 Tag: Kestrun/Kestrun@53c97a4806941d5aa8d4dcc6779071adf1ae537601/12/2026 - 18:03:06 Line coverage: 41.5% (740/1781) Branch coverage: 33% (492/1490) Total lines: 5112 Tag: Kestrun/Kestrun@956332ccc921363590dccd99d5707fb20b50966b01/14/2026 - 07:55:07 Line coverage: 43% (775/1800) Branch coverage: 31.7% (454/1430) Total lines: 5241 Tag: Kestrun/Kestrun@13bd81d8920e7e63e39aafdd188e7d766641ad3501/17/2026 - 04:33:35 Line coverage: 38.7% (742/1917) Branch coverage: 28.1% (428/1522) Total lines: 5619 Tag: Kestrun/Kestrun@aca34ea8d284564e2f9f6616dc937668dce926ba01/17/2026 - 18:18:02 Line coverage: 39% (753/1928) Branch coverage: 28.4% (434/1528) Total lines: 5667 Tag: Kestrun/Kestrun@8dd16f7908c0e15b594d16bb49be0240e2c7c01801/18/2026 - 06:40:41 Line coverage: 39.1% (763/1949) Branch coverage: 28.4% (439/1542) Total lines: 5726 Tag: Kestrun/Kestrun@99e92690d0fd95f6f4896f3410d2c024350a979401/18/2026 - 21:37:07 Line coverage: 37.2% (767/2060) Branch coverage: 27.6% (442/1600) Total lines: 6109 Tag: Kestrun/Kestrun@99c4ae445e8e5afc8b7080e01d5d9cdf39f972b801/19/2026 - 18:47:02 Line coverage: 36.9% (759/2054) Branch coverage: 27.3% (437/1598) Total lines: 6096 Tag: Kestrun/Kestrun@716db6917075bf04d6f8ae45a1bad48ca5cfacfe01/21/2026 - 17:07:46 Line coverage: 38.4% (819/2128) Branch coverage: 29.1% (480/1644) Total lines: 6261 Tag: Kestrun/Kestrun@3f6f61710c7ef7d5953cab578fe699c1e5e01a3601/21/2026 - 23:00:42 Line coverage: 38.1% (807/2116) Branch coverage: 29.1% (480/1644) Total lines: 6249 Tag: Kestrun/Kestrun@14e8864e34955316f20616ecfbeb1640fd06c40901/23/2026 - 00:12:18 Line coverage: 38% (823/2162) Branch coverage: 29.1% (487/1668) Total lines: 6369 Tag: Kestrun/Kestrun@67ed8a99376189d7ed94adba1b1854518edd75d902/05/2026 - 00:28:18 Line coverage: 39.2% (939/2392) Branch coverage: 31.1% (573/1842) Total lines: 6996 Tag: Kestrun/Kestrun@d9261bd752e45afa789d10bc0c82b7d5724d958902/18/2026 - 08:33:07 Line coverage: 42% (1078/2564) Branch coverage: 34.1% (670/1964) Total lines: 7418 Tag: Kestrun/Kestrun@bf8a937cfb7e8936c225b9df4608f8ddd85558b102/19/2026 - 11:34:19 Line coverage: 42.1% (1081/2567) Branch coverage: 34.1% (673/1968) Total lines: 7433 Tag: Kestrun/Kestrun@8aa46e1988031758b311143cd39bf5749fbcd39e

Coverage delta

Coverage delta 8 -8

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: LoadAnnotatedFunctions(...)0%7280%
File 1: ProcessFunction(...)0%2040%
File 1: ProcessFunctionAttributes(...)0%930300%
File 1: ApplyFormBindingAttribute(...)0%2040%
File 1: ApplyExtensionAttribute(...)100%66100%
File 1: ApplyPathAttribute(...)0%272160%
File 1: ApplyPathLikePath(...)0%4260%
File 1: AddQueryParametersFromTemplate(...)0%7280%
File 1: ApplyPathLikeWebhook(...)0%2040%
File 1: ApplyPathLikeCallback(...)0%2040%
File 1: ChooseFirstNonEmpty(...)0%2040%
File 1: NormalizeNewlines(...)0%620%
File 1: ApplyResponseRefAttribute(...)0%342180%
File 1: ApplyResponseAttribute(...)58.33%141275%
File 1: SetDefaultResponseContentType(...)60%121075%
File 1: TryInferClrTypeFromSchema(...)0%110100%
File 1: InferNonArrayClrType(...)0%156120%
File 1: InferIntegerClrType(...)0%620%
File 1: InferNumberClrType(...)0%620%
File 1: InferStringClrType(...)0%156120%
File 1: SelectDefaultSuccessResponse(...)0%2040%
File 1: TryParseStatusCode(...)0%620%
File 1: HasContent(...)0%2040%
File 1: ApplyPropertyAttribute(...)0%342180%
File 1: ApplyAuthorizationAttribute(...)0%620%
File 1: BuildPolicyList(...)0%620%
File 1: ProcessParameters(...)0%1190340%
File 1: ApplyParameterAttribute(...)0%272160%
File 1: ApplyParameterRefAttribute(...)0%210140%
File 1: ApplyParameterExampleRefAttribute(...)0%110100%
File 1: RemoveExistingParameter(...)0%4260%
File 1: ApplyRequestBodyRefAttribute(...)0%2040%
File 1: ResolveRequestBodyReferenceId(...)0%156120%
File 1: FindReferenceIdForParameter(...)0%2040%
File 1: TryGetFirstRequestBodySchema(...)0%7280%
File 1: IsRequestBodySchemaMatchForParameter(...)0%7280%
File 1: ApplyRequestBodyAttribute(...)0%506220%
File 1: ResolveFormOptions(...)0%7280%
File 1: ApplyFormRequestBody(...)0%620%
File 1: ApplyRequestBodyExampleRefAttribute(...)0%7280%
File 1: BuildFormRequestBodyWithSchema(...)0%7280%
File 1: ResolveFormContentTypes(...)0%4260%
File 1: IsMultipartContentType(...)100%210%
File 1: BuildMultipartEncoding(...)0%110100%
File 1: IsProbablyFileRule(...)0%210140%
File 1: ApplyPreferredRequestBody(...)0%2040%
File 1: EnsureDefaultResponses(...)0%4260%
File 1: FinalizeRouteOptions(...)0%7280%
File 1: FinalizePathRouteOptions(...)0%4260%
File 1: RegisterWebhook(...)0%620%
File 1: RegisterCallback(...)0%620%
File 1: GetDocDescriptorOrThrow(...)0%620%
File 1: EnsureParamOnlyScriptBlock(...)0%620%
File 1: CreateRequestBodyFromAttribute(...)0%7280%
File 2: BuildPathsFromRegisteredRoutes(...)66.66%6688.88%
File 2: CreateOpenApiRouteEntries()91.66%121291.66%
File 2: ProcessOpenApiRouteGroup(...)75%4487.5%
File 2: GetOrCreatePathItem(...)66.66%66100%
File 2: ProcessOpenApiRouteEntry(...)57.14%161476.92%
File 2: .ctor(...)100%11100%
File 2: get_Pattern()100%11100%
File 2: get_Method()100%11100%
File 2: get_Map()100%11100%
File 2: get_Metadata()100%11100%
File 2: ApplyPathLevelMetadata(...)0%620%
File 2: ApplyPathLevelServers(...)0%420200%
File 2: ApplyPathLevelParameters(...)0%420200%
File 3: .ctor(...)100%11100%
File 3: MergeXmlAttributes(...)100%1414100%
File 3: BuildSchema(...)100%88100%
File 3: BuildPropertySchema(...)75%4492.3%
File 3: UnwrapNullableType(...)100%22100%
File 3: BuildFilePartSchema(...)0%620%
File 3: ApplyKrPartScope(...)100%1010100%
File 3: BuildPropertyTypeSchema(...)100%44100%
File 3: ApplyNullableSchema(...)75%8894.11%
File 3: ShouldPushNestedScope(...)62.5%8883.33%
File 3: BuildComplexTypeSchema(...)100%11100%
File 3: BuildEnumSchema(...)0%620%
File 3: BuildArraySchema(...)75%4473.68%
File 3: BuildPrimitiveSchema(...)100%210%
File 3: GetOrCreateSchemaItem(...)0%156120%
File 3: TryGetSchemaItem(...)0%2040%
File 3: TryGetSchemaItem(...)100%210%
File 4: ApplyCallbackRefAttribute(...)0%156120%
File 4: BuildCallbacks(...)0%4260%
File 4: ProcessCallbacksGroup(...)0%4260%
File 4: ProcessCallbackOperation(...)0%156120%
File 4: GetOrCreateCallbackItem(...)0%156120%
File 4: ApplyCallbacks(...)50%5466.66%
File 5: AddComponentExample(...)75%44100%
File 5: TryAddExample(...)80%101090%
File 5: NewOpenApiExample(...)0%620%
File 5: NewOpenApiExample(...)100%210%
File 5: NewOpenApiExternalExample(...)100%210%
File 5: NewOpenApiExample(...)0%620%
File 6: NewOpenApiHeader(...)50%22100%
File 6: ResolveHeaderSchema(...)100%44100%
File 6: ThrowIfBothSchemaAndContentProvided(...)100%44100%
File 6: ApplyHeaderSchema(...)100%22100%
File 6: ApplyHeaderExamples(...)100%1010100%
File 6: ResolveHeaderExampleValue(...)75%9872.72%
File 6: ApplyHeaderContent(...)90%101087.5%
File 6: ResolveHeaderMediaTypeValue(...)50%18845.45%
File 6: AddComponentHeader(...)0%2040%
File 6: ApplyResponseHeaderAttribute(...)0%156120%
File 6: TryAddHeader(...)0%7280%
File 6: TryGetHeaderItem(...)0%2040%
File 7: GetSchema(...)0%7280%
File 7: GetParameter(...)0%7280%
File 7: GetRequestBody(...)0%7280%
File 7: GetHeader(...)0%7280%
File 7: GetResponse(...)0%7280%
File 7: ComponentSchemasExists(...)50%44100%
File 7: ComponentRequestBodiesExists(...)0%2040%
File 7: ComponentResponsesExists(...)0%2040%
File 7: ComponentParametersExists(...)0%2040%
File 7: ComponentExamplesExists(...)0%2040%
File 7: ComponentHeadersExists(...)0%2040%
File 7: ComponentCallbacksExists(...)0%2040%
File 7: ComponentLinksExists(...)0%2040%
File 7: ComponentPathItemsExists(...)0%2040%
File 7: BuildExtensions(...)87.5%161688.23%
File 8: CreateExternalDocs(...)100%11100%
File 8: CreateExternalDocs(...)50%2266.66%
File 8: CreateInfoContact(...)100%66100%
File 9: AddInlineExample(...)100%22100%
File 9: AddInlineLink(...)100%22100%
File 9: AddComponent(...)83.33%6692.3%
File 9: TryGetComponent(...)100%11100%
File 9: TryGetInline(...)100%11100%
File 9: TryGetFromComponents(...)42.85%271460%
File 9: TryGetAndCast()100%44100%
File 9: ValidateComponentType(...)57.14%211466.66%
File 9: TryGet(...)100%44100%
File 9: ThrowTypeMismatch(...)100%11100%
File 10: AddComponentLink(...)75%44100%
File 10: TryAddLink(...)87.5%88100%
File 10: ApplyResponseLinkAttribute(...)25%371244.44%
File 10: ApplyLinkRefAttribute(...)0%620%
File 10: NewOpenApiLink(...)100%210%
File 10: ValidateLinkOperation(...)0%7280%
File 10: ApplyLinkDescription(...)0%620%
File 10: ApplyLinkServer(...)0%620%
File 10: ApplyLinkOperation(...)0%2040%
File 10: ApplyLinkRequestBody(...)0%4260%
File 10: ApplyLinkParameters(...)0%156120%
File 10: ToRuntimeExpressionAnyWrapper(...)0%620%
File 11: MergeSchemaAttributes(...)0%7280%
File 11: MergeStringProperties(...)0%156120%
File 11: MergeEnumAndCollections(...)0%110100%
File 11: MergeNumericProperties(...)0%110100%
File 11: MergeBooleanProperties(...)100%210%
File 11: MergeTypeAndRequired(...)0%7280%
File 11: MergeCustomFields(...)0%4260%
File 12: CreateParameterFromAttribute(...)0%2040%
File 12: ApplyParameterAttribute(...)0%156120%
File 12: ApplyExampleRefAttribute(...)0%156120%
File 12: ProcessParameterComponent(...)75%1212100%
File 12: ApplyParameterCommonFields(...)50%22100%
File 12: TryApplyVariableTypeSchema(...)40%402063.15%
File 12: ProcessParameterExampleRef(...)0%2040%
File 12: ValidateParameterHasSchemaOrContent(...)0%4260%
File 12: AddExampleToParameterExamples(...)0%620%
File 12: AddExamplesToContentMediaTypes(...)0%7280%
File 12: AddExamplesToContentMediaTypes(...)0%110100%
File 12: ProcessPowerShellAttribute(...)75%4477.77%
File 12: ValidateParameterHasSchemaOrContentForPowerShell(...)50%11650%
File 12: ApplyPowerShellAttributeToParameter(...)50%3240%
File 12: ApplyPowerShellAttributeToRequestBody(...)0%2040%
File 12: ApplyPowerShellAttributeToMediaTypeSchemas(...)0%2040%
File 12: ApplyPowerShellAttributesToSchema(...)100%11100%
File 12: ApplyItemConstraints(...)50%5460%
File 12: ApplyRangeConstraints(...)100%44100%
File 12: ApplyLengthConstraints(...)50%5460%
File 12: ApplyPatternConstraints(...)50%2266.66%
File 12: ApplyAllowedValuesConstraints(...)50%5466.66%
File 12: ApplyNullabilityConstraints(...)50%9657.14%
File 12: GetOrCreateParameterItem(...)50%131280%
File 12: TryGetParameterItem(...)75%4477.77%
File 12: TryGetParameterItem(...)100%11100%
File 13: BuildOperationFromMetadata(...)87.5%88100%
File 13: EnsureAutoClientErrorResponses(...)75%8890%
File 13: GetAutoClientErrorStatuses(...)87.5%1616100%
File 13: AddMissingAutoClientErrorResponses(...)83.33%66100%
File 13: ResponseKeyExists(...)100%11100%
File 13: EnsureAutoErrorSchemaComponent()75%88100%
File 13: GetAutoErrorResponseContentTypes()50%6688.88%
File 13: CreateAutoClientErrorResponse(...)90%101094.73%
File 13: ApplyExtensions(...)50%6450%
File 13: ApplyTags(...)83.33%66100%
File 13: ApplyServers(...)15%1392033.33%
File 13: ApplyParameters(...)90.9%232285.71%
File 13: ApplySecurity(...)28.57%1371414.28%
File 14: CloneExampleOrThrow(...)0%7280%
File 14: ProcessRequestBodyComponent(...)0%620%
File 14: ApplyRequestBodyCommonFields(...)100%210%
File 14: TryApplyVariableTypeSchema(...)0%210140%
File 14: ProcessRequestBodyExampleRef(...)0%4260%
File 14: GetOrCreateRequestBodyItem(...)0%156120%
File 14: TryGetRequestBodyItem(...)50%5455.55%
File 14: TryGetRequestBodyItem(...)100%11100%
File 15: GetKeyOverride(...)0%620%
File 15: CreateResponseFromAttribute(...)8.33%301250%
File 15: ApplyResponseAttribute(...)100%11100%
File 15: ApplyDescription(...)100%22100%
File 15: ResolveResponseSchema(...)25%7440%
File 15: ApplySchemaToContentTypes(...)91.66%121287.5%
File 15: ApplyHeaderRefAttribute(...)0%620%
File 15: ApplyHeaderAttribute(...)0%2040%
File 15: ApplyExampleRefAttribute(...)0%7280%
File 15: ApplyExampleRefAttribute(...)0%4260%
File 15: ResolveExampleTargets(...)0%7280%
File 15: ResolveExampleTargets(...)0%7280%
File 15: GetOrAddMediaType(...)100%44100%
File 15: CloneSchemaOrThrow(...)0%4260%
File 15: ProcessResponseExampleRef(...)0%4260%
File 15: ProcessResponseLinkRef(...)0%4260%
File 15: ProcessResponseHeaderRef(...)0%4260%
File 15: ProcessResponseComponent(...)100%210%
File 15: GetOrCreateResponseItem(...)0%156120%
File 15: TryGetResponseItem(...)0%2040%
File 15: TryGetResponseItem(...)100%210%
File 16: GetSchemaIdentity(...)0%7280%
File 16: BuildSchemaForType(...)91.66%121294.44%
File 16: TryBuildFormPayloadSchemaParent(...)66.66%66100%
File 16: ProcessExtensions(...)33.33%17633.33%
File 16: TryBuildPrimitiveSchema(...)100%22100%
File 16: TryBuildDerivedSchemaFromBaseType(...)72.22%231875%
File 16: HasComposableBaseType(...)50%22100%
File 16: TryResolveSimpleOrReferenceBaseSchema(...)25%9433.33%
File 16: IsSimpleSchemaOrReference(...)50%44100%
File 16: TryResolveArrayWrapperDerivedSchema(...)25%41820%
File 16: CreateAllOfAdditionalObjectSchema(...)100%210%
File 16: CreateSchemaForDeclaredProperties(...)100%22100%
File 16: ComposeWithParentSchema(...)30%191054.54%
File 16: BuildBaseTypeSchema(...)50%2266.66%
File 16: BuildCustomBaseTypeSchema(...)31.25%201675.86%
File 16: RegisterEnumSchema(...)75%44100%
File 16: ApplyTypeAttributes(...)100%11100%
File 16: ApplyGeneratedRequiredPropertiesMetadata(...)16.66%891218.75%
File 16: ApplySchemaComponentAttributes(...)100%22100%
File 16: ApplySchemaComponentExamples(...)16.66%19628.57%
File 16: ApplyPatternProperties(...)100%88100%
File 16: BuildPatternSchema(...)50%8438.46%
File 16: ProcessTypeProperties(...)93.75%1616100%
File 16: ShouldSkipRuntimeFormPayloadStorageProperty(...)37.5%88100%
File 16: TryCreateTypeInstance(...)100%11100%
File 16: CapturePropertyDefault(...)100%9877.77%
File 16: IsIntrinsicDefault(...)100%1616100%
File 16: MakeNullable(...)75%44100%
File 16: InferPrimitiveSchema(...)91.66%121291.66%
File 16: InferArraySchema(...)33.33%8660%
File 16: InferPowerShellClassSchema(...)0%4260%
File 16: .cctor()100%1156.6%
File 16: ApplySchemaAttr(...)100%66100%
File 16: ApplyConcreteSchemaAttributes(...)25%8890.9%
File 16: ApplyTitleAndDescription(...)83.33%6680%
File 16: ApplySchemaType(...)28.57%441446.66%
File 16: ApplyFormatAndNumericBounds(...)50%461238.46%
File 16: ApplyLengthAndPattern(...)50%9657.14%
File 16: ApplyCollectionConstraints(...)50%191054.54%
File 16: ApplyFlags(...)100%44100%
File 16: ApplyAdditionalProperties(...)83.33%6677.77%
File 16: ApplyArrayAdditionalProperties(...)100%210%
File 16: EnsureAdditionalPropertiesAllowed(...)75%8883.33%
File 16: IsObjectSchemaType(...)50%22100%
File 16: ApplyExamplesAndDefaults(...)44.44%711845.45%
File 16: ApplyXmlMetadata(...)100%2222100%
File 16: ApplyReferenceSchemaAttributes(...)75%4480%
File 17: ApplySecurityScheme(...)81.25%191678.57%
File 17: GetSecurityScheme(...)100%22100%
File 17: GetSecurityScheme(...)50%22100%
File 17: GetSecurityScheme(...)0%2040%
File 17: GetSecurityScheme(...)0%156120%
File 17: GetSecurityScheme(...)100%11100%
File 17: GetSecurityScheme(...)100%11100%
File 17: GetSecurityScheme(...)100%11100%
File 17: GetSecurityScheme(...)100%11100%
File 17: AddSecurityComponent(...)100%44100%
File 18: AddTag(...)100%1010100%
File 18: AddTagIfMissing(...)100%22100%
File 18: RemoveTag(...)50%2266.66%
File 18: RemoveTag(...)50%22100%
File 18: GetOrCreateTagItem(...)100%44100%
File 18: TryGetTag(...)100%22100%
File 18: ContainsTag(...)100%22100%
File 19: BuildWebhooks(...)50%23622.22%
File 19: ProcessWebhookGroup(...)0%4260%
File 19: ProcessWebhookOperation(...)100%210%
File 19: GetOrCreateWebhookItem(...)0%4260%
File 20: .cctor()100%11100%
File 20: get_Host()100%11100%
File 20: get_DocumentId()100%11100%
File 20: get_Document()100%11100%
File 20: get_AutoErrorResponseSchemaId()100%11100%
File 20: get_AutoErrorResponseContentTypes()100%11100%
File 20: get_SecurityRequirement()100%11100%
File 20: get_InlineComponents()100%11100%
File 20: get_WebHook()100%11100%
File 20: get_Callbacks()100%11100%
File 20: .ctor(...)100%11100%
File 20: get_HasBeenGenerated()100%11100%
File 20: GenerateComponents(...)50%22100%
File 20: ProcessComponentTypes(...)66.66%6683.33%
File 20: GenerateComponents()100%11100%
File 20: AddFormOptions(...)100%1010100%
File 20: AddFormPartRules(...)100%88100%
File 20: BuildFormOptionsSchema(...)50%4475%
File 20: ProcessVariableAnnotations(...)91.66%1212100%
File 20: DispatchComponentAnnotations(...)45.45%1982228.57%
File 20: ProcessVariableExtension(...)0%110100%
File 20: TryApplyVariableTypeSchema(...)0%156120%
File 20: ApplyResponseCommonFields(...)0%2040%
File 20: GenerateDoc()100%11100%
File 20: ReadAndDiagnose(...)100%210%
File 20: ToJson(...)100%11100%
File 20: ToYaml(...)100%210%
File 20: AddOpenApiExtension(...)0%4260%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_AnnotatedFunctions.cs

#LineLine coverage
 1using System.Management.Automation;
 2using System.Reflection;
 3using System.Management.Automation.Internal;
 4using System.Management.Automation.Language;
 5using System.Text.Json.Nodes;
 6using Kestrun.Forms;
 7using Kestrun.Hosting;
 8using Kestrun.Hosting.Options;
 9using Kestrun.Languages;
 10using Kestrun.Utilities;
 11using Microsoft.OpenApi;
 12
 13namespace Kestrun.OpenApi;
 14
 15public partial class OpenApiDocDescriptor
 16{
 17    /// <summary>
 18    /// Enumerates all in-session PowerShell functions in the given runspace,
 19    /// detects those annotated with [OpenApiPath], and maps them into the provided KestrunHost.
 20    /// </summary>
 21    /// <param name="cmdInfos">List of FunctionInfo objects representing PowerShell functions.</param>
 22    public void LoadAnnotatedFunctions(List<FunctionInfo> cmdInfos)
 23    {
 024        ArgumentNullException.ThrowIfNull(cmdInfos);
 025        var callbacks = cmdInfos
 026                .Where(f => f.ScriptBlock.Attributes?.All(a => a is OpenApiCallbackAttribute) != false);
 27
 028        var others = cmdInfos
 029            .Where(f => f.ScriptBlock.Attributes?.All(a => a is not OpenApiCallbackAttribute) != false);
 30        // (equivalent to NOT having any callback attribute)
 31
 032        foreach (var func in callbacks)
 33        {
 034            ProcessFunction(func);
 35        }
 36
 037        BuildCallbacks(Callbacks);
 038        foreach (var func in others)
 39        {
 040            ProcessFunction(func);
 41        }
 042    }
 43
 44    /// <summary>
 45    /// Processes a single PowerShell function, extracting OpenAPI annotations and configuring the host accordingly.
 46    /// </summary>
 47    /// <param name="func">The function information.</param>
 48    private void ProcessFunction(FunctionInfo func)
 49    {
 50        try
 51        {
 052            var help = func.GetHelp();
 053            var sb = func.ScriptBlock;
 054            if (sb is null)
 55            {
 056                return;
 57            }
 58
 059            var attrs = sb.Attributes;
 060            if (attrs.Count == 0)
 61            {
 062                return;
 63            }
 64            // Create route options and OpenAPI metadata
 065            var routeOptions = new MapRouteOptions
 066            {
 067                // Seed defaults up-front so response attributes can safely add per-status entries
 068                // without losing the host-level 'default' fallback.
 069                DefaultResponseContentType = new Dictionary<string, ICollection<ContentTypeWithSchema>>(Host.Options.Def
 070            };
 071            var openApiMetadata = new OpenAPIPathMetadata(mapOptions: routeOptions);
 72            // Process attributes to populate route options and OpenAPI metadata
 073            var parsedVerb = ProcessFunctionAttributes(func, help!, attrs, routeOptions, openApiMetadata);
 74
 075            ProcessParameters(func, help!, routeOptions, openApiMetadata);
 76
 077            EnsureDefaultResponses(openApiMetadata);
 078            FinalizeRouteOptions(func, sb, openApiMetadata, routeOptions, parsedVerb);
 079        }
 080        catch (Exception ex)
 81        {
 082            Host.Logger.Error("Error loading OpenAPI annotated function '{funcName}': {message}", func.Name, ex.Message)
 083        }
 084    }
 85
 86    /// <summary>
 87    /// Processes the OpenAPI-related attributes on the specified function.
 88    /// </summary>
 89    /// <param name="func">The function information.</param>
 90    /// <param name="help">The comment help information.</param>
 91    /// <param name="attrs">The collection of attributes applied to the function.</param>
 92    /// <param name="routeOptions">The route options to configure.</param>
 93    /// <param name="openApiMetadata">The OpenAPI metadata to populate.</param>
 94    /// <returns>The parsed HTTP verb for the function.</returns>
 95    private HttpVerb ProcessFunctionAttributes(
 96        FunctionInfo func,
 97        CommentHelpInfo help,
 98        IReadOnlyCollection<Attribute> attrs,
 99        MapRouteOptions routeOptions,
 100        OpenAPIPathMetadata openApiMetadata)
 101    {
 0102        var parsedVerb = HttpVerb.Get;
 103
 0104        foreach (var attr in attrs)
 105        {
 106            try
 107            {
 108                switch (attr)
 109                {
 110                    case OpenApiPathAttribute path:
 0111                        parsedVerb = ApplyPathAttribute(func, help, routeOptions, openApiMetadata, parsedVerb, path);
 0112                        break;
 113                    case OpenApiWebhookAttribute webhook:
 0114                        parsedVerb = ApplyPathAttribute(func, help, routeOptions, openApiMetadata, parsedVerb, webhook);
 0115                        break;
 116                    case OpenApiCallbackAttribute callbackOperation:
 0117                        parsedVerb = ApplyPathAttribute(func, help, routeOptions, openApiMetadata, parsedVerb, callbackO
 0118                        break;
 119                    case OpenApiExtensionAttribute extensionAttr:
 0120                        ApplyExtensionAttribute(openApiMetadata, extensionAttr);
 0121                        break;
 122                    case OpenApiResponseRefAttribute responseRef:
 0123                        ApplyResponseRefAttribute(openApiMetadata, responseRef, routeOptions);
 0124                        break;
 125                    case OpenApiResponseAttribute responseAttr:
 0126                        ApplyResponseAttribute(openApiMetadata, responseAttr, routeOptions);
 0127                        break;
 128                    case OpenApiResponseExampleRefAttribute responseAttr:
 0129                        ApplyResponseAttribute(openApiMetadata, responseAttr, routeOptions);
 0130                        break;
 131                    case OpenApiResponseLinkRefAttribute linkRefAttr:
 0132                        ApplyResponseLinkAttribute(openApiMetadata, linkRefAttr);
 0133                        break;
 134                    case OpenApiPropertyAttribute propertyAttr:
 0135                        ApplyPropertyAttribute(openApiMetadata, propertyAttr);
 0136                        break;
 137                    case OpenApiAuthorizationAttribute authAttr:
 0138                        ApplyAuthorizationAttribute(routeOptions, openApiMetadata, authAttr);
 0139                        break;
 140                    case IOpenApiResponseHeaderAttribute responseHeaderAttr:
 0141                        ApplyResponseHeaderAttribute(openApiMetadata, responseHeaderAttr);
 0142                        break;
 143                    case OpenApiCallbackRefAttribute callbackRefAttr:
 0144                        ApplyCallbackRefAttribute(openApiMetadata, callbackRefAttr);
 0145                        break;
 146                    case KrBindFormAttribute formAttr:
 0147                        ApplyFormBindingAttribute(routeOptions, formAttr);
 0148                        break;
 149                    case KestrunAnnotation ka:
 0150                        throw new InvalidOperationException($"Unhandled Kestrun annotation: {ka.GetType().Name}");
 151                }
 0152            }
 0153            catch (InvalidOperationException ex)
 154            {
 0155                Host.Logger.Error(ex, "Error processing OpenApiPath attribute on function '{funcName}': {message}", func
 0156            }
 0157            catch (Exception ex)
 158            {
 0159                Host.Logger.Error(ex, "Error processing OpenApiPath attribute on function '{funcName}': {message}", func
 0160            }
 161        }
 162
 0163        return parsedVerb;
 164    }
 165
 166    private void ApplyFormBindingAttribute(MapRouteOptions routeOptions, KrBindFormAttribute formAttr)
 167    {
 0168        if (formAttr.Template is not null)
 169        {
 0170            if (!Host.Runtime.FormOptions.TryGetValue(formAttr.Template, out var template))
 171            {
 0172                throw new InvalidOperationException($"Form options template '{formAttr.Template}' not found.");
 173            }
 174
 175            // Clone the template to avoid modifying the original
 0176            routeOptions.FormOptions = new KrFormOptions(template);
 0177            return;
 178        }
 179
 180        // If no template is specified, apply the attribute properties directly
 0181        var formOptions = FormHelper.ApplyKrPartAttributes(formAttr);
 182
 183        // Assign the form options to the route options
 0184        routeOptions.FormOptions = formOptions;
 0185    }
 186
 187    /// <summary>
 188    /// Applies the OpenApiExtension attribute to the function's OpenAPI metadata.
 189    /// </summary>
 190    /// <param name="openApiMetadata">The OpenAPI metadata to which the extension will be applied.</param>
 191    /// <param name="extensionAttr">The OpenApiExtension attribute containing the extension data.</param>
 192    private void ApplyExtensionAttribute(OpenAPIPathMetadata openApiMetadata, OpenApiExtensionAttribute extensionAttr)
 193    {
 3194        if (Host.Logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 195        {
 3196            Host.Logger.Debug("Applying OpenApiExtension '{extensionName}' to function metadata", extensionAttr.Name);
 197        }
 3198        openApiMetadata.Extensions ??= [];
 199
 200        // Parse string into a JsonNode tree.
 3201        var node = JsonNode.Parse(extensionAttr.Json);
 3202        if (node is null)
 203        {
 1204            Host.Logger.Error("Error parsing OpenAPI extension '{extensionName}': JSON is null", extensionAttr.Name);
 1205            return;
 206        }
 2207        openApiMetadata.Extensions[extensionAttr.Name] = new JsonNodeExtension(node);
 2208    }
 209
 210    /// <summary>
 211    /// Applies the OpenApiPath attribute to the function's route options and metadata.
 212    /// </summary>
 213    /// <param name="func">The function information.</param>
 214    /// <param name="help">The comment help information.</param>
 215    /// <param name="routeOptions">The route options to configure.</param>
 216    /// <param name="metadata">The OpenAPI metadata to populate.</param>
 217    /// <param name="parsedVerb">The currently parsed HTTP verb.</param>
 218    /// <param name="oaPath">The OpenApiPath attribute instance.</param>
 219    /// <returns>The updated HTTP verb after processing the attribute.</returns>
 220    private static HttpVerb ApplyPathAttribute(
 221        FunctionInfo func,
 222        CommentHelpInfo help,
 223        MapRouteOptions routeOptions,
 224        OpenAPIPathMetadata metadata,
 225        HttpVerb parsedVerb,
 226        IOpenApiPathAttribute oaPath)
 227    {
 0228        var httpVerb = oaPath.HttpVerb ?? string.Empty;
 0229        if (!string.IsNullOrWhiteSpace(httpVerb))
 230        {
 0231            parsedVerb = HttpVerbExtensions.FromMethodString(httpVerb);
 0232            routeOptions.HttpVerbs.Add(parsedVerb);
 233        }
 234
 0235        var pattern = oaPath.Pattern;
 0236        if (string.IsNullOrWhiteSpace(pattern))
 237        {
 0238            throw new InvalidOperationException("OpenApiPath attribute must specify a non-empty Pattern property.");
 239        }
 0240        var openApiPattern = pattern;
 0241        var routePattern = pattern;
 0242        if (oaPath is OpenApiPathAttribute)
 243        {
 0244            if (!Rfc6570PathTemplateMapper.TryMapToKestrelRoute(pattern, out var mapping, out var error))
 245            {
 0246                throw new InvalidOperationException($"OpenApiPath pattern '{pattern}' is invalid: {error}");
 247            }
 248
 0249            openApiPattern = mapping.OpenApiPattern;
 0250            routePattern = mapping.KestrelPattern;
 0251            AddQueryParametersFromTemplate(metadata, mapping.QueryParameters);
 252        }
 253
 254        // Apply pattern, summary, description, tags
 0255        routeOptions.Pattern = routePattern;
 0256        metadata.Summary = ChooseFirstNonEmpty(oaPath.Summary, help.GetSynopsis());
 0257        metadata.Description = ChooseFirstNonEmpty(oaPath.Description, help.GetDescription());
 0258        metadata.Tags = [.. oaPath.Tags];
 259
 260        // Apply deprecated flag if specified
 0261        metadata.Deprecated |= oaPath.Deprecated;
 262        // Apply document ID if specified
 0263        metadata.DocumentId = oaPath.DocumentId;
 264        switch (oaPath)
 265        {
 266            case OpenApiPathAttribute oaPathConcrete:
 0267                ApplyPathLikePath(func, routeOptions, metadata, oaPathConcrete, openApiPattern);
 0268                break;
 269            case OpenApiWebhookAttribute oaWebhook:
 0270                ApplyPathLikeWebhook(func, metadata, oaWebhook, pattern);
 0271                break;
 272            case OpenApiCallbackAttribute oaCallback:
 0273                ApplyPathLikeCallback(func, metadata, oaCallback, httpVerb, pattern);
 274                break;
 275        }
 276
 0277        return parsedVerb;
 278    }
 279
 280    /// <summary>
 281    /// Applies the OpenApiPath attribute to the function's route options and metadata for a standard path.
 282    /// </summary>
 283    /// <param name="func">The function information.</param>
 284    /// <param name="routeOptions">The route options to configure.</param>
 285    /// <param name="metadata">The OpenAPI metadata to populate.</param>
 286    /// <param name="oaPath">The OpenApiPath attribute instance.</param>
 287    /// <param name="openApiPattern">The OpenAPI path pattern.</param>
 288    private static void ApplyPathLikePath(
 289        FunctionInfo func,
 290        MapRouteOptions routeOptions,
 291        OpenAPIPathMetadata metadata,
 292        OpenApiPathAttribute oaPath,
 293        string openApiPattern)
 294    {
 0295        metadata.Pattern = openApiPattern;
 0296        metadata.PathLikeKind = OpenApiPathLikeKind.Path;
 0297        if (!string.IsNullOrWhiteSpace(oaPath.CorsPolicy))
 298        {
 299            // Apply Cors policy name if specified
 0300            routeOptions.CorsPolicy = oaPath.CorsPolicy;
 301        }
 302
 0303        metadata.OperationId = oaPath.OperationId is null
 0304            ? func.Name
 0305            : string.IsNullOrWhiteSpace(oaPath.OperationId) ? metadata.OperationId : oaPath.OperationId;
 0306    }
 307
 308    /// <summary>
 309    /// Adds query parameters inferred from RFC6570 query expressions.
 310    /// </summary>
 311    /// <param name="metadata">The OpenAPI metadata to update.</param>
 312    /// <param name="queryParameterNames">The query parameter names to add.</param>
 313    private static void AddQueryParametersFromTemplate(
 314        OpenAPIPathMetadata metadata,
 315        IReadOnlyList<string> queryParameterNames)
 316    {
 0317        if (queryParameterNames.Count == 0)
 318        {
 0319            return;
 320        }
 321
 0322        metadata.Parameters ??= [];
 0323        foreach (var name in queryParameterNames.Distinct(StringComparer.OrdinalIgnoreCase))
 324        {
 0325            if (metadata.Parameters.Any(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase)))
 326            {
 327                continue;
 328            }
 329
 0330            metadata.Parameters.Add(new OpenApiParameter
 0331            {
 0332                Name = name,
 0333                In = ParameterLocation.Query,
 0334                Required = false,
 0335                Schema = new OpenApiSchema { Type = JsonSchemaType.String },
 0336            });
 337        }
 0338    }
 339    /// <summary>
 340    /// Applies the OpenApiWebhook attribute to the function's OpenAPI metadata.
 341    /// </summary>
 342    /// <param name="func">The function information.</param>
 343    /// <param name="metadata">The OpenAPI metadata to populate.</param>
 344    /// <param name="oaPath">The OpenApiWebhook attribute instance.</param>
 345    /// <param name="pattern">The route pattern.</param>
 346    private static void ApplyPathLikeWebhook(FunctionInfo func, OpenAPIPathMetadata metadata, OpenApiWebhookAttribute oa
 347    {
 0348        metadata.Pattern = pattern;
 0349        metadata.PathLikeKind = OpenApiPathLikeKind.Webhook;
 0350        metadata.OperationId = oaPath.OperationId is null
 0351            ? func.Name
 0352            : string.IsNullOrWhiteSpace(oaPath.OperationId) ? metadata.OperationId : oaPath.OperationId;
 0353    }
 354
 355    /// <summary>
 356    /// Applies the OpenApiCallback attribute to the function's OpenAPI metadata.
 357    /// </summary>
 358    /// <param name="func">The function information.</param>
 359    /// <param name="metadata">The OpenAPI metadata to populate.</param>
 360    /// <param name="oaCallback">The OpenApiCallback attribute instance.</param>
 361    /// <param name="httpVerb">The HTTP verb associated with the callback.</param>
 362    /// <param name="callbackPattern">The callback route pattern.</param>
 363    /// <exception cref="InvalidOperationException">Thrown when the Expression property of the OpenApiCallback attribute
 364    private static void ApplyPathLikeCallback(
 365        FunctionInfo func,
 366        OpenAPIPathMetadata metadata,
 367        OpenApiCallbackAttribute oaCallback,
 368        string httpVerb,
 369        string callbackPattern)
 370    {
 371        // Callbacks are neither paths nor webhooks
 0372        metadata.PathLikeKind = OpenApiPathLikeKind.Callback;
 0373        if (string.IsNullOrWhiteSpace(oaCallback.Expression))
 374        {
 0375            throw new InvalidOperationException("OpenApiCallback attribute must specify a non-empty Expression property.
 376        }
 377        // Callbacks must have an expression
 0378        metadata.Expression = CallbackOperationId.BuildCallbackKey(oaCallback.Expression, callbackPattern);
 0379        metadata.Inline = oaCallback.Inline;
 0380        metadata.Pattern = func.Name;
 0381        metadata.OperationId = string.IsNullOrWhiteSpace(oaCallback.OperationId)
 0382           ? CallbackOperationId.FromLastSegment(func.Name, httpVerb, oaCallback.Expression)
 0383           : oaCallback.OperationId;
 0384    }
 385
 386    /// <summary>
 387    /// Chooses the first non-empty string from the provided values, normalizing newlines.
 388    /// </summary>
 389    /// <param name="values">An array of string values to evaluate.</param>
 390    /// <returns>The first non-empty string with normalized newlines, or null if all are null or whitespace.</returns>
 391    private static string? ChooseFirstNonEmpty(params string?[] values)
 392    {
 0393        foreach (var value in values)
 394        {
 0395            if (!string.IsNullOrWhiteSpace(value))
 396            {
 0397                return NormalizeNewlines(value);
 398            }
 399        }
 400
 0401        return null;
 402    }
 403
 404    /// <summary>
 405    /// Normalizes newlines in the given string to use '\n' only.
 406    /// </summary>
 407    /// <param name="value">The string to normalize.</param>
 408    /// <returns>The normalized string.</returns>
 0409    private static string? NormalizeNewlines(string? value) => value?.Replace("\r\n", "\n");
 410
 411    /// <summary>
 412    /// Applies the OpenApiResponseRef attribute to the function's OpenAPI metadata.
 413    /// </summary>
 414    /// <param name="metadata">The OpenAPI metadata to update.</param>
 415    /// <param name="attribute">The OpenApiResponseRef attribute containing response reference details.</param>
 416    /// <param name="routeOptions">The route options to update.</param>
 417    private void ApplyResponseRefAttribute(OpenAPIPathMetadata metadata, OpenApiResponseRefAttribute attribute, MapRoute
 418    {
 0419        metadata.Responses ??= [];
 420
 0421        if (!TryGetResponseItem(attribute.ReferenceId, out var response, out var inline))
 422        {
 0423            throw new InvalidOperationException($"Response component with ID '{attribute.ReferenceId}' not found.");
 424        }
 425
 0426        IOpenApiResponse iResponse = attribute.Inline || inline ? response!.Clone() : new OpenApiResponseReference(attri
 427
 0428        if (attribute.Description is not null)
 429        {
 0430            iResponse.Description = attribute.Description;
 431        }
 432
 0433        if (metadata.Responses.ContainsKey(attribute.StatusCode))
 434        {
 0435            throw new InvalidOperationException($"Response for status code '{attribute.StatusCode}' is already defined f
 436        }
 437
 0438        metadata.Responses.Add(attribute.StatusCode, iResponse);
 0439        routeOptions.DefaultResponseContentType ??= new Dictionary<string, ICollection<ContentTypeWithSchema>>(StringCom
 440
 0441        if (response?.Content is not { Count: > 0 })
 442        {
 0443            routeOptions.DefaultResponseContentType[attribute.StatusCode] = [];
 0444            return;
 445        }
 446
 447        // Note: if the existing response is a reference, we still apply the new response details to it.
 448        // This allows attributes to override referenced responses without needing to define new references for each var
 449        //   if (CreateResponseFromAttribute(attribute, response))
 450        // {
 451        // SetDefaultResponseContentType(metadata.Responses, routeOptions, attribute.StatusCode);
 452        // Merge into existing dictionary instead of overwriting so we preserve host defaults
 453        // and can add multiple entries (e.g., 201 + 4XX).
 454
 455        // Materialize keys to avoid OpenAPI collections being mutated later.
 456        // Also capture the schema CLR type (best-effort) to enable runtime negotiation/serialization decisions.
 0457        routeOptions.DefaultResponseContentType[attribute.StatusCode] =
 0458        [
 0459            .. response.Content.Select(kvp =>
 0460                new ContentTypeWithSchema(kvp.Key, kvp.Value.Schema!.Id))
 0461        ];
 462        //}
 0463    }
 464
 465    /// <summary>
 466    /// Applies the OpenApiResponse attribute to the function's OpenAPI metadata.
 467    /// </summary>
 468    /// <param name="metadata">The OpenAPI metadata to update.</param>
 469    /// <param name="attribute">The OpenApiResponse attribute containing response details.</param>
 470    /// <param name="routeOptions">The route options to update.</param>
 471    private void ApplyResponseAttribute(OpenAPIPathMetadata metadata, IOpenApiResponseAttribute attribute, MapRouteOptio
 472    {
 1473        metadata.Responses ??= [];
 474        OpenApiResponse response;
 1475        if (!metadata.Responses.TryGetValue(attribute.StatusCode, out var existing))
 476        {
 0477            response = new OpenApiResponse();
 0478            metadata.Responses.Add(attribute.StatusCode, response);
 479        }
 1480        else if (existing is OpenApiResponse existingResponse)
 481        {
 0482            response = existingResponse;
 483        }
 484        else
 485        {
 1486            response = new OpenApiResponse();
 1487            metadata.Responses[attribute.StatusCode] = response;
 488        }
 489
 490        // Note: if the existing response is a reference, we still apply the new response details to it.
 491        // This allows attributes to override referenced responses without needing to define new references for each var
 1492        if (CreateResponseFromAttribute(attribute, response))
 493        {
 1494            var schema = (attribute is OpenApiResponseAttribute concreteAttr && concreteAttr.Schema is not null) ? concr
 1495            SetDefaultResponseContentType(metadata.Responses, routeOptions, attribute.StatusCode, schema);
 496        }
 1497    }
 498
 499    /// <summary>
 500    /// Updates <see cref="MapRouteOptions.DefaultResponseContentType"/> with the response content types for the provide
 501    /// This enables runtime content negotiation in <c>Write-KrResponse</c> for exact codes (e.g. <c>400</c>) and OpenAP
 502    /// </summary>
 503    /// <param name="responses">The collection of OpenAPI responses to check and update.</param>
 504    /// <param name="routeOptions">The route options to update.</param>
 505    /// <param name="newStatusCode">The status code of the new response that was just added.</param>
 506    /// <param name="schema">The schema type associated with the response content type.</param>
 507    private static void SetDefaultResponseContentType(OpenApiResponses responses, MapRouteOptions routeOptions, string n
 508    {
 1509        ArgumentNullException.ThrowIfNull(responses);
 1510        ArgumentNullException.ThrowIfNull(routeOptions);
 1511        if (string.IsNullOrWhiteSpace(newStatusCode))
 512        {
 0513            return;
 514        }
 515
 1516        if (!responses.TryGetValue(newStatusCode, out var newResponse))
 517        {
 0518            return;
 519        }
 520
 521        // Merge into existing dictionary instead of overwriting so we preserve host defaults
 522        // and can add multiple entries (e.g., 201 + 4XX).
 1523        routeOptions.DefaultResponseContentType ??= new Dictionary<string, ICollection<ContentTypeWithSchema>>(StringCom
 524
 1525        if (newResponse.Content is null || newResponse.Content.Count == 0)
 526        {
 0527            routeOptions.DefaultResponseContentType[newStatusCode] = [];
 0528            return;
 529        }
 530
 531        // Materialize keys to avoid OpenAPI collections being mutated later.
 1532        routeOptions.DefaultResponseContentType[newStatusCode] =
 1533        [
 1534            .. newResponse.Content.Select(kvp =>
 1535                new ContentTypeWithSchema(kvp.Key, schema))
 1536        ];
 1537    }
 538
 539    /// <summary>
 540    /// Attempts to infer a reasonable CLR <see cref="Type"/> from an OpenAPI schema.
 541    /// This is best-effort only; reference/complex schemas will typically map to <see cref="object"/>.
 542    /// </summary>
 543    /// <param name="schema">The OpenAPI schema.</param>
 544    /// <returns>The inferred CLR type, or null when unknown.</returns>
 545    private static Type? TryInferClrTypeFromSchema(IOpenApiSchema? schema)
 546    {
 0547        if (schema is null)
 548        {
 0549            return null;
 550        }
 551
 552        // References (components) can represent arbitrary shapes.
 0553        if (schema is OpenApiSchemaReference)
 554        {
 0555            return typeof(object);
 556        }
 557
 0558        if (schema is not OpenApiSchema s)
 559        {
 0560            return typeof(object);
 561        }
 562
 563        // Ignore nullability bit; we only capture the underlying CLR type.
 0564        var type = (s.Type ?? JsonSchemaType.Object) & ~JsonSchemaType.Null;
 565
 0566        if ((type & JsonSchemaType.Array) != 0)
 567        {
 0568            var itemType = TryInferClrTypeFromSchema(s.Items);
 0569            return itemType is null ? typeof(object[]) : itemType.MakeArrayType();
 570        }
 571
 0572        return InferNonArrayClrType(s, type);
 573    }
 574
 575    /// <summary>
 576    /// Infers a CLR type for non-array OpenAPI schema kinds.
 577    /// </summary>
 578    /// <param name="schema">The concrete OpenAPI schema.</param>
 579    /// <param name="type">The normalized OpenAPI schema type flags.</param>
 580    /// <returns>The inferred CLR type.</returns>
 581    private static Type InferNonArrayClrType(OpenApiSchema schema, JsonSchemaType type)
 582    {
 583        // For objects, we have no better information to go on, so we default to object.
 0584        if ((type & JsonSchemaType.Object) != 0)
 585        {
 0586            return typeof(object);
 587        }
 588        // For booleans, we can directly map to bool.
 0589        if ((type & JsonSchemaType.Boolean) != 0)
 590        {
 0591            return typeof(bool);
 592        }
 593        // For integers, we attempt to infer more specific types based on the format, but if the format is unrecognized,
 0594        if ((type & JsonSchemaType.Integer) != 0)
 595        {
 0596            return InferIntegerClrType(schema.Format);
 597        }
 598        // For numbers, we attempt to infer more specific types based on the format, but if the format is unrecognized, 
 0599        if ((type & JsonSchemaType.Number) != 0)
 600        {
 0601            return InferNumberClrType(schema.Format);
 602        }
 603        // For strings, we can attempt to infer more specific types based on the format, but if the format is unrecogniz
 0604        if ((type & JsonSchemaType.String) != 0)
 605        {
 0606            return InferStringClrType(schema.Format);
 607        }
 608        // If we have no type or an unrecognized type, default to object. This is a best-effort inference and may not be
 0609        return (type & JsonSchemaType.Null) != 0 ? typeof(void) : typeof(object);
 610    }
 611
 612    /// <summary>
 613    /// Infers the CLR integer type from an OpenAPI integer format.
 614    /// </summary>
 615    /// <param name="format">The OpenAPI schema format value.</param>
 616    /// <returns>The inferred CLR integer type.</returns>
 617    private static Type InferIntegerClrType(string? format)
 0618        => string.Equals(format, "int64", StringComparison.OrdinalIgnoreCase) ? typeof(long) : typeof(int);
 619
 620    /// <summary>
 621    /// Infers the CLR numeric type from an OpenAPI number format.
 622    /// </summary>
 623    /// <param name="format">The OpenAPI schema format value.</param>
 624    /// <returns>The inferred CLR number type.</returns>
 625    private static Type InferNumberClrType(string? format)
 0626        => string.Equals(format, "float", StringComparison.OrdinalIgnoreCase) ? typeof(float) : typeof(double);
 627
 628    /// <summary>
 629    /// Infers the CLR string-like type from an OpenAPI string format.
 630    /// </summary>
 631    /// <param name="format">The OpenAPI schema format value.</param>
 632    /// <returns>The inferred CLR type for the string format.</returns>
 633    private static Type InferStringClrType(string? format)
 0634        => format?.ToLowerInvariant() switch
 0635        {
 0636            "binary" => typeof(byte[]),
 0637            "uuid" => typeof(Guid),
 0638            "uri" => typeof(Uri),
 0639            "duration" => typeof(TimeSpan),
 0640            "date-time" => typeof(DateTimeOffset),
 0641            _ => typeof(string)
 0642        };
 643
 644    /// <summary>
 645    /// Selects the default success response (2xx) from the given OpenApiResponses.
 646    /// </summary>
 647    /// <param name="responses">The collection of OpenApiResponses to select from.</param>
 648    /// <returns>The status code of the default success response, or null if none found.</returns>
 649    private static string? SelectDefaultSuccessResponse(OpenApiResponses responses)
 650    {
 0651        return responses
 0652            .Select(kvp => new
 0653            {
 0654                StatusCode = kvp.Key,
 0655                Code = TryParseStatusCode(kvp.Key),
 0656                Response = kvp.Value
 0657            })
 0658            .Where(x =>
 0659                x.Code is >= 200 and < 300 &&
 0660                HasContent(x.Response))
 0661            .OrderBy(x => x.Code)
 0662            .Select(x => x.StatusCode)
 0663            .FirstOrDefault();
 664    }
 665
 666    /// <summary>
 667    /// Tries to parse the given status code string into an integer.
 668    /// </summary>
 669    /// <param name="statusCode">The status code as a string.</param>
 670    /// <returns>The parsed integer status code, or -1 if parsing fails.</returns>
 671    private static int TryParseStatusCode(string statusCode)
 0672        => int.TryParse(statusCode, out var code) ? code : -1;
 673
 674    /// <summary>
 675    /// Determines if the given response has content defined.
 676    /// </summary>
 677    /// <param name="response">The OpenAPI response to check for content.</param>
 678    /// <returns>True if the response has content; otherwise, false.</returns>
 679    private static bool HasContent(IOpenApiResponse response)
 680    {
 681        // If your concrete type is OpenApiResponse (common), this is the easiest path:
 0682        if (response is OpenApiResponse r)
 683        {
 0684            return r.Content is not null && r.Content.Count > 0;
 685        }
 686
 687        // Otherwise, we can't reliably know. Be conservative:
 0688        return false;
 689    }
 690
 691    /// <summary>
 692    /// Applies the OpenApiProperty attribute to the function's OpenAPI metadata.
 693    /// </summary>
 694    /// <param name="metadata">The OpenAPI metadata to update.</param>
 695    /// <param name="attribute">The OpenApiProperty attribute containing property details.</param>
 696    /// <exception cref="InvalidOperationException"></exception>
 697    private void ApplyPropertyAttribute(OpenAPIPathMetadata metadata, OpenApiPropertyAttribute attribute)
 698    {
 0699        if (attribute.StatusCode is null)
 700        {
 0701            return;
 702        }
 703
 0704        if (metadata.Responses is null || !metadata.Responses.TryGetValue(attribute.StatusCode, out var res))
 705        {
 0706            throw new InvalidOperationException($"Response for status code '{attribute.StatusCode}' is not defined for t
 707        }
 708        // Note: if the existing response is a reference, we still apply the new response details to it. This allows att
 0709        if (res is OpenApiResponseReference)
 710        {
 0711            throw new InvalidOperationException($"Cannot apply OpenApiPropertyAttribute to response '{attribute.StatusCo
 712        }
 713        // We have to be able to modify the response content to apply the property attribute, so we require a concrete O
 0714        if (res is OpenApiResponse response)
 715        {
 0716            if (response.Content is null || response.Content.Count == 0)
 717            {
 0718                throw new InvalidOperationException($"Cannot apply OpenApiPropertyAttribute to response '{attribute.Stat
 719            }
 720
 0721            foreach (var content in response.Content.Values)
 722            {
 0723                if (content.Schema is null)
 724                {
 0725                    throw new InvalidOperationException($"Cannot apply OpenApiPropertyAttribute to response '{attribute.
 726                }
 727
 0728                ApplySchemaAttr(attribute, content.Schema);
 729            }
 730        }
 0731    }
 732
 733    private void ApplyAuthorizationAttribute(MapRouteOptions routeOptions, OpenAPIPathMetadata metadata, OpenApiAuthoriz
 734    {
 0735        metadata.SecuritySchemes ??= [];
 0736        var policyList = BuildPolicyList(attribute.Policies);
 0737        var securitySchemeList = Host.AddSecurityRequirementObject(attribute.Scheme, policyList, metadata.SecurityScheme
 0738        routeOptions.AddSecurityRequirementObject(schemes: securitySchemeList, policies: policyList);
 0739    }
 740
 741    private static List<string> BuildPolicyList(string? policies)
 742    {
 0743        return [.. (string.IsNullOrWhiteSpace(policies) ? new List<string>() : [.. policies.Split(',')])
 0744            .Where(p => !string.IsNullOrWhiteSpace(p))
 0745            .Select(p => p.Trim())];
 746    }
 747
 748    /// <summary>
 749    /// Processes the parameters of the specified function, applying OpenAPI annotations as needed.
 750    /// </summary>
 751    /// <param name="func">The function information.</param>
 752    /// <param name="help">The comment help information.</param>
 753    /// <param name="routeOptions">The route options to update.</param>
 754    /// <param name="openApiMetadata">The OpenAPI metadata to update.</param>
 755    /// <exception cref="InvalidOperationException">Thrown when an invalid operation occurs during parameter processing.
 756    private void ProcessParameters(
 757        FunctionInfo func,
 758        CommentHelpInfo help,
 759        MapRouteOptions routeOptions,
 760        OpenAPIPathMetadata openApiMetadata)
 761    {
 0762        foreach (var paramInfo in func.Parameters.Values)
 763        {
 764            // First pass for parameter and request body attributes
 0765            foreach (var attribute in paramInfo.Attributes)
 766            {
 767                switch (attribute)
 768                {
 769                    case OpenApiParameterAttribute paramAttr:
 0770                        ApplyParameterAttribute(func, help, routeOptions, openApiMetadata, paramInfo, paramAttr);
 0771                        break;
 772                    case OpenApiParameterRefAttribute paramRefAttr:
 0773                        ApplyParameterRefAttribute(help, routeOptions, openApiMetadata, paramInfo, paramRefAttr);
 0774                        break;
 775                    case OpenApiRequestBodyRefAttribute requestBodyRefAttr:
 0776                        ApplyRequestBodyRefAttribute(help, routeOptions, openApiMetadata, paramInfo, requestBodyRefAttr)
 0777                        break;
 778                    case OpenApiRequestBodyAttribute requestBodyAttr:
 0779                        ApplyRequestBodyAttribute(help, routeOptions, openApiMetadata, paramInfo, requestBodyAttr);
 0780                        break;
 781                    case OpenApiRequestBodyExampleRefAttribute:
 782                    case OpenApiParameterExampleRefAttribute:
 783                        // Do nothing here; handled later
 784                        break;
 785                    case KestrunAnnotation ka:
 0786                        throw new InvalidOperationException($"Unhandled Kestrun annotation: {ka.GetType().Name}");
 787                }
 788            }
 789            // Second pass for example references
 0790            foreach (var attribute in paramInfo.Attributes)
 791            {
 792                switch (attribute)
 793                {
 794                    case OpenApiParameterAttribute:
 795                    case OpenApiParameterRefAttribute:
 796                    case OpenApiRequestBodyRefAttribute:
 797                    case OpenApiRequestBodyAttribute:
 798                        // Already handled
 799                        break;
 800                    case OpenApiRequestBodyExampleRefAttribute requestBodyExampleRefAttr:
 0801                        ApplyRequestBodyExampleRefAttribute(openApiMetadata, requestBodyExampleRefAttr);
 0802                        break;
 803                    case OpenApiParameterExampleRefAttribute parameterExampleRefAttr:
 0804                        ApplyParameterExampleRefAttribute(openApiMetadata, paramInfo, parameterExampleRefAttr);
 0805                        break;
 806                    case KestrunAnnotation ka:
 0807                        throw new InvalidOperationException($"Unhandled Kestrun annotation: {ka.GetType().Name}");
 808                }
 809            }
 810        }
 0811    }
 812
 813    #region Parameter Handlers
 814    /// <summary>
 815    /// Applies the OpenApiParameter attribute to the function's OpenAPI metadata.
 816    /// </summary>
 817    /// <param name="func">The function information.</param>
 818    /// <param name="help">The comment help information.</param>
 819    /// <param name="routeOptions">The route options to update.</param>
 820    /// <param name="metadata">The OpenAPI metadata to update.</param>
 821    /// <param name="paramInfo">The parameter information.</param>
 822    /// <param name="attribute">The OpenApiParameter attribute containing parameter details.</param>
 823    private void ApplyParameterAttribute(
 824        FunctionInfo func,
 825        CommentHelpInfo help,
 826        MapRouteOptions routeOptions,
 827        OpenAPIPathMetadata metadata,
 828        ParameterMetadata paramInfo,
 829        OpenApiParameterAttribute attribute)
 830    {
 0831        metadata.Parameters ??= [];
 0832        var parameter = new OpenApiParameter();
 0833        if (!CreateParameterFromAttribute(attribute, parameter))
 834        {
 0835            Host.Logger.Error("Error processing OpenApiParameter attribute on parameter '{paramName}' of function '{func
 0836            return;
 837        }
 838
 0839        if (!string.IsNullOrEmpty(parameter.Name) && parameter.Name != paramInfo.Name)
 840        {
 0841            throw new InvalidOperationException($"Parameter name {parameter.Name} is different from variable name: '{par
 842        }
 843
 0844        parameter.Name = paramInfo.Name;
 0845        parameter.Schema = InferPrimitiveSchema(paramInfo.ParameterType);
 846
 0847        if (parameter.Schema is OpenApiSchema schema)
 848        {
 0849            var defaultValue = func.GetDefaultParameterValue(paramInfo.Name);
 0850            if (defaultValue is not null)
 851            {
 0852                schema.Default = OpenApiJsonNodeFactory.ToNode(defaultValue);
 853            }
 854        }
 855
 0856        parameter.Description ??= help.GetParameterDescription(paramInfo.Name);
 857
 0858        foreach (var attr in paramInfo.Attributes.OfType<CmdletMetadataAttribute>())
 859        {
 0860            PowerShellAttributes.ApplyPowerShellAttribute(attr, (OpenApiSchema)parameter.Schema);
 861        }
 862
 0863        RemoveExistingParameter(metadata, paramInfo.Name);
 0864        metadata.Parameters.Add(parameter);
 0865        routeOptions.ScriptCode.Parameters.Add(new ParameterForInjectionInfo(paramInfo, parameter));
 0866    }
 867
 868    /// <summary>
 869    /// Applies the OpenApiParameterRef attribute to the function's OpenAPI metadata.
 870    /// </summary>
 871    /// <param name="help">The comment help information.</param>
 872    /// <param name="routeOptions">The route options to update.</param>
 873    /// <param name="metadata">The OpenAPI metadata to update.</param>
 874    /// <param name="paramInfo">The parameter information.</param>
 875    /// <param name="attribute">The OpenApiParameterRef attribute containing parameter reference details.</param>
 876    /// <exception cref="InvalidOperationException">If the parameter name does not match the reference name when inlinin
 877    private void ApplyParameterRefAttribute(
 878        CommentHelpInfo help,
 879        MapRouteOptions routeOptions,
 880        OpenAPIPathMetadata metadata,
 881        ParameterMetadata paramInfo,
 882        OpenApiParameterRefAttribute attribute)
 883    {
 0884        metadata.Parameters ??= [];
 0885        routeOptions.ScriptCode.Parameters ??= [];
 886
 0887        if (!TryGetParameterItem(attribute.ReferenceId, out var componentParameter, out var isInline) ||
 0888             componentParameter is null)
 889        {
 0890            throw new InvalidOperationException($"Parameter component with ID '{attribute.ReferenceId}' not found.");
 891        }
 892        IOpenApiParameter parameter;
 893
 0894        if (attribute.Inline || isInline)
 895        {
 0896            parameter = componentParameter.Clone();
 0897            if (componentParameter.Name != paramInfo.Name)
 898            {
 0899                throw new InvalidOperationException($"Parameter name {componentParameter.Name} is different from variabl
 900            }
 901
 0902            parameter.Description ??= help.GetParameterDescription(paramInfo.Name);
 903        }
 904        else
 905        {
 0906            parameter = new OpenApiParameterReference(attribute.ReferenceId);
 907        }
 908
 0909        routeOptions.ScriptCode.Parameters.Add(new ParameterForInjectionInfo(paramInfo, componentParameter));
 0910        RemoveExistingParameter(metadata, paramInfo.Name);
 0911        metadata.Parameters.Add(parameter);
 0912    }
 913
 914    private void ApplyParameterExampleRefAttribute(
 915       OpenAPIPathMetadata metadata,
 916       ParameterMetadata paramInfo,
 917       OpenApiParameterExampleRefAttribute attribute)
 918    {
 0919        var parameters = metadata.Parameters
 0920        ?? throw new InvalidOperationException(
 0921            "OpenApiParameterExampleRefAttribute must follow OpenApiParameterAttribute or OpenApiParameterRefAttribute."
 922
 0923        var parameter = parameters.FirstOrDefault(p => p.Name == paramInfo.Name)
 0924        ?? throw new InvalidOperationException(
 0925            $"OpenApiParameterExampleRefAttribute requires the parameter '{paramInfo.Name}' to be defined.");
 0926        if (parameter is OpenApiParameterReference)
 927        {
 0928            throw new InvalidOperationException(
 0929                "Cannot apply OpenApiParameterExampleRefAttribute to a parameter reference.");
 930        }
 0931        if (parameter is OpenApiParameter opp)
 932        {
 0933            opp.Examples ??= new Dictionary<string, IOpenApiExample>();
 934            // Clone or reference the example
 0935            _ = TryAddExample(opp.Examples, attribute);
 936        }
 0937    }
 938
 939    /// <summary>
 940    /// Removes any existing parameter with the specified name from the OpenAPI metadata.
 941    /// </summary>
 942    /// <param name="metadata">The OpenAPI metadata.</param>
 943    /// <param name="name">The parameter name to remove.</param>
 944    private static void RemoveExistingParameter(OpenAPIPathMetadata metadata, string name)
 945    {
 0946        if (metadata.Parameters is null)
 947        {
 0948            return;
 949        }
 950
 0951        for (var i = metadata.Parameters.Count - 1; i >= 0; i--)
 952        {
 0953            if (string.Equals(metadata.Parameters[i].Name, name, StringComparison.OrdinalIgnoreCase))
 954            {
 0955                metadata.Parameters.RemoveAt(i);
 956            }
 957        }
 0958    }
 959
 960    #endregion
 961    #region Request Body Handlers
 962    /// <summary>
 963    /// Applies the OpenApiRequestBodyRef attribute to the function's OpenAPI metadata.
 964    /// </summary>
 965    /// <param name="help">The comment help information.</param>
 966    /// <param name="routeOptions">The route options to update.</param>
 967    /// <param name="metadata">The OpenAPI metadata to update.</param>
 968    /// <param name="paramInfo">The parameter information.</param>
 969    /// <param name="attribute">The OpenApiRequestBodyRef attribute containing request body reference details.</param>
 970    private void ApplyRequestBodyRefAttribute(
 971        CommentHelpInfo help,
 972        MapRouteOptions routeOptions,
 973        OpenAPIPathMetadata metadata,
 974        ParameterMetadata paramInfo,
 975        OpenApiRequestBodyRefAttribute attribute)
 976    {
 0977        var referenceId = ResolveRequestBodyReferenceId(attribute, paramInfo);
 0978        var componentRequestBody = GetRequestBody(referenceId);
 979
 0980        metadata.RequestBody = attribute.Inline ? componentRequestBody.Clone() : new OpenApiRequestBodyReference(referen
 0981        metadata.RequestBody.Description = attribute.Description ?? help.GetParameterDescription(paramInfo.Name);
 982
 0983        routeOptions.ScriptCode.Parameters.Add(new ParameterForInjectionInfo(paramInfo, componentRequestBody, routeOptio
 0984    }
 985
 986    /// <summary>
 987    /// Resolves the reference ID for the OpenApiRequestBodyRef attribute.
 988    /// </summary>
 989    /// <param name="attribute">The OpenApiRequestBodyRef attribute.</param>
 990    /// <param name="paramInfo">The parameter metadata.</param>
 991    /// <returns>The resolved reference ID.</returns>
 992    /// <exception cref="InvalidOperationException">
 993    /// Thrown when the ReferenceId is not specified and the parameter type is 'object',
 994    /// or when the ReferenceId does not match the parameter type name.
 995    /// </exception>
 996    private string ResolveRequestBodyReferenceId(OpenApiRequestBodyRefAttribute attribute, ParameterMetadata paramInfo)
 997    {
 0998        if (string.IsNullOrWhiteSpace(attribute.ReferenceId))
 999        {
 01000            if (paramInfo.ParameterType.Name is "Object" or null)
 1001            {
 01002                throw new InvalidOperationException("OpenApiRequestBodyRefAttribute must have a ReferenceId specified wh
 1003            }
 1004
 01005            attribute.ReferenceId = paramInfo.ParameterType.Name;
 1006        }
 01007        else if (paramInfo.ParameterType.Name != "Object" && attribute.ReferenceId != paramInfo.ParameterType.Name)
 1008        {
 01009            return FindReferenceIdForParameter(attribute.ReferenceId, paramInfo);
 1010        }
 1011        // Return the reference ID as is
 01012        return attribute.ReferenceId;
 1013    }
 1014
 1015    /// <summary>
 1016    /// Finds and validates the reference ID for a request body parameter.
 1017    /// </summary>
 1018    /// <param name="referenceId">The reference ID to validate.</param>
 1019    /// <param name="paramInfo">The parameter metadata.</param>
 1020    /// <returns>The validated reference ID.</returns>
 1021    /// <exception cref="InvalidOperationException">Thrown when the reference ID does not match the parameter type name.
 1022    private string FindReferenceIdForParameter(string referenceId, ParameterMetadata paramInfo)
 1023    {
 1024        // Ensure the reference ID exists and has a schema
 01025        if (!TryGetFirstRequestBodySchema(referenceId, out var schema))
 1026        {
 01027            throw new InvalidOperationException(
 01028                $"Request body component with ReferenceId '{referenceId}' was not found or does not define a schema.");
 1029        }
 1030        // Validate that the schema matches the parameter type
 01031        if (!IsRequestBodySchemaMatchForParameter(schema, paramInfo.ParameterType))
 1032        {
 01033            throw new InvalidOperationException(
 01034                $"Schema for request body component '{referenceId}' does not match parameter type '{paramInfo.ParameterT
 1035        }
 1036        // return the validated reference ID
 01037        return referenceId;
 1038    }
 1039
 1040    /// <summary>
 1041    /// Attempts to retrieve the first schema defined on a request body component.
 1042    /// </summary>
 1043    /// <param name="referenceId">The request body component reference ID.</param>
 1044    /// <param name="schema">The extracted schema when available.</param>
 1045    /// <returns><see langword="true"/> if a non-null schema is found; otherwise <see langword="false"/>.</returns>
 1046    private bool TryGetFirstRequestBodySchema(string referenceId, out IOpenApiSchema schema)
 1047    {
 01048        schema = null!;
 1049
 01050        if (!TryGetRequestBodyItem(referenceId, out var requestBody, out _))
 1051        {
 01052            return false;
 1053        }
 1054
 01055        if (requestBody?.Content is null || requestBody.Content.Count == 0)
 1056        {
 01057            return false;
 1058        }
 1059
 01060        schema = requestBody.Content.First().Value.Schema!;
 01061        return schema is not null;
 1062    }
 1063
 1064    /// <summary>
 1065    /// Determines whether a request-body schema matches a given CLR parameter type.
 1066    /// </summary>
 1067    /// <param name="schema">The schema declared for the request body.</param>
 1068    /// <param name="parameterType">The CLR parameter type being validated.</param>
 1069    /// <returns><see langword="true"/> if the schema matches the parameter type; otherwise <see langword="false"/>.</re
 1070    private static bool IsRequestBodySchemaMatchForParameter(IOpenApiSchema schema, Type parameterType)
 1071    {
 01072        if (schema is OpenApiSchemaReference schemaRef)
 1073        {
 01074            return schemaRef.Reference.Id == parameterType.Name;
 1075        }
 1076
 01077        if (schema is OpenApiSchema inlineSchema && PrimitiveSchemaMap.TryGetValue(parameterType, out var valueType))
 1078        {
 01079            var expected = valueType();
 01080            return inlineSchema.Format == expected.Format && inlineSchema.Type == expected.Type;
 1081        }
 1082
 01083        return false;
 1084    }
 1085
 1086    /// <summary>
 1087    /// Applies the OpenApiRequestBody attribute to the function's OpenAPI metadata.
 1088    /// </summary>
 1089    /// <param name="help">The comment help information.</param>
 1090    /// <param name="routeOptions">The route options to update.</param>
 1091    /// <param name="metadata">The OpenAPI metadata to update.</param>
 1092    /// <param name="paramInfo">The parameter information.</param>
 1093    /// <param name="attribute">The OpenApiRequestBody attribute containing request body details.</param>
 1094    private void ApplyRequestBodyAttribute(
 1095        CommentHelpInfo help,
 1096        MapRouteOptions routeOptions,
 1097        OpenAPIPathMetadata metadata,
 1098        ParameterMetadata paramInfo,
 1099        OpenApiRequestBodyAttribute attribute)
 1100    {
 01101        ResolveFormOptions(routeOptions, paramInfo);
 1102
 1103        // Special handling for form payloads
 01104        if (routeOptions.FormOptions is not null)
 1105        // && (paramInfo.ParameterType == typeof(KrFormData) || paramInfo.ParameterType == typeof(KrFormPayload)))
 1106        {
 01107            ApplyFormRequestBody(help, routeOptions, metadata, paramInfo, attribute);
 01108            return;
 1109        }
 1110        // Check for preferred request body in components
 01111        var requestBodyPreferred = ComponentRequestBodiesExists(paramInfo.ParameterType.Name);
 01112        if (requestBodyPreferred)
 1113        {
 01114            ApplyPreferredRequestBody(help, routeOptions, metadata, paramInfo, attribute);
 01115            return;
 1116        }
 1117
 01118        var requestBody = new OpenApiRequestBody();
 01119        var schema = InferPrimitiveSchema(type: paramInfo.ParameterType, inline: attribute.Inline);
 1120
 01121        if (!CreateRequestBodyFromAttribute(attribute, requestBody, schema))
 1122        {
 01123            return;
 1124        }
 1125
 01126        metadata.RequestBody = requestBody;
 01127        metadata.RequestBody.Description ??= help.GetParameterDescription(paramInfo.Name);
 1128
 01129        if (routeOptions.FormOptions is not null && requestBody.Content?.Keys.Count > 0)
 1130        {
 01131            routeOptions.FormOptions.AllowedContentTypes.Clear();
 01132            routeOptions.FormOptions.AllowedContentTypes.AddRange(requestBody.Content?.Keys ?? []);
 1133        }
 1134        // If the request body defines content types, use them as allowed content types for the route and form options
 01135        routeOptions.AllowedRequestContentTypes.Clear();
 01136        routeOptions.AllowedRequestContentTypes.AddRange(requestBody.Content?.Keys ?? []);
 1137
 1138        // Add the parameter for injection
 01139        routeOptions.ScriptCode.Parameters.Add(new ParameterForInjectionInfo(paramInfo, requestBody, routeOptions.FormOp
 01140    }
 1141
 1142    /// <summary>
 1143    /// Resolves form options for the request body parameter when form binding is configured.
 1144    /// </summary>
 1145    /// <param name="routeOptions">The route options to update.</param>
 1146    /// <param name="paramInfo">The parameter metadata.</param>
 1147    private void ResolveFormOptions(MapRouteOptions routeOptions, ParameterMetadata paramInfo)
 1148    {
 01149        if (routeOptions.FormOptions is not null)
 1150        {
 01151            return;
 1152        }
 1153
 01154        if (Host.Runtime.FormOptions.TryGetValue(paramInfo.ParameterType.Name, out var formOptionsValue))
 1155        {
 01156            routeOptions.FormOptions = new KrFormOptions(formOptionsValue);
 01157            return;
 1158        }
 1159
 01160        var formAttr = paramInfo.ParameterType.GetCustomAttribute<KrBindFormAttribute>(inherit: true);
 01161        if (formAttr is null)
 1162        {
 01163            return;
 1164        }
 1165
 01166        var formOptions = FormHelper.ApplyKrPartAttributes(formAttr);
 01167        formOptions.Name = paramInfo.ParameterType.FullName ?? paramInfo.ParameterType.Name;
 1168
 01169        var rules = FormHelper.BuildFormPartRulesFromType(paramInfo.ParameterType);
 01170        AddFormPartRules(formOptions, rules);
 1171
 01172        routeOptions.FormOptions = formOptions;
 01173    }
 1174
 1175    /// <summary>
 1176    /// Applies request body metadata for form payload parameters.
 1177    /// </summary>
 1178    /// <param name="help">The comment help information.</param>
 1179    /// <param name="routeOptions">The route options to update.</param>
 1180    /// <param name="metadata">The OpenAPI metadata to update.</param>
 1181    /// <param name="paramInfo">The parameter information.</param>
 1182    /// <param name="attribute">The OpenApiRequestBody attribute containing request body details.</param>
 1183    private void ApplyFormRequestBody(
 1184        CommentHelpInfo help,
 1185        MapRouteOptions routeOptions,
 1186        OpenAPIPathMetadata metadata,
 1187        ParameterMetadata paramInfo,
 1188        OpenApiRequestBodyAttribute attribute)
 1189    {
 01190        var contentTypes = ResolveFormContentTypes(attribute, routeOptions.FormOptions!);
 01191        routeOptions.FormOptions!.AllowedContentTypes.Clear();
 01192        routeOptions.FormOptions.AllowedContentTypes.AddRange(contentTypes);
 1193
 01194        var formSchema = InferPrimitiveSchema(type: paramInfo.ParameterType, inline: attribute.Inline);
 01195        var requestBodyContent = BuildFormRequestBodyWithSchema(formSchema, contentTypes, routeOptions.FormOptions, attr
 01196        metadata.RequestBody = requestBodyContent;
 01197        metadata.RequestBody.Description ??= help.GetParameterDescription(paramInfo.Name);
 1198        // Add the parameter for injection
 01199        routeOptions.ScriptCode.Parameters.Add(new ParameterForInjectionInfo(paramInfo, requestBodyContent, routeOptions
 01200    }
 1201
 1202    /// <summary>
 1203    /// Applies the OpenApiRequestBodyExampleRef attribute to the function's OpenAPI metadata.
 1204    /// </summary>
 1205    /// <param name="metadata">The OpenAPI metadata to update.</param>
 1206    /// <param name="attribute">The OpenApiRequestBodyExampleRef attribute containing example reference details.</param>
 1207    /// <exception cref="InvalidOperationException">Thrown when the request body or its content is not properly defined.
 1208    private void ApplyRequestBodyExampleRefAttribute(
 1209       OpenAPIPathMetadata metadata,
 1210       OpenApiRequestBodyExampleRefAttribute attribute)
 1211    {
 01212        var requestBody = metadata.RequestBody
 01213        ?? throw new InvalidOperationException(
 01214            "OpenApiRequestBodyExampleRefAttribute must follow OpenApiRequestBodyAttribute or OpenApiRequestBodyRefAttri
 1215
 01216        if (requestBody.Content is null)
 1217        {
 01218            throw new InvalidOperationException(
 01219                "OpenApiRequestBodyExampleRefAttribute requires the request body to have content defined.");
 1220        }
 1221
 01222        foreach (var oamt in requestBody.Content.Values.OfType<OpenApiMediaType>())
 1223        {
 01224            oamt.Examples ??= new Dictionary<string, IOpenApiExample>();
 01225            _ = TryAddExample(oamt.Examples, attribute);
 1226        }
 01227    }
 1228
 1229    private static OpenApiRequestBody BuildFormRequestBodyWithSchema(
 1230        IOpenApiSchema schema,
 1231        string[] contentTypes,
 1232        KrFormOptions options,
 1233        OpenApiRequestBodyAttribute attribute)
 1234    {
 01235        var requestBody = new OpenApiRequestBody
 01236        {
 01237            Description = attribute.Description,
 01238            Required = attribute.Required,
 01239            Content = new Dictionary<string, IOpenApiMediaType>(StringComparer.OrdinalIgnoreCase)
 01240        };
 1241
 01242        var encoding = BuildMultipartEncoding(options);
 1243
 01244        foreach (var contentType in contentTypes)
 1245        {
 01246            if (string.IsNullOrWhiteSpace(contentType))
 1247            {
 1248                continue;
 1249            }
 1250
 01251            var mediaType = new OpenApiMediaType
 01252            {
 01253                Schema = schema
 01254            };
 1255
 01256            if (IsMultipartContentType(contentType) && encoding is not null)
 1257            {
 01258                mediaType.Encoding = encoding;
 1259            }
 1260
 01261            requestBody.Content[contentType] = mediaType;
 1262        }
 1263
 01264        return requestBody;
 1265    }
 1266
 1267    /// <summary>
 1268    /// Resolves the content types for a form request body based on the attribute and form options.
 1269    /// </summary>
 1270    /// <param name="attribute">The OpenApiRequestBodyAttribute containing request body details.</param>
 1271    /// <param name="options">The KrFormOptions specifying allowed content types.</param>
 1272    /// <returns>An array of content type strings to be used for the form request body.</returns>
 1273    private static string[] ResolveFormContentTypes(OpenApiRequestBodyAttribute attribute, KrFormOptions options)
 1274    {
 1275        // if content type is specified on the attribute, use that
 01276        if (attribute.ContentType is { Length: > 0 })
 1277        {
 01278            return attribute.ContentType;
 1279        }
 1280
 1281        // if allowed content types are specified, use those
 01282        if (options.AllowedContentTypes.Count > 0)
 1283        {
 01284            return [.. options.AllowedContentTypes];
 1285        }
 1286        // default to multipart/form-data
 01287        return ["multipart/form-data"];
 1288    }
 1289
 1290    private static bool IsMultipartContentType(string contentType)
 01291        => contentType.StartsWith("multipart/", StringComparison.OrdinalIgnoreCase);
 1292
 1293    private static Dictionary<string, OpenApiEncoding>? BuildMultipartEncoding(KrFormOptions options)
 1294    {
 01295        var encoding = new Dictionary<string, OpenApiEncoding>(StringComparer.Ordinal);
 1296
 01297        foreach (var rule in options.Rules)
 1298        {
 01299            if (string.IsNullOrWhiteSpace(rule.Name))
 1300            {
 1301                continue;
 1302            }
 1303
 01304            if (!IsProbablyFileRule(rule))
 1305            {
 1306                continue;
 1307            }
 1308
 01309            if (rule.AllowedContentTypes.Count == 0)
 1310            {
 1311                continue;
 1312            }
 1313
 01314            encoding[rule.Name] = new OpenApiEncoding
 01315            {
 01316                ContentType = string.Join(", ", rule.AllowedContentTypes)
 01317            };
 1318        }
 1319
 01320        return encoding.Count > 0 ? encoding : null;
 1321    }
 1322
 1323    private static bool IsProbablyFileRule(KrFormPartRule rule)
 1324    {
 01325        if (rule.StoreToDisk)
 1326        {
 01327            return true;
 1328        }
 1329
 01330        if (rule.AllowedExtensions.Count > 0)
 1331        {
 01332            return true;
 1333        }
 1334
 01335        foreach (var ct in rule.AllowedContentTypes)
 1336        {
 01337            if (string.IsNullOrWhiteSpace(ct))
 1338            {
 1339                continue;
 1340            }
 1341
 01342            if (!ct.StartsWith("text/", StringComparison.OrdinalIgnoreCase)
 01343                && !ct.Equals("application/json", StringComparison.OrdinalIgnoreCase)
 01344                && !ct.Equals("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase))
 1345            {
 01346                return true;
 1347            }
 1348        }
 1349
 01350        return false;
 01351    }
 1352
 1353    /// <summary>
 1354    /// Applies the preferred request body from components to the function's OpenAPI metadata.
 1355    /// </summary>
 1356    /// <param name="help">The comment help information.</param>
 1357    /// <param name="routeOptions">The route options to update.</param>
 1358    /// <param name="metadata">The OpenAPI metadata to update.</param>
 1359    /// <param name="paramInfo">The parameter information.</param>
 1360    /// <param name="attribute">The OpenApiRequestBody attribute containing request body details.</param>
 1361    private void ApplyPreferredRequestBody(
 1362        CommentHelpInfo help,
 1363        MapRouteOptions routeOptions,
 1364        OpenAPIPathMetadata metadata,
 1365        ParameterMetadata paramInfo,
 1366        OpenApiRequestBodyAttribute attribute)
 1367    {
 01368        var componentRequestBody = GetRequestBody(paramInfo.ParameterType.Name);
 1369
 01370        metadata.RequestBody = attribute.Inline
 01371            ? componentRequestBody.Clone()
 01372            : new OpenApiRequestBodyReference(paramInfo.ParameterType.Name);
 1373
 01374        metadata.RequestBody.Description ??= help.GetParameterDescription(paramInfo.Name);
 01375        routeOptions.ScriptCode.Parameters.Add(new ParameterForInjectionInfo(paramInfo, componentRequestBody, routeOptio
 01376    }
 1377    #endregion
 1378
 1379    /// <summary>
 1380    /// Ensures that the OpenAPIPathMetadata has default responses defined.
 1381    /// </summary>
 1382    /// <param name="metadata">The OpenAPI metadata to update.</param>
 1383    private static void EnsureDefaultResponses(OpenAPIPathMetadata metadata)
 1384    {
 01385        metadata.Responses ??= [];
 01386        if (metadata.Responses.Count > 0)
 1387        {
 01388            return;
 1389        }
 01390        if (metadata.IsOpenApiCallback)
 1391        {
 01392            metadata.Responses.Add("204", new OpenApiResponse { Description = "Accepted" });
 1393        }
 1394        else
 1395        {
 01396            metadata.Responses.Add("200", new OpenApiResponse { Description = "Ok" });
 01397            metadata.Responses.Add("default", new OpenApiResponse { Description = "Unexpected error" });
 1398        }
 01399    }
 1400
 1401    /// <summary>
 1402    /// Finalizes the route options by adding OpenAPI metadata and configuring defaults.
 1403    /// </summary>
 1404    /// <param name="func">The function information.</param>
 1405    /// <param name="sb">The script block.</param>
 1406    /// <param name="metadata">The OpenAPI metadata.</param>
 1407    /// <param name="routeOptions">The route options to update.</param>
 1408    /// <param name="parsedVerb">The HTTP verb parsed from the function.</param>
 1409    private void FinalizeRouteOptions(
 1410        FunctionInfo func,
 1411        ScriptBlock sb,
 1412        OpenAPIPathMetadata metadata,
 1413        MapRouteOptions routeOptions,
 1414        HttpVerb parsedVerb)
 1415    {
 01416        metadata.DocumentId ??= Host.OpenApiDocumentIds;
 01417        var documentIds = metadata.DocumentId;
 01418        if (metadata.IsOpenApiPath)
 1419        {
 01420            FinalizePathRouteOptions(func, sb, metadata, routeOptions, parsedVerb);
 01421            return;
 1422        }
 1423
 01424        if (metadata.IsOpenApiWebhook)
 1425        {
 01426            RegisterWebhook(func, sb, metadata, parsedVerb, documentIds);
 01427            return;
 1428        }
 1429
 01430        if (metadata.IsOpenApiCallback)
 1431        {
 01432            RegisterCallback(func, sb, metadata, parsedVerb, documentIds);
 1433        }
 01434    }
 1435
 1436    /// <summary>
 1437    /// Finalizes the route options for a standard OpenAPI path.
 1438    /// </summary>
 1439    /// <param name="func">The function information.</param>
 1440    /// <param name="sb">The script block.</param>
 1441    /// <param name="metadata">The OpenAPI metadata.</param>
 1442    /// <param name="routeOptions">The route options to update.</param>
 1443    /// <param name="parsedVerb">The HTTP verb parsed from the function.</param>
 1444    private void FinalizePathRouteOptions(
 1445        FunctionInfo func,
 1446        ScriptBlock sb,
 1447        OpenAPIPathMetadata metadata,
 1448        MapRouteOptions routeOptions,
 1449        HttpVerb parsedVerb)
 1450    {
 01451        routeOptions.OpenAPI.Add(parsedVerb, metadata);
 1452
 01453        if (string.IsNullOrWhiteSpace(routeOptions.Pattern))
 1454        {
 01455            routeOptions.Pattern = "/" + func.Name;
 1456        }
 1457
 01458        if (!string.IsNullOrWhiteSpace(metadata.CorsPolicy))
 1459        {
 01460            routeOptions.CorsPolicy = metadata.CorsPolicy;
 1461        }
 1462
 1463        // Set the script block or wrap for form options
 01464        routeOptions.ScriptCode.ScriptBlock = sb;
 01465        routeOptions.IsOpenApiAnnotatedFunctionRoute = true;
 01466        routeOptions.DefaultResponseContentType ??= new Dictionary<string, ICollection<ContentTypeWithSchema>>(Host.Opti
 01467        _ = Host.AddMapRoute(routeOptions);
 01468    }
 1469
 1470    /// <summary>
 1471    /// Registers a webhook in the OpenAPI document descriptors.
 1472    /// </summary>
 1473    /// <param name="func">The function information.</param>
 1474    /// <param name="sb">The script block.</param>
 1475    /// <param name="metadata">The OpenAPI path metadata.</param>
 1476    /// <param name="parsedVerb">The HTTP verb parsed from the function.</param>
 1477    /// <param name="documentIds">The collection of OpenAPI document IDs.</param>
 1478    private void RegisterWebhook(FunctionInfo func, ScriptBlock sb, OpenAPIPathMetadata metadata, HttpVerb parsedVerb, I
 1479    {
 01480        EnsureParamOnlyScriptBlock(func, sb, kind: "webhook");
 01481        foreach (var docId in documentIds)
 1482        {
 01483            var docdesc = GetDocDescriptorOrThrow(docId, attributeName: "OpenApiWebhook");
 01484            _ = docdesc.WebHook.TryAdd((metadata.Pattern, parsedVerb), metadata);
 1485        }
 01486    }
 1487    /// <summary>
 1488    /// Registers a callback in the OpenAPI document descriptors.
 1489    /// </summary>
 1490    /// <param name="func">The function information.</param>
 1491    /// <param name="sb">The script block.</param>
 1492    /// <param name="metadata">The OpenAPI path metadata.</param>
 1493    /// <param name="parsedVerb">The HTTP verb parsed from the function.</param>
 1494    /// <param name="documentIds">The collection of OpenAPI document IDs.</param>
 1495    private void RegisterCallback(FunctionInfo func, ScriptBlock sb, OpenAPIPathMetadata metadata, HttpVerb parsedVerb, 
 1496    {
 01497        EnsureParamOnlyScriptBlock(func, sb, kind: "callback");
 01498        foreach (var docId in documentIds)
 1499        {
 01500            var docdesc = GetDocDescriptorOrThrow(docId, attributeName: "OpenApiCallback");
 01501            _ = docdesc.Callbacks.TryAdd((metadata.Pattern, parsedVerb), metadata);
 1502        }
 01503    }
 1504
 1505    /// <summary>
 1506    /// Retrieves the OpenApiDocDescriptor for the specified document ID or throws an exception if not found.
 1507    /// </summary>
 1508    /// <param name="docId">The document ID to look up.</param>
 1509    /// <param name="attributeName">The name of the attribute requesting the document.</param>
 1510    /// <returns>The corresponding OpenApiDocDescriptor.</returns>
 1511    private OpenApiDocDescriptor GetDocDescriptorOrThrow(string docId, string attributeName)
 1512    {
 01513        return Host.OpenApiDocumentDescriptor.TryGetValue(docId, out var docdesc)
 01514            ? docdesc
 01515            : throw new InvalidOperationException($"The OpenAPI document ID '{docId}' specified in the {attributeName} a
 1516    }
 1517
 1518    /// <summary>
 1519    /// Ensures that the ScriptBlock contains only a param() block with no executable statements.
 1520    /// </summary>
 1521    /// <param name="func">The function information.</param>
 1522    /// <param name="sb">The ScriptBlock to validate.</param>
 1523    /// <param name="kind">The kind of function (e.g., "webhook" or "callback").</param>
 1524    /// <exception cref="InvalidOperationException">Thrown if the ScriptBlock contains executable statements other than 
 1525    private static void EnsureParamOnlyScriptBlock(FunctionInfo func, ScriptBlock sb, string kind)
 1526    {
 01527        if (!PsScriptBlockValidation.IsParamLast(sb))
 1528        {
 01529            throw new InvalidOperationException($"The ScriptBlock for {kind} function '{func.Name}' must contain only a 
 1530        }
 01531    }
 1532
 1533    /// <summary>
 1534    /// Creates a request body from the given attribute.
 1535    /// </summary>
 1536    /// <param name="attribute">The attribute containing request body information.</param>
 1537    /// <param name="requestBody">The OpenApiRequestBody object to populate.</param>
 1538    /// <param name="schema">The schema to associate with the request body.</param>
 1539    /// <returns>True if the request body was created successfully; otherwise, false.</returns>
 1540    private static bool CreateRequestBodyFromAttribute(KestrunAnnotation attribute, OpenApiRequestBody requestBody, IOpe
 1541    {
 1542        switch (attribute)
 1543        {
 1544            case OpenApiRequestBodyAttribute request:
 01545                requestBody.Description = request.Description;
 01546                requestBody.Required = request.Required;
 1547                // Content
 01548                requestBody.Content ??= new Dictionary<string, IOpenApiMediaType>(StringComparer.Ordinal);
 01549                var mediaType = new OpenApiMediaType();
 1550                // Example
 01551                if (request.Example is not null)
 1552                {
 01553                    mediaType.Example = OpenApiJsonNodeFactory.ToNode(request.Example);
 1554                }
 1555                // Schema
 01556                mediaType.Schema = schema;
 01557                foreach (var contentType in request.ContentType)
 1558                {
 01559                    requestBody.Content[contentType] = mediaType;
 1560                }
 01561                return true;
 1562            default:
 01563                return false;
 1564        }
 1565    }
 1566}

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_BuildPath.cs

#LineLine coverage
 1using Microsoft.OpenApi;
 2using Kestrun.Hosting.Options;
 3using Kestrun.Utilities;
 4
 5namespace Kestrun.OpenApi;
 6
 7public partial class OpenApiDocDescriptor
 8{
 9    /// <summary>
 10    /// Populates Document.Paths from the registered routes using OpenAPI metadata on each route.
 11    /// </summary>
 12    /// <param name="routes">The registered routes with OpenAPI metadata.</param>
 13    private void BuildPathsFromRegisteredRoutes(Dictionary<(string Pattern, HttpVerb Method), MapRouteOptions> routes)
 14    {
 215        if (routes is null || routes.Count == 0)
 16        {
 017            return;
 18        }
 219        Document.Paths = [];
 20
 221        var groups = CreateOpenApiRouteEntries(routes)
 222            .GroupBy(entry => entry.Pattern, StringComparer.Ordinal)
 423            .Where(g => !string.IsNullOrWhiteSpace(g.Key));
 24
 825        foreach (var grp in groups)
 26        {
 227            ProcessOpenApiRouteGroup(grp);
 28        }
 229    }
 30
 31    /// <summary>
 32    /// Flattens registered routes into OpenAPI entries using the OpenAPI metadata pattern.
 33    /// </summary>
 34    /// <param name="routes">The registered routes.</param>
 35    /// <returns>The OpenAPI route entries.</returns>
 36    private static IEnumerable<OpenApiRouteEntry> CreateOpenApiRouteEntries(
 37        Dictionary<(string Pattern, HttpVerb Method), MapRouteOptions> routes)
 38    {
 839        foreach (var kvp in routes)
 40        {
 241            var map = kvp.Value;
 242            if (map is null || map.OpenAPI.Count == 0)
 43            {
 44                continue;
 45            }
 46
 847            foreach (var metaKvp in map.OpenAPI)
 48            {
 249                var meta = metaKvp.Value;
 250                var pattern = meta.Pattern;
 251                if (string.IsNullOrWhiteSpace(pattern))
 52                {
 053                    pattern = kvp.Key.Pattern;
 54                }
 55
 256                if (string.IsNullOrWhiteSpace(pattern))
 57                {
 58                    continue;
 59                }
 60
 261                yield return new OpenApiRouteEntry(pattern, metaKvp.Key, map, meta);
 62            }
 263        }
 264    }
 65
 66    /// <summary>
 67    /// Processes a group of routes sharing the same OpenAPI pattern to build the corresponding OpenAPI path item.
 68    /// </summary>
 69    /// <param name="grp">The group of routes sharing the same OpenAPI pattern.</param>
 70    private void ProcessOpenApiRouteGroup(IGrouping<string, OpenApiRouteEntry> grp)
 71    {
 272        var pattern = grp.Key;
 273        var pathItem = GetOrCreatePathItem(pattern);
 274        OpenAPICommonMetadata? pathMeta = null;
 75
 876        foreach (var entry in grp)
 77        {
 278            pathMeta = ProcessOpenApiRouteEntry(entry, pathItem, pathMeta);
 79        }
 80
 281        if (pathMeta is not null)
 82        {
 083            ApplyPathLevelMetadata(pathItem, pathMeta, pattern);
 84        }
 285    }
 86    /// <summary>
 87    /// Retrieves or creates an OpenApiPathItem for the specified pattern.
 88    /// </summary>
 89    /// <param name="pattern">The route pattern.</param>
 90    /// <returns>The corresponding OpenApiPathItem.</returns>
 91    private OpenApiPathItem GetOrCreatePathItem(string pattern)
 92    {
 293        Document.Paths ??= [];
 294        if (!Document.Paths.TryGetValue(pattern, out var pathInterface) || pathInterface is null)
 95        {
 296            pathInterface = new OpenApiPathItem();
 297            Document.Paths[pattern] = pathInterface;
 98        }
 299        return (OpenApiPathItem)pathInterface;
 100    }
 101
 102    /// <summary>
 103    /// Processes a single OpenAPI route entry and adds it to the OpenApiPathItem.
 104    /// </summary>
 105    /// <param name="entry">The OpenAPI route entry.</param>
 106    /// <param name="pathItem">The OpenApiPathItem to which the operation will be added.</param>
 107    /// <param name="currentPathMeta">The current path-level OpenAPI metadata.</param>
 108    /// <returns>The updated path-level OpenAPI metadata.</returns>
 109    private OpenAPICommonMetadata? ProcessOpenApiRouteEntry(
 110        OpenApiRouteEntry entry,
 111        OpenApiPathItem pathItem,
 112        OpenAPICommonMetadata? currentPathMeta)
 113    {
 2114        var method = entry.Method;
 2115        var map = entry.Map;
 116
 2117        if (map is null || map.OpenAPI.Count == 0)
 118        {
 0119            return currentPathMeta;
 120        }
 121
 2122        if ((map.PathLevelOpenAPIMetadata is not null) && (currentPathMeta is null))
 123        {
 0124            currentPathMeta = map.PathLevelOpenAPIMetadata;
 125        }
 126
 2127        var meta = entry.Metadata;
 2128        if (meta.Enabled)
 129        {
 2130            if (meta.DocumentId is not null && !meta.DocumentId.Contains(DocumentId))
 131            {
 0132                return currentPathMeta;
 133            }
 2134            var op = BuildOperationFromMetadata(meta);
 2135            pathItem.AddOperation(HttpMethod.Parse(method.ToMethodString()), op);
 136        }
 137
 2138        return currentPathMeta;
 139    }
 140
 141    /// <summary>
 142    /// Represents a flattened OpenAPI route entry derived from registered routes.
 143    /// </summary>
 144    /// <param name="Pattern">The OpenAPI pattern.</param>
 145    /// <param name="Method">The HTTP verb.</param>
 146    /// <param name="Map">The map route options.</param>
 147    /// <param name="Metadata">The OpenAPI metadata.</param>
 2148    private sealed record OpenApiRouteEntry(
 2149        string Pattern,
 2150        HttpVerb Method,
 2151        MapRouteOptions Map,
 4152        OpenAPIPathMetadata Metadata);
 153
 154    /// <summary>
 155    /// Applies path-level OpenAPI metadata to the given OpenApiPathItem.
 156    /// </summary>
 157    /// <param name="pathItem">The OpenApiPathItem to which the metadata will be applied.</param>
 158    /// <param name="pathMeta">The path-level OpenAPI metadata.</param>
 159    /// <param name="pattern">The route pattern associated with the path item.</param>
 160    private void ApplyPathLevelMetadata(OpenApiPathItem pathItem, OpenAPICommonMetadata pathMeta, string pattern)
 161    {
 0162        pathItem.Description = pathMeta.Description;
 0163        pathItem.Summary = pathMeta.Summary;
 164        try
 165        {
 0166            ApplyPathLevelServers(pathItem, pathMeta);
 0167            ApplyPathLevelParameters(pathItem, pathMeta);
 0168        }
 0169        catch (Exception ex)
 170        {
 0171            if (Host.Logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 172            {
 0173                Host.Logger.Debug(ex, "Tolerated exception in path-level OpenAPI metadata assignment for pattern {Patter
 174            }
 0175        }
 0176    }
 177
 178    /// <summary>
 179    /// Applies server information from path-level metadata to the OpenApiPathItem.
 180    /// </summary>
 181    /// <param name="pathItem">The OpenApiPathItem to modify.</param>
 182    /// <param name="pathMeta">The path-level OpenAPI metadata containing server information.</param>
 183    private static void ApplyPathLevelServers(OpenApiPathItem pathItem, OpenAPICommonMetadata pathMeta)
 184    {
 0185        if (pathMeta.Servers is { Count: > 0 })
 186        {
 0187            dynamic dPath = pathItem;
 0188            if (dPath.Servers == null) { dPath.Servers = new List<OpenApiServer>(); }
 0189            foreach (var s in pathMeta.Servers)
 190            {
 0191                dPath.Servers.Add(s);
 192            }
 193        }
 0194    }
 195
 196    /// <summary>
 197    /// Applies parameter information from path-level metadata to the OpenApiPathItem.
 198    /// </summary>
 199    /// <param name="pathItem">The OpenApiPathItem to modify.</param>
 200    /// <param name="pathMeta">The path-level OpenAPI metadata containing parameter information.</param>
 201    private static void ApplyPathLevelParameters(OpenApiPathItem pathItem, OpenAPICommonMetadata pathMeta)
 202    {
 0203        if (pathMeta.Parameters is { Count: > 0 })
 204        {
 0205            dynamic dPath = pathItem;
 0206            if (dPath.Parameters == null) { dPath.Parameters = new List<IOpenApiParameter>(); }
 0207            foreach (var p in pathMeta.Parameters)
 208            {
 0209                dPath.Parameters.Add(p);
 210            }
 211        }
 0212    }
 213}

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_BuildSchema.cs

#LineLine coverage
 1using System.Reflection;
 2using System.Text.Json.Nodes;
 3using Kestrun.Forms;
 4using Microsoft.OpenApi;
 5
 6namespace Kestrun.OpenApi;
 7
 8public partial class OpenApiDocDescriptor
 9{
 13810    private readonly Stack<string?> _formPartScopeStack = new();
 11
 12    /// <summary>
 13    /// Merges OpenApiProperties with OpenApiXmlAttribute if present.
 14    /// </summary>
 15    /// <param name="prop">The property to extract attributes from.</param>
 16    /// <returns>Merged OpenApiProperties with XML metadata applied.</returns>
 17    private static OpenApiProperties? MergeXmlAttributes(PropertyInfo prop)
 18    {
 19619        var properties = prop.GetCustomAttribute<OpenApiProperties>();
 19620        var xmlAttr = prop.GetCustomAttribute<OpenApiXmlAttribute>();
 21
 19622        if (xmlAttr == null)
 23        {
 18824            return properties;
 25        }
 26
 27        // If no OpenApiProperties, create a new one to hold XML data
 828        properties ??= new OpenApiPropertyAttribute();
 29
 30        // Merge XML attribute properties into OpenApiProperties
 831        if (!string.IsNullOrWhiteSpace(xmlAttr.Name))
 32        {
 233            properties.XmlName = xmlAttr.Name;
 34        }
 35
 836        if (!string.IsNullOrWhiteSpace(xmlAttr.Namespace))
 37        {
 238            properties.XmlNamespace = xmlAttr.Namespace;
 39        }
 40
 841        if (!string.IsNullOrWhiteSpace(xmlAttr.Prefix))
 42        {
 143            properties.XmlPrefix = xmlAttr.Prefix;
 44        }
 45
 846        if (xmlAttr.Attribute)
 47        {
 248            properties.XmlAttribute = true;
 49        }
 50
 851        if (xmlAttr.Wrapped)
 52        {
 253            properties.XmlWrapped = true;
 54        }
 55
 856        return properties;
 57    }
 58
 59    /// <summary>
 60    /// Builds and adds the schema for a given type to the document components.
 61    /// </summary>
 62    /// <param name="t">The type to build the schema for.</param>
 63    /// <param name="built">The set of already built types to avoid recursion.</param>
 64    private void BuildSchema(Type t, HashSet<Type>? built = null)
 65    {
 4666        if (Document.Components is not null && Document.Components.Schemas is not null)
 67        {
 4668            if (!ComponentSchemasExists(t.Name))
 69            {
 3870                if (!PrimitiveSchemaMap.ContainsKey(t))
 71                {
 3872                    Document.Components.Schemas[t.Name] = BuildSchemaForType(t, built);
 73                }
 74            }
 75        }
 4676    }
 77
 78    /// <summary>
 79    /// Builds the schema for a property, handling nullable types and complex types.
 80    /// </summary>
 81    /// <param name="p">The property info.</param>
 82    /// <param name="built">The set of already built types to avoid recursion.</param>
 83    /// <returns>The constructed OpenAPI schema for the property.</returns>
 84    private IOpenApiSchema BuildPropertySchema(PropertyInfo p, HashSet<Type> built)
 85    {
 16886        var (propertyType, allowNull) = UnwrapNullableType(p.PropertyType);
 87
 16888        if (propertyType == typeof(KrFilePart))
 89        {
 090            return BuildFilePartSchema(p, allowNull);
 91        }
 92
 16893        ApplyKrPartScope(p, propertyType, out var pushScope);
 94
 95        IOpenApiSchema schema;
 96        try
 97        {
 16898            schema = BuildPropertyTypeSchema(propertyType, p, built);
 16899        }
 100        finally
 101        {
 168102            if (pushScope)
 103            {
 9104                _ = _formPartScopeStack.Pop();
 105            }
 168106        }
 107
 168108        schema = ApplyNullableSchema(schema, allowNull);
 168109        ApplySchemaAttr(MergeXmlAttributes(p), schema);
 168110        PowerShellAttributes.ApplyPowerShellAttributes(p, schema);
 168111        return schema;
 112    }
 113
 114    /// <summary>
 115    /// Unwraps nullable types and returns the underlying type and nullable flag.
 116    /// </summary>
 117    /// <param name="propertyType">The original property type.</param>
 118    /// <returns>A tuple containing the non-nullable type and a nullable flag.</returns>
 119    private static (Type PropertyType, bool AllowNull) UnwrapNullableType(Type propertyType)
 120    {
 168121        var underlying = Nullable.GetUnderlyingType(propertyType);
 168122        return underlying is null ? (propertyType, false) : (underlying, true);
 123    }
 124
 125    /// <summary>
 126    /// Builds the schema for a <see cref="KrFilePart"/> property, including nullability when needed.
 127    /// </summary>
 128    /// <param name="p">The property info.</param>
 129    /// <param name="allowNull">Whether the property allows null.</param>
 130    /// <returns>The constructed OpenAPI schema for the file part.</returns>
 131    private IOpenApiSchema BuildFilePartSchema(PropertyInfo p, bool allowNull)
 132    {
 0133        var fileSchema = new OpenApiSchema
 0134        {
 0135            Type = JsonSchemaType.String,
 0136            Format = "binary"
 0137        };
 0138        ApplySchemaAttr(MergeXmlAttributes(p), fileSchema);
 0139        PowerShellAttributes.ApplyPowerShellAttributes(p, fileSchema);
 0140        return allowNull ? MakeNullable(fileSchema, isNullable: true) : fileSchema;
 141    }
 142
 143    /// <summary>
 144    /// Applies form part attributes and pushes nested scope when required.
 145    /// </summary>
 146    /// <param name="p">The property info.</param>
 147    /// <param name="propertyType">The resolved property type.</param>
 148    /// <param name="pushScope">Set to <c>true</c> when a new scope was pushed.</param>
 149    private void ApplyKrPartScope(PropertyInfo p, Type propertyType, out bool pushScope)
 150    {
 168151        var currentScope = _formPartScopeStack.Count > 0 ? _formPartScopeStack.Peek() : null;
 168152        FormHelper.ApplyKrPartAttributes(Host, p, currentScope);
 153
 168154        var hasKrPartAttribute = p.IsDefined(typeof(KrPartAttribute), inherit: false);
 168155        var partName = hasKrPartAttribute ? FormHelper.ResolvePartName(p) : null;
 168156        pushScope = hasKrPartAttribute && !string.IsNullOrWhiteSpace(partName) && ShouldPushNestedScope(propertyType);
 168157        if (pushScope)
 158        {
 9159            _formPartScopeStack.Push(partName);
 160        }
 168161    }
 162
 163    /// <summary>
 164    /// Builds the schema for the resolved property type.
 165    /// </summary>
 166    /// <param name="propertyType">The resolved property type.</param>
 167    /// <param name="p">The property info.</param>
 168    /// <param name="built">The set of already built types to avoid recursion.</param>
 169    /// <returns>The constructed OpenAPI schema for the property type.</returns>
 170    private IOpenApiSchema BuildPropertyTypeSchema(Type propertyType, PropertyInfo p, HashSet<Type> built)
 171    {
 168172        if (PrimitiveSchemaMap.TryGetValue(propertyType, out var getSchema))
 173        {
 144174            return getSchema();
 175        }
 176
 24177        if (propertyType.IsArray)
 178        {
 10179            return BuildArraySchema(propertyType, p, built);
 180        }
 181
 182        // Treat enums and complex types the same: register as component and reference
 14183        return BuildComplexTypeSchema(propertyType, p, built);
 184    }
 185
 186    /// <summary>
 187    /// Applies nullable behavior to the schema when required.
 188    /// </summary>
 189    /// <param name="schema">The schema to update.</param>
 190    /// <param name="allowNull">Whether the property allows null.</param>
 191    /// <returns>The updated schema.</returns>
 192    private static IOpenApiSchema ApplyNullableSchema(IOpenApiSchema schema, bool allowNull)
 193    {
 168194        if (!allowNull)
 195        {
 160196            return schema;
 197        }
 198
 8199        if (schema is OpenApiSchema s)
 200        {
 201            // For inline schemas, add null type directly
 7202            s.Type |= JsonSchemaType.Null;
 7203            return s;
 204        }
 205
 1206        if (schema is OpenApiSchemaReference refSchema)
 207        {
 1208            var modifiedRefSchema = refSchema.Clone();
 1209            modifiedRefSchema.Description = null; // clear description to avoid duplication
 210            // For $ref schemas (enums/complex types), wrap in anyOf with null
 1211            return new OpenApiSchema
 1212            {
 1213                AnyOf =
 1214                [
 1215                    modifiedRefSchema,
 1216                    new OpenApiSchema { Type = JsonSchemaType.Null }
 1217                ]
 1218            };
 219        }
 220
 0221        return schema;
 222    }
 223
 224    /// <summary>
 225    /// Determines whether to push a new nested scope based on the property type.
 226    /// </summary>
 227    /// <param name="propertyType">The type of the property to evaluate.</param>
 228    /// <returns><c>true</c> if a new nested scope should be pushed; otherwise, <c>false</c>.</returns>
 229    private static bool ShouldPushNestedScope(Type propertyType)
 230    {
 18231        var candidate = propertyType;
 232
 18233        if (candidate.IsArray)
 234        {
 4235            candidate = candidate.GetElementType()!;
 236        }
 237
 18238        if (candidate.IsGenericType && candidate.GetGenericTypeDefinition() == typeof(Nullable<>))
 239        {
 0240            candidate = Nullable.GetUnderlyingType(candidate)!;
 241        }
 242
 18243        return !candidate.IsEnum && !PrimitiveSchemaMap.ContainsKey(candidate);
 244    }
 245
 246    /// <summary>
 247    /// Builds the schema for a complex type property.
 248    /// </summary>
 249    /// <param name="pt">The property type.</param>
 250    /// <param name="p">The property info.</param>
 251    /// <param name="built">The set of already built types to avoid recursion.</param>
 252    /// <returns>The constructed OpenAPI schema for the complex type property.</returns>
 253    private OpenApiSchemaReference BuildComplexTypeSchema(Type pt, PropertyInfo p, HashSet<Type> built)
 254    {
 15255        BuildSchema(pt, built); // ensure component exists
 15256        var refSchema = new OpenApiSchemaReference(pt.Name);
 15257        ApplySchemaAttr(MergeXmlAttributes(p), refSchema);
 15258        return refSchema;
 259    }
 260
 261    /// <summary>
 262    /// Builds the schema for an enum property.
 263    /// </summary>
 264    /// <param name="pt">The property type.</param>
 265    /// <param name="p">The property info.</param>
 266    /// <returns>The constructed OpenAPI schema for the enum property.</returns>
 267    private OpenApiSchema BuildEnumSchema(Type pt, PropertyInfo p)
 268    {
 0269        var s = new OpenApiSchema
 0270        {
 0271            Type = JsonSchemaType.String,
 0272            Enum = [.. pt.GetEnumNames().Select(n => (JsonNode)n)]
 0273        };
 0274        var attrs = p.GetCustomAttributes<OpenApiPropertyAttribute>(inherit: false).ToArray();
 0275        var a = MergeSchemaAttributes(attrs);
 0276        ApplySchemaAttr(MergeXmlAttributes(p) ?? a, s);
 0277        PowerShellAttributes.ApplyPowerShellAttributes(p, s);
 0278        return s;
 279    }
 280
 281    /// <summary>
 282    /// Builds the schema for an array property.
 283    /// </summary>
 284    /// <param name="pt">The property type.</param>
 285    /// <param name="p">The property info.</param>
 286    /// <param name="built">The set of already built types to avoid recursion.</param>
 287    /// <returns>The constructed OpenAPI schema for the array property.</returns>
 288    private OpenApiSchema BuildArraySchema(Type pt, PropertyInfo p, HashSet<Type> built)
 289    {
 13290        var item = pt.GetElementType()!;
 291        IOpenApiSchema itemSchema;
 292
 13293        if (item == typeof(KrFilePart))
 294        {
 0295            itemSchema = new OpenApiSchema
 0296            {
 0297                Type = JsonSchemaType.String,
 0298                Format = "binary"
 0299            };
 300        }
 301        else
 302        {
 13303            if (PrimitiveSchemaMap.TryGetValue(item, out var getSchema))
 304            {
 7305                itemSchema = getSchema();
 306            }
 307            else
 308            {
 309                // Treat enums and complex types the same: register as component and reference
 6310                BuildSchema(item, built); // ensure component exists
 6311                itemSchema = new OpenApiSchemaReference(item.Name);
 312            }
 313        }
 13314        var s = new OpenApiSchema
 13315        {
 13316            Type = JsonSchemaType.Array,
 13317            Items = itemSchema
 13318        };
 13319        ApplySchemaAttr(MergeXmlAttributes(p), s);
 13320        PowerShellAttributes.ApplyPowerShellAttributes(p, s);
 13321        return s;
 322    }
 323
 324    /// <summary>
 325    /// Builds the schema for a primitive type property.
 326    /// </summary>
 327    /// <param name="pt">The property type.</param>
 328    /// <param name="p">The property info.</param>
 329    /// <returns>The constructed OpenAPI schema for the primitive type property.</returns>
 330    private IOpenApiSchema BuildPrimitiveSchema(Type pt, PropertyInfo p)
 331    {
 0332        var prim = InferPrimitiveSchema(pt);
 0333        ApplySchemaAttr(MergeXmlAttributes(p), prim);
 0334        PowerShellAttributes.ApplyPowerShellAttributes(p, prim);
 0335        return prim;
 336    }
 337
 338    /// <summary>
 339    /// Gets or creates an OpenAPI schema item in either inline or document components.
 340    /// </summary>
 341    /// <param name="schemaName">The name of the schema.</param>
 342    /// <param name="inline">Whether to use inline components or document components.</param>
 343    /// <returns>The OpenApiSchema item.</returns>
 344    private OpenApiSchema GetOrCreateSchemaItem(string schemaName, bool inline)
 345    {
 346        IDictionary<string, IOpenApiSchema> schema;
 347        // Determine whether to use inline components or document components
 0348        if (inline)
 349        {
 350            // Use inline components
 0351            InlineComponents.Schemas ??= new Dictionary<string, IOpenApiSchema>(StringComparer.Ordinal);
 0352            schema = InlineComponents.Schemas;
 353        }
 354        else
 355        {
 356            // Use document components
 0357            Document.Components ??= new OpenApiComponents();
 0358            Document.Components.Schemas ??= new Dictionary<string, IOpenApiSchema>(StringComparer.Ordinal);
 0359            schema = Document.Components.Schemas;
 360        }
 361        // Retrieve or create the request body item
 0362        if (!schema.TryGetValue(schemaName, out var openApiSchemaItem) || openApiSchemaItem is null)
 363        {
 364            // Create a new OpenApiSchema if it doesn't exist
 0365            openApiSchemaItem = new OpenApiSchema();
 0366            schema[schemaName] = openApiSchemaItem;
 367        }
 368        // return the request body item
 0369        return (OpenApiSchema)openApiSchemaItem;
 370    }
 371
 372    /// <summary>
 373    /// Tries to get a schema by name from either inline or document components.
 374    /// </summary>
 375    /// <param name="schemaName">The name of the schema to retrieve.</param>
 376    /// <param name="schema">The retrieved schema if found; otherwise, null.</param>
 377    /// <param name="isInline">Indicates whether the schema was found in inline components.</param>
 378    /// <returns>True if the schema was found; otherwise, false.</returns>
 379    private bool TryGetSchemaItem(string schemaName, out IOpenApiSchema? schema, out bool isInline)
 380    {
 0381        if (TryGetInline(name: schemaName, kind: OpenApiComponentKind.Schemas, out schema))
 382        {
 0383            isInline = true;
 0384            return true;
 385        }
 0386        else if (TryGetComponent(name: schemaName, kind: OpenApiComponentKind.Schemas, out schema))
 387        {
 0388            isInline = false;
 0389            return true;
 390        }
 0391        schema = null;
 0392        isInline = false;
 0393        return false;
 394    }
 395
 396    /// <summary>
 397    /// Tries to get a schema by name from either inline or document components.
 398    /// </summary>
 399    /// <param name="schemaName">The name of the schema to retrieve.</param>
 400    /// <param name="schema">The retrieved schema if found; otherwise, null.</param>
 401    /// <returns>True if the schema was found; otherwise, false.</returns>
 402    private bool TryGetSchemaItem(string schemaName, out IOpenApiSchema? schema) =>
 0403    TryGetSchemaItem(schemaName, out schema, out _);
 404}

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Callbacks.cs

#LineLine coverage
 1using Kestrun.Callback;
 2using Kestrun.Hosting.Options;
 3using Kestrun.Utilities;
 4using Microsoft.OpenApi;
 5
 6namespace Kestrun.OpenApi;
 7
 8/// <summary>
 9/// Helper methods for accessing OpenAPI document components.
 10/// </summary>
 11public partial class OpenApiDocDescriptor
 12{
 13    /// <summary>
 14    /// Applies the OpenApiCallbackRef attribute to the function's OpenAPI metadata.
 15    /// </summary>
 16    /// <param name="metadata">The OpenAPI metadata to populate.</param>
 17    /// <param name="attribute">The OpenApiCallbackRef attribute instance.</param>
 18    /// <exception cref="InvalidOperationException">Thrown if the referenced callback component is not found.</exception
 19    private void ApplyCallbackRefAttribute(OpenAPIPathMetadata metadata, OpenApiCallbackRefAttribute attribute)
 20    {
 021        metadata.Callbacks ??= new Dictionary<string, IOpenApiCallback>();
 22
 023        if (TryGetInline(name: attribute.ReferenceId, kind: OpenApiComponentKind.Callbacks, out OpenApiCallback? callbac
 24        {
 025            metadata.Callbacks.Add(attribute.Key, callback!.Clone());
 26        }
 027        else if (TryGetComponent(name: attribute.ReferenceId, kind: OpenApiComponentKind.Callbacks, out callback))
 28        {
 029            if (attribute.Inline)
 30            {
 031                metadata.Callbacks.Add(attribute.Key, callback!.Clone());
 32            }
 33            else
 34            {
 035                var reference = new OpenApiCallbackReference(attribute.ReferenceId);
 036                metadata.Callbacks.Add(attribute.Key, reference);
 37            }
 38        }
 039        else if (attribute.Inline)
 40        {
 041            throw new InvalidOperationException($"Inline callback component with ID '{attribute.ReferenceId}' not found.
 42        }
 43
 044        if (callback is not null)
 45        {
 46            // Compile and store the CallbackPlan for this callback
 047            metadata.MapOptions.CallbackPlan.AddRange(CallbackPlanCompiler.Compile(callback, attribute.ReferenceId));
 48        }
 049    }
 50
 51    /// <summary>
 52    /// Populates Document.Callbacks from the registered callbacks using OpenAPI metadata on each callback.
 53    /// </summary>
 54    /// <param name="Metadata"> The dictionary containing callback patterns, HTTP methods, and their associated OpenAPI 
 55    private void BuildCallbacks(Dictionary<(string Pattern, HttpVerb Method), OpenAPIPathMetadata> Metadata)
 56    {
 057        if (Metadata is null || Metadata.Count == 0)
 58        {
 059            return;
 60        }
 61
 062        var groups = Metadata
 063            .GroupBy(kvp => kvp.Key.Pattern, StringComparer.Ordinal)
 064            .Where(g => !string.IsNullOrWhiteSpace(g.Key));
 65
 066        foreach (var grp in groups)
 67        {
 068            ProcessCallbacksGroup(grp);
 69        }
 070    }
 71    /// <summary>
 72    /// Processes a group of callbacks sharing the same pattern to build the corresponding OpenAPI callback item.
 73    /// </summary>
 74    /// <param name="grp">The group of callbacks sharing the same pattern. </param>
 75    private void ProcessCallbacksGroup(IGrouping<string, KeyValuePair<(string Pattern, HttpVerb Method), OpenAPIPathMeta
 76    {
 077        var pattern = grp.Key;
 78
 079        foreach (var kvp in grp)
 80        {
 081            if (kvp.Value.DocumentId is not null && !kvp.Value.DocumentId.Contains(DocumentId))
 82            {
 83                continue;
 84            }
 085            var callbackItem = GetOrCreateCallbackItem(pattern, kvp.Value.Inline);
 086            ProcessCallbackOperation(kvp, callbackItem);
 87        }
 088    }
 89
 90    /// <summary>
 91    /// Processes a single callback operation and adds it to the OpenApiCallback.
 92    /// </summary>
 93    /// <param name="kvp"> The key-value pair representing the callback pattern, HTTP method, and OpenAPI metadata.</par
 94    /// <param name="callbackItem"> The OpenApiCallback to which the operation will be added.</param>
 95    /// <exception cref="InvalidOperationException"> Thrown when the required Expression property is missing in the Open
 96    private void ProcessCallbackOperation(KeyValuePair<(string Pattern, HttpVerb Method), OpenAPIPathMetadata> kvp, Open
 97    {
 098        callbackItem.PathItems ??= [];
 099        var method = kvp.Key.Method;
 0100        var openapiMetadata = kvp.Value;
 0101        if (openapiMetadata.Expression is null)
 102        {
 0103            throw new InvalidOperationException($"Callback OpenAPI metadata for pattern '{kvp.Key.Pattern}' and method '
 104        }
 105        // Check if the path item for this expression already exists
 0106        var expr = openapiMetadata.Expression;
 0107        var httpMethod = HttpMethod.Parse(method.ToMethodString());
 108        // Only add the path item if it doesn't already exist
 0109        if (!callbackItem.PathItems.TryGetValue(expr, out var iPathItem))
 110        {
 0111            var op = BuildOperationFromMetadata(openapiMetadata);
 0112            var pathItem = new OpenApiPathItem();
 0113            pathItem.AddOperation(httpMethod, op);
 114            // Add the new path item to the callback
 0115            callbackItem.PathItems.Add(expr, pathItem);
 116        }
 117        else
 118        {
 0119            if (iPathItem is OpenApiPathItem pathItem)
 120            {
 0121                if (pathItem.Operations is not null && pathItem.Operations.ContainsKey(httpMethod))
 122                {
 123                    // Operation for this method already exists; skip adding
 0124                    return;
 125                }
 0126                var op = BuildOperationFromMetadata(openapiMetadata);
 0127                pathItem.AddOperation(httpMethod, op);
 128            }
 129            else
 130            {
 0131                throw new InvalidOperationException($"Existing path item for expression '{expr.Expression}' is not of ty
 132            }
 133        }
 134    }
 135
 136    /// <summary>
 137    /// Retrieves or creates an OpenApiCallback for the specified pattern.
 138    /// </summary>
 139    /// <param name="pattern">The callback pattern.</param>
 140    /// <param name="inline">Indicates whether the callback is inline.</param>
 141    /// <returns>The corresponding OpenApiCallback.</returns>
 142    private OpenApiCallback GetOrCreateCallbackItem(string pattern, bool inline)
 143    {
 144        IDictionary<string, IOpenApiCallback> callbacks;
 145        // Determine whether to use inline components or document components
 0146        if (inline)
 147        {
 148            // Use inline components
 0149            InlineComponents.Callbacks ??= new Dictionary<string, IOpenApiCallback>(StringComparer.Ordinal);
 0150            callbacks = InlineComponents.Callbacks;
 151        }
 152        else
 153        {
 154            // Use document components
 0155            Document.Components ??= new OpenApiComponents();
 0156            Document.Components.Callbacks ??= new Dictionary<string, IOpenApiCallback>(StringComparer.Ordinal);
 0157            callbacks = Document.Components.Callbacks;
 158        }
 159        // Retrieve or create the callback item
 0160        if (!callbacks.TryGetValue(pattern, out var pathInterface) || pathInterface is null)
 161        {
 162            // Create a new OpenApiCallback if it doesn't exist
 0163            pathInterface = new OpenApiCallback();
 0164            callbacks[pattern] = pathInterface;
 165        }
 166        // return the callback item
 0167        return (OpenApiCallback)pathInterface;
 168    }
 169
 170    /// <summary>
 171    /// Applies callback information from metadata to the OpenApiOperation.
 172    /// </summary>
 173    /// <param name="op">The OpenApiOperation to modify.</param>
 174    /// <param name="meta">The OpenAPIPathMetadata containing callback information.</param>
 175    private static void ApplyCallbacks(OpenApiOperation op, OpenAPIPathMetadata meta)
 176    {
 7177        if (meta.Callbacks is not null && meta.Callbacks.Count > 0)
 178        {
 0179            op.Callbacks = new Dictionary<string, IOpenApiCallback>(meta.Callbacks);
 180        }
 7181    }
 182}

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Examples.cs

#LineLine coverage
 1using System.Collections;
 2using Microsoft.OpenApi;
 3
 4namespace Kestrun.OpenApi;
 5
 6/// <summary>
 7/// Generates OpenAPI v2 (Swagger) documents from C# types decorated with OpenApiSchema attributes.
 8/// </summary>
 9public partial class OpenApiDocDescriptor
 10{
 11    /// <summary>
 12    /// Adds a component example to the OpenAPI document.
 13    /// </summary>
 14    /// <param name="name">The name of the example component.</param>
 15    /// <param name="example">The example component to add.</param>
 16    /// <param name="ifExists">The conflict resolution strategy if an example with the same name already exists.</param>
 17    public void AddComponentExample(
 18        string name,
 19        OpenApiExample example,
 20        OpenApiComponentConflictResolution ifExists = OpenApiComponentConflictResolution.Overwrite)
 21    {
 1622        Document.Components ??= new OpenApiComponents();
 23        // Ensure Examples dictionary exists
 1624        Document.Components.Examples ??= new Dictionary<string, IOpenApiExample>(StringComparer.Ordinal);
 1625        AddComponent(Document.Components.Examples, name,
 1626                        example, ifExists,
 1627                        OpenApiComponentKind.Examples);
 1528    }
 29    /// <summary>
 30    /// Tries to add an example to the given examples dictionary based on the provided attribute.
 31    /// </summary>
 32    /// <param name="examples">The dictionary of examples to add to.</param>
 33    /// <param name="attribute">The example attribute containing reference details.</param>
 34    /// <returns>True if the example was added successfully; otherwise, false.</returns>
 35    /// <exception cref="InvalidOperationException"></exception>
 36    private bool TryAddExample(IDictionary<string, IOpenApiExample>? examples, IOpenApiExampleAttribute attribute)
 37    {
 38        // If no examples dictionary, cannot add
 439        if (examples is null)
 40        {
 041            return false;
 42        }
 43        // Try to get the example from inline components first
 444        if (TryGetInline(name: attribute.ReferenceId, kind: OpenApiComponentKind.Examples, out OpenApiExample? example))
 45        {
 46            // If InlineComponents, clone the example
 147            return examples.TryAdd(attribute.Key, example!.Clone());
 48        }
 349        else if (TryGetComponent(name: attribute.ReferenceId, kind: OpenApiComponentKind.Examples, out example))
 50        {
 51            // if in main components, reference it or clone based on Inline flag
 152            IOpenApiExample oaExample = attribute.Inline ? example!.Clone() : new OpenApiExampleReference(attribute.Refe
 153            return examples.TryAdd(attribute.Key, oaExample);
 54        }
 255        else if (attribute.Inline)
 56        {
 157            throw new InvalidOperationException($"Inline example component with ID '{attribute.ReferenceId}' not found."
 58        }
 159        return false;
 60    }
 61
 62    /// <summary>
 63    /// Creates a new OpenApiExample object.
 64    /// </summary>
 65    /// <param name="summary">Creates a new OpenApiExample object.</param>
 66    /// <param name="description">The description of the example.</param>
 67    /// <param name="extensions">The extensions for the example.</param>
 68    /// <returns>A new instance of OpenApiExample.</returns>
 69    private OpenApiExample NewOpenApiExample(
 70               string summary,
 71               string? description,
 72               IDictionary? extensions)
 73    {
 074        var example = new OpenApiExample
 075        {
 076            Summary = summary
 077        };
 78
 079        if (!string.IsNullOrWhiteSpace(description))
 80        {
 081            example.Description = description;
 82        }
 83        // Extensions
 084        example.Extensions = BuildExtensions(extensions);
 85
 086        return example;
 87    }
 88
 89    /// <summary>
 90    /// Creates a new OpenApiExample object.
 91    /// </summary>
 92    /// <param name="summary">Creates a new OpenApiExample object.</param>
 93    /// <param name="description">The description of the example.</param>
 94    /// <param name="value">The value of the example.</param>
 95    /// <param name="extensions">The extensions for the example.</param>
 96    /// <returns>A new instance of OpenApiExample.</returns>
 97    public OpenApiExample NewOpenApiExample(
 98           string summary,
 99           string? description,
 100           object? value,
 101           IDictionary? extensions)
 102    {
 0103        var example = NewOpenApiExample(
 0104               summary: summary,
 0105               description: description,
 0106               extensions: extensions);
 107
 108        // AllowNull: treat null as null JsonNode
 0109        example.Value = OpenApiJsonNodeFactory.ToNode(value);
 110        // return example
 0111        return example;
 112    }
 113
 114    /// <summary>
 115    /// Creates a new OpenApiExample object. Using ExternalValue
 116    /// </summary>
 117    /// <param name="summary">Creates a new OpenApiExample object.</param>
 118    /// <param name="description">The description of the example.</param>
 119    /// <param name="externalValue">The external value of the example.</param>
 120    /// <param name="extensions">The extensions for the example.</param>
 121    /// <returns>A new instance of OpenApiExample.</returns>
 122    public OpenApiExample NewOpenApiExternalExample(
 123               string summary,
 124               string? description,
 125               string? externalValue,
 126               IDictionary? extensions)
 127    {
 0128        var example = NewOpenApiExample(
 0129                summary: summary,
 0130                description: description,
 0131                extensions: extensions);
 132
 0133        example.ExternalValue = externalValue;
 134
 135        // return example
 0136        return example;
 137    }
 138
 139    /// <summary>
 140    /// Creates a new OpenApiExample object.
 141    /// </summary>
 142    /// <param name="summary">Creates a new OpenApiExample object.</param>
 143    /// <param name="description">The description of the example.</param>
 144    /// <param name="dataValue">The data value of the example.</param>
 145    /// <param name="serializedValue">The serialized value of the example.</param>
 146    /// <param name="extensions">The extensions for the example.</param>
 147    /// <returns>A new instance of OpenApiExample.</returns>
 148    public OpenApiExample NewOpenApiExample(
 149               string summary,
 150               string? description,
 151               object? dataValue,
 152               string? serializedValue,
 153               IDictionary? extensions)
 154    {
 0155        var example = NewOpenApiExample(
 0156               summary: summary,
 0157               description: description,
 0158               extensions: extensions);
 159
 0160        example.DataValue = OpenApiJsonNodeFactory.ToNode(dataValue);
 0161        if (!string.IsNullOrWhiteSpace(serializedValue))
 162        {
 0163            example.SerializedValue = serializedValue;
 164        }
 165
 166        // return example
 0167        return example;
 168    }
 169}

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Headers.cs

#LineLine coverage
 1using System.Collections;
 2using Kestrun.Hosting.Options;
 3using Microsoft.OpenApi;
 4
 5namespace Kestrun.OpenApi;
 6
 7/// <summary>
 8/// Generates OpenAPI v2 (Swagger) documents from C# types decorated with OpenApiSchema attributes.
 9/// </summary>
 10public partial class OpenApiDocDescriptor
 11{
 12    /// <summary>
 13    /// Creates a new OpenApiHeader with the specified properties.
 14    /// </summary>
 15    /// <param name="description">The description of the header.</param>
 16    /// <param name="required">Indicates whether the header is required.</param>
 17    /// <param name="deprecated">Indicates whether the header is deprecated.</param>
 18    /// <param name="allowEmptyValue">Indicates whether empty values are allowed.</param>
 19    /// <param name="style">The style of the header.</param>
 20    /// <param name="explode">Indicates whether the header should be exploded.</param>
 21    /// <param name="allowReserved">Indicates whether the header allows reserved characters.</param>
 22    /// <param name="example">An example of the header's value.</param>
 23    /// <param name="examples">A collection of examples for the header.</param>
 24    /// <param name="schema">The schema of the header.</param>
 25    /// <param name="content">The content of the header.</param>
 26    /// <param name="extensions">A collection of extensions for the header.</param>
 27    /// <returns>A new instance of OpenApiHeader with the specified properties.</returns>
 28    /// <exception cref="InvalidOperationException">Thrown when header examples keys or values are invalid.</exception>
 29    public OpenApiHeader NewOpenApiHeader(
 30        string? description = null,
 31        bool required = false,
 32        bool deprecated = false,
 33        bool allowEmptyValue = false,
 34        ParameterStyle? style = null,
 35        bool explode = false,
 36        bool allowReserved = false,
 37        object? example = null,
 38        Hashtable? examples = null,
 39        Type? schema = null,
 40        IDictionary? content = null,
 41        IDictionary? extensions = null
 42        )
 43    {
 744        schema = ResolveHeaderSchema(schema, content);
 745        ThrowIfBothSchemaAndContentProvided(schema, content);
 46
 647        var header = new OpenApiHeader
 648        {
 649            Description = string.IsNullOrWhiteSpace(description) ? null : description,
 650            Required = required,
 651            Deprecated = deprecated,
 652            AllowEmptyValue = allowEmptyValue,
 653            Style = style,
 654            Explode = explode,
 655            AllowReserved = allowReserved,
 656            Example = OpenApiJsonNodeFactory.ToNode(example)
 657        };
 58
 659        ApplyHeaderSchema(header, schema);
 660        ApplyHeaderExamples(header, examples);
 461        ApplyHeaderContent(header, content);
 462        header.Extensions = BuildExtensions(extensions);
 463        return header;
 64    }
 65
 66    /// <summary>
 67    /// Resolves the schema for an OpenApiHeader.
 68    /// </summary>
 69    /// <param name="schema">The schema of the header.</param>
 70    /// <param name="content">The content of the header.</param>
 71    /// <returns>The resolved schema type.</returns>
 72    private static Type? ResolveHeaderSchema(Type? schema, IDictionary? content)
 73    {
 774        return schema is null && content is null
 775            ? typeof(string)
 776            : schema;
 77    }
 78
 79    /// <summary>
 80    /// Throws an exception if both schema and content are provided for an OpenApiHeader.
 81    /// </summary>
 82    /// <param name="schema">The schema of the header.</param>
 83    /// <param name="content">The content of the header.</param>
 84    /// <exception cref="InvalidOperationException">Thrown when both schema and content are provided.</exception>
 85    private static void ThrowIfBothSchemaAndContentProvided(Type? schema, IDictionary? content)
 86    {
 787        if (schema is not null && content is not null)
 88        {
 189            throw new InvalidOperationException("Cannot specify both schema and content for an OpenApiHeader.");
 90        }
 691    }
 92
 93    /// <summary>
 94    /// Applies schema to the given OpenApiHeader.
 95    /// </summary>
 96    /// <param name="header">The OpenApiHeader to which the schema will be applied.</param>
 97    /// <param name="schema">The schema to apply to the header.</param>
 98    private void ApplyHeaderSchema(OpenApiHeader header, Type? schema)
 99    {
 6100        if (schema is not null)
 101        {
 5102            header.Schema = InferPrimitiveSchema(schema);
 103        }
 6104    }
 105
 106    /// <summary>
 107    /// Applies examples to the given OpenApiHeader from a PowerShell hashtable.
 108    /// </summary>
 109    /// <param name="header">The OpenApiHeader to which examples will be applied.</param>
 110    /// <param name="examples">A hashtable representing the examples to apply.</param>
 111    /// <exception cref="InvalidOperationException">Thrown when example keys are not strings or values are invalid.</exc
 112    private void ApplyHeaderExamples(OpenApiHeader header, Hashtable? examples)
 113    {
 114        // Multi examples from PowerShell hashtable
 6115        if (examples is null || examples.Count == 0)
 116        {
 2117            return;
 118        }
 119
 4120        header.Examples ??= new Dictionary<string, IOpenApiExample>(StringComparer.Ordinal);
 121
 14122        foreach (var rawKey in examples.Keys)
 123        {
 4124            if (rawKey is not string key)
 125            {
 1126                throw new InvalidOperationException("Header examples keys must be strings.");
 127            }
 128
 3129            header.Examples[key] = ResolveHeaderExampleValue(examples[key]);
 130        }
 2131    }
 132
 133    /// <summary>
 134    /// Resolves an example value for a header example entry.
 135    /// </summary>
 136    /// <param name="value">The example value, which can be an IOpenApiExample or a reference string.</param>
 137    /// <returns>The resolved IOpenApiExample.</returns>
 138    /// <exception cref="InvalidOperationException">Thrown when the example value is invalid or not found.</exception>
 139    private IOpenApiExample ResolveHeaderExampleValue(object? value)
 140    {
 3141        if (value is IOpenApiExample example)
 142        {
 0143            return example;
 144        }
 145
 3146        if (value is string exampleRef)
 147        {
 2148            if (TryGetInline(name: exampleRef, kind: OpenApiComponentKind.Examples, out OpenApiExample? inlineExample))
 149            {
 150                // If InlineComponents, clone the example
 1151                return inlineExample!.Clone();
 152            }
 153
 1154            if (TryGetComponent(name: exampleRef, kind: OpenApiComponentKind.Examples, out OpenApiExample? componentExam
 155            {
 156                // if in main components, reference it
 157                _ = componentExample;
 1158                return new OpenApiExampleReference(exampleRef);
 159            }
 160
 0161            throw new InvalidOperationException(
 0162                $"Example with ReferenceId '{exampleRef}' not found in components or inline components.");
 163        }
 164
 1165        throw new InvalidOperationException(
 1166            "Header examples values must be OpenApiExample or OpenApiExampleReference instances or example reference nam
 167    }
 168
 169    /// <summary>
 170    /// Applies content to the given OpenApiHeader from a PowerShell hashtable.
 171    /// </summary>
 172    /// <param name="header">The OpenApiHeader to which content will be applied.</param>
 173    /// <param name="content">A hashtable representing the content to apply.</param>
 174    /// <exception cref="InvalidOperationException">Thrown when content keys are not valid media type strings.</exceptio
 175    private void ApplyHeaderContent(OpenApiHeader header, IDictionary? content)
 176    {
 177        // Header content (media type map) from PowerShell hashtable
 4178        if (content is null || content.Count == 0)
 179        {
 3180            return;
 181        }
 182
 1183        header.Content ??= new Dictionary<string, IOpenApiMediaType>(StringComparer.Ordinal);
 184
 4185        foreach (var rawKey in content.Keys)
 186        {
 1187            if (rawKey is not string key)
 188            {
 0189                throw new InvalidOperationException("Header content keys must be media type strings.");
 190            }
 191
 1192            header.Content[key] = ResolveHeaderMediaTypeValue(content[key]);
 193        }
 1194    }
 195
 196    /// <summary>
 197    /// Resolves a media type value for a header content entry.
 198    /// </summary>
 199    /// <param name="value">The media type value, which can be an IOpenApiMediaType or a reference string.</param>
 200    /// <returns>The resolved IOpenApiMediaType.</returns>
 201    /// <exception cref="InvalidOperationException">Thrown when the media type value is invalid or not found.</exception
 202    private IOpenApiMediaType ResolveHeaderMediaTypeValue(object? value)
 203    {
 1204        if (value is IOpenApiMediaType mediaType)
 205        {
 0206            return mediaType;
 207        }
 208
 1209        if (value is string mediaRef)
 210        {
 1211            if (TryGetInline(name: mediaRef, kind: OpenApiComponentKind.MediaTypes, out OpenApiMediaType? inlineMediaTyp
 212            {
 213                // If InlineComponents, clone the media type
 0214                return inlineMediaType!.Clone();
 215            }
 216
 1217            if (TryGetComponent(name: mediaRef, kind: OpenApiComponentKind.MediaTypes, out OpenApiMediaType? componentMe
 218            {
 219                // if in main components, clone it
 1220                return componentMediaType!.Clone();
 221            }
 222
 0223            throw new InvalidOperationException(
 0224                $"MediaType with ReferenceId '{mediaRef}' not found in components or inline components.");
 225        }
 226
 0227        throw new InvalidOperationException(
 0228            "Header content values must be OpenApiMediaType instances or media type reference name strings.");
 229    }
 230
 231    /// <summary>
 232    /// Adds an OpenApiHeader component to the OpenAPI document.
 233    /// </summary>
 234    /// <param name="name"> The name of the header component. </param>
 235    /// <param name="header"> The OpenApiHeader object to add. </param>
 236    /// <param name="ifExists"> Conflict resolution strategy if the component already exists. </param>
 237    public void AddComponentHeader(
 238    string name,
 239    OpenApiHeader header,
 240    OpenApiComponentConflictResolution ifExists = OpenApiComponentConflictResolution.Overwrite)
 241    {
 0242        Document.Components ??= new OpenApiComponents();
 243        // Ensure headers dictionary exists
 0244        Document.Components.Headers ??= new Dictionary<string, IOpenApiHeader>(StringComparer.Ordinal);
 0245        AddComponent(Document.Components.Headers, name,
 0246                        header, ifExists,
 0247                        OpenApiComponentKind.Headers);
 0248    }
 249
 250    /// <summary>
 251    /// Applies an OpenApiResponseHeaderAttribute to the given OpenAPIPathMetadata.
 252    /// </summary>
 253    /// <param name="metadata">The OpenAPIPathMetadata to apply the attribute to.</param>
 254    /// <param name="attribute">The OpenApiResponseHeaderAttribute containing header information.</param>
 255    /// <exception cref="InvalidOperationException">Thrown when the attribute is missing required information or is inva
 256    private void ApplyResponseHeaderAttribute(OpenAPIPathMetadata metadata, IOpenApiResponseHeaderAttribute attribute)
 257    {
 0258        if (attribute.StatusCode is null)
 259        {
 0260            throw new InvalidOperationException($"{attribute.GetType().Name} must have a StatusCode specified to associa
 261        }
 0262        if (attribute.Key is null)
 263        {
 0264            throw new InvalidOperationException("Response header attributes must have a Key specified to define the head
 265        }
 0266        metadata.Responses ??= [];
 0267        var response = metadata.Responses.TryGetValue(attribute.StatusCode, out var value) ? value as OpenApiResponse : 
 0268        if (response is not null && CreateResponseFromAttribute(attribute, response))
 269        {
 0270            _ = metadata.Responses.TryAdd(attribute.StatusCode, response);
 271        }
 0272    }
 273
 274    /// <summary>
 275    /// Tries to add a header to the given headers dictionary based on the provided attribute.
 276    /// </summary>
 277    /// <param name="headers"> The dictionary of headers to add to. </param>
 278    /// <param name="attribute"> The attribute containing header reference information. </param>
 279    /// <returns> True if the header was added successfully; otherwise, false. </returns>
 280    /// <exception cref="InvalidOperationException"> Thrown if the header reference ID is not found in components or inl
 281    private bool TryAddHeader(IDictionary<string, IOpenApiHeader> headers, OpenApiResponseHeaderRefAttribute attribute)
 282    {
 0283        if (TryGetInline(name: attribute.ReferenceId, kind: OpenApiComponentKind.Headers, out OpenApiHeader? header))
 284        {
 285            // If InlineComponents, clone the header
 0286            return headers.TryAdd(attribute.Key, header!.Clone());
 287        }
 0288        else if (TryGetComponent(name: attribute.ReferenceId, kind: OpenApiComponentKind.Headers, out header))
 289        {
 290            // if in main components, reference it or clone based on Inline flag
 0291            IOpenApiHeader oaHeader = attribute.Inline ? header!.Clone() : new OpenApiHeaderReference(attribute.Referenc
 0292            return headers.TryAdd(attribute.Key, oaHeader);
 293        }
 0294        else if (attribute.Inline)
 295        {
 0296            throw new InvalidOperationException($"Inline header component with ID '{attribute.ReferenceId}' not found.")
 297        }
 0298        return false;
 299    }
 300
 301    /// <summary>
 302    /// Tries to get an OpenApiHeader item from inline or main components.
 303    /// </summary>
 304    /// <param name="headerName"> The name of the header to retrieve. </param>
 305    /// <param name="header"> The retrieved OpenApiHeader if found; otherwise, null. </param>
 306    /// <returns> True if the header was found; otherwise, false. </returns>
 307    private bool TryGetHeaderItem(string headerName, out OpenApiHeader? header)
 308    {
 0309        if (TryGetInline(name: headerName, kind: OpenApiComponentKind.Headers, out header))
 310        {
 0311            return true;
 312        }
 0313        else if (TryGetComponent(name: headerName, kind: OpenApiComponentKind.Headers, out header))
 314        {
 0315            return true;
 316        }
 0317        return false;
 318    }
 319}

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Helper.cs

#LineLine coverage
 1using System.Collections;
 2using Kestrun.Logging;
 3using Microsoft.OpenApi;
 4
 5namespace Kestrun.OpenApi;
 6
 7/// <summary>
 8/// Helper methods for accessing OpenAPI document components.
 9/// </summary>
 10public partial class OpenApiDocDescriptor
 11{
 12    private IOpenApiSchema GetSchema(string id)
 13    {
 014        ArgumentException.ThrowIfNullOrWhiteSpace(id);
 15        // Look up schema in components
 016        return Document.Components?.Schemas is { } schemas
 017               && schemas.TryGetValue(id, out var p)
 018               && p is IOpenApiSchema op
 019            ? op
 020            : throw new InvalidOperationException($"Schema '{id}' not found.");
 21    }
 22
 23    private OpenApiParameter GetParameter(string id)
 24    {
 025        ArgumentException.ThrowIfNullOrWhiteSpace(id);
 26        // Look up parameter in components
 027        return Document.Components?.Parameters is { } parameters
 028               && parameters.TryGetValue(id, out var p)
 029               && p is OpenApiParameter op
 030            ? op
 031            : throw new InvalidOperationException($"Parameter '{id}' not found.");
 32    }
 33
 34    private OpenApiRequestBody GetRequestBody(string id)
 35    {
 036        ArgumentException.ThrowIfNullOrWhiteSpace(id);
 37        // Look up request body in components
 038        return Document.Components?.RequestBodies is { } requestBodies
 039               && requestBodies.TryGetValue(id, out var p)
 040               && p is OpenApiRequestBody op
 041            ? op
 042            : throw new InvalidOperationException($"RequestBody '{id}' not found.");
 43    }
 44
 45    private OpenApiHeader GetHeader(string id)
 46    {
 047        ArgumentException.ThrowIfNullOrWhiteSpace(id);
 48        // Look up header in components
 049        return Document.Components?.Headers is { } headers
 050               && headers.TryGetValue(id, out var p)
 051               && p is OpenApiHeader op
 052            ? op
 053            : throw new InvalidOperationException($"Header '{id}' not found.");
 54    }
 55
 56    private OpenApiResponse GetResponse(string id)
 57    {
 058        ArgumentException.ThrowIfNullOrWhiteSpace(id);
 59        // Look up response in components
 060        return Document.Components?.Responses is { } responses
 061               && responses.TryGetValue(id, out var p)
 062               && p is OpenApiResponse op
 063            ? op
 064            : throw new InvalidOperationException($"Response '{id}' not found.");
 65    }
 66
 67    private bool ComponentSchemasExists(string id) =>
 4868        Document.Components?.Schemas?.ContainsKey(id) == true;
 69
 70    private bool ComponentRequestBodiesExists(string id) =>
 071        Document.Components?.RequestBodies?.ContainsKey(id) == true;
 72
 73    private bool ComponentResponsesExists(string id) =>
 074        Document.Components?.Responses?.ContainsKey(id) == true;
 75
 76    private bool ComponentParametersExists(string id) =>
 077        Document.Components?.Parameters?.ContainsKey(id) == true;
 78
 79    private bool ComponentExamplesExists(string id) =>
 080        Document.Components?.Examples?.ContainsKey(id) == true;
 81
 82    private bool ComponentHeadersExists(string id) =>
 083        Document.Components?.Headers?.ContainsKey(id) == true;
 84    private bool ComponentCallbacksExists(string id) =>
 085        Document.Components?.Callbacks?.ContainsKey(id) == true;
 86
 87    private bool ComponentLinksExists(string id) =>
 088        Document.Components?.Links?.ContainsKey(id) == true;
 89    private bool ComponentPathItemsExists(string id) =>
 090        Document.Components?.PathItems?.ContainsKey(id) == true;
 91
 92    /// <summary>
 93    /// Normalizes a raw extensions dictionary into OpenAPI extensions.
 94    /// </summary>
 95    /// <param name="extensions">The raw extensions dictionary to normalize.</param>
 96    /// <returns>A normalized dictionary of OpenAPI extensions, or null if no valid extensions exist.</returns>
 97    private Dictionary<string, IOpenApiExtension>? BuildExtensions(
 98    IDictionary? extensions)
 99    {
 18100        if (extensions is null || extensions.Count == 0)
 101        {
 12102            return null;
 103        }
 104
 6105        Dictionary<string, IOpenApiExtension>? result = null;
 106
 66107        foreach (DictionaryEntry entry in extensions)
 108        {
 27109            var rawKey = entry.Key?.ToString();
 27110            if (string.IsNullOrWhiteSpace(rawKey))
 111            {
 112                continue;
 113            }
 114
 115            string key;
 22116            if (rawKey.StartsWith("x-", StringComparison.Ordinal))
 117            {
 22118                key = rawKey;
 119            }
 120            else
 121            {
 0122                Host.Logger.WarningSanitized("OpenAPI extension '{rawKey}' is invalid. Extension names must start with '
 0123                continue;
 124            }
 125
 22126            var node = OpenApiJsonNodeFactory.ToNode(entry.Value);
 22127            if (node is null)
 128            {
 6129                Host.Logger.WarningSanitized("OpenAPI extension '{key}' has a null value and will be skipped.", key);
 6130                continue;
 131            }
 132
 16133            result ??= new Dictionary<string, IOpenApiExtension>(StringComparer.Ordinal);
 16134            result[key] = new JsonNodeExtension(node);
 135        }
 136
 6137        return result;
 138    }
 139}

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Info.cs

#LineLine coverage
 1using System.Collections;
 2using Microsoft.OpenApi;
 3
 4namespace Kestrun.OpenApi;
 5
 6/// <summary>
 7/// Helper methods for accessing OpenAPI document components.
 8/// </summary>
 9public partial class OpenApiDocDescriptor
 10{
 11    /// <summary>
 12    /// Creates an OpenApiExternalDocs object with optional extensions.
 13    /// </summary>
 14    /// <param name="url">The URL for the external documentation.</param>
 15    /// <param name="description">An optional description of the external documentation.</param>
 16    /// <param name="extensions">Optional extensions for the external documentation.</param>
 17    /// <returns>An OpenApiExternalDocs object.</returns>
 18    /// <exception cref="ArgumentException">Thrown when the URL is null, empty, or whitespace.</exception>
 19    public OpenApiExternalDocs CreateExternalDocs(
 20        Uri url,
 21        string? description = null,
 22        IDictionary? extensions = null)
 23    {
 224        var docs = new OpenApiExternalDocs
 225        {
 226            Url = url,
 227            Description = description,
 228            Extensions = BuildExtensions(extensions)
 229        };
 30
 231        return docs;
 32    }
 33    /// <summary>
 34    /// Creates an OpenApiExternalDocs object from a URL string with optional extensions.
 35    /// </summary>
 36    /// <param name="url">The URL for the external documentation.</param>
 37    /// <param name="description">An optional description of the external documentation.</param>
 38    /// <param name="extensions">Optional extensions for the external documentation.</param>
 39    /// <returns>An OpenApiExternalDocs object.</returns>
 40    /// <exception cref="ArgumentException">Thrown when the URL is null, empty, or whitespace.</exception>
 41    public OpenApiExternalDocs CreateExternalDocs(
 42            string url,
 43            string? description = null,
 44            IDictionary? extensions = null)
 45    {
 146        if (string.IsNullOrWhiteSpace(url))
 47        {
 148            throw new ArgumentException("ExternalDocs url is required.", nameof(url));
 49        }
 50        // Reuse the other overload
 051        return CreateExternalDocs(new Uri(url, UriKind.RelativeOrAbsolute), description, extensions);
 52    }
 53
 54    /// <summary>
 55    /// Creates an OpenApiContact object with optional extensions.
 56    /// </summary>
 57    /// <param name="name">The name of the contact person or organization.</param>
 58    /// <param name="url">The URL of the contact person or organization.</param>
 59    /// <param name="email">The email address of the contact person or organization.</param>
 60    /// <param name="extensions">Optional extensions for the contact information.</param>
 61    /// <returns>An OpenApiContact object.</returns>
 62    public OpenApiContact CreateInfoContact(
 63            string? name = null,
 64            Uri? url = null,
 65            string? email = null,
 66            IDictionary? extensions = null)
 67    {
 368        var contact = new OpenApiContact
 369        {
 370            Extensions = BuildExtensions(extensions)
 371        };
 72
 373        if (url != null)
 74        {
 375            contact.Url = url;
 76        }
 77
 378        if (!string.IsNullOrEmpty(name))
 79        {
 380            contact.Name = name;
 81        }
 82
 383        if (!string.IsNullOrEmpty(email))
 84        {
 385            contact.Email = email;
 86        }
 87
 388        return contact;
 89    }
 90}

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Inline.cs

#LineLine coverage
 1using System.Diagnostics.CodeAnalysis;
 2using Microsoft.OpenApi;
 3
 4namespace Kestrun.OpenApi;
 5
 6public partial class OpenApiDocDescriptor
 7{
 8    /// <summary>
 9    /// Adds an inline example to the OpenAPI document.
 10    /// </summary>
 11    /// <param name="name">The name of the inline example.</param>
 12    /// <param name="example">The inline example to add.</param>
 13    /// <param name="ifExists">Specifies the behavior if an example with the same name already exists.</param>
 14    /// <exception cref="InvalidOperationException">Thrown if an example with the same name already exists and ifExists 
 15    /// <exception cref="ArgumentOutOfRangeException">Thrown if the ifExists parameter has an invalid value.</exception>
 16    public void AddInlineExample(
 17    string name,
 18    OpenApiExample example,
 19    OpenApiComponentConflictResolution ifExists = OpenApiComponentConflictResolution.Overwrite)
 20    {
 321        InlineComponents.Examples ??= new Dictionary<string, IOpenApiExample>(StringComparer.Ordinal);
 322        AddComponent(InlineComponents.Examples, name,
 323                    example, ifExists,
 324                    OpenApiComponentKind.Examples);
 225    }
 26
 27    /// <summary>
 28    /// Adds an inline link to the OpenAPI document.
 29    /// </summary>
 30    /// <param name="name">The name of the inline link.</param>
 31    /// <param name="link">The inline link to add.</param>
 32    /// <param name="ifExists">Specifies the behavior if a link with the same name already exists.</param>
 33    /// <exception cref="InvalidOperationException">Thrown if a link with the same name already exists and ifExists is s
 34    /// <exception cref="ArgumentOutOfRangeException">Thrown if the ifExists parameter has an invalid value.</exception>
 35    public void AddInlineLink(
 36        string name,
 37        OpenApiLink link,
 38        OpenApiComponentConflictResolution ifExists = OpenApiComponentConflictResolution.Overwrite)
 39    {
 140        InlineComponents.Links ??= new Dictionary<string, IOpenApiLink>(StringComparer.Ordinal);
 141        AddComponent(InlineComponents.Links, name,
 142                           link, ifExists,
 143                           OpenApiComponentKind.Links);
 144    }
 45
 46    /// <summary>
 47    /// Adds a component to the inline components of the OpenAPI document.
 48    /// </summary>
 49    /// <typeparam name="T">The type of the component to add. </typeparam>
 50    /// <param name="components">The dictionary of components to which the new component will be added.</param>
 51    /// <param name="name">The name of the component to add.</param>
 52    /// <param name="value">The component value to add.</param>
 53    /// <param name="ifExists">Specifies the behavior if a component with the same name already exists.</param>
 54    /// <param name="componentKind">The kind of component being added.</param>
 55    /// <exception cref="InvalidOperationException">Thrown if a component with the same name already exists and ifExists
 56    /// <exception cref="ArgumentOutOfRangeException">Thrown if the ifExists parameter has an invalid value.</exception>
 57    private static void AddComponent<T>(
 58        IDictionary<string, T> components,
 59        string name,
 60        T value,
 61        OpenApiComponentConflictResolution ifExists,
 62        OpenApiComponentKind componentKind)
 63        where T : class
 64    {
 2465        ArgumentNullException.ThrowIfNull(components);
 2466        ArgumentNullException.ThrowIfNull(name);
 2467        ArgumentNullException.ThrowIfNull(value);
 68
 69        switch (ifExists)
 70        {
 71            case OpenApiComponentConflictResolution.Error:
 272                if (!components.TryAdd(name, value))
 73                {
 274                    var kind = componentKind.ToInlineLabel();
 275                    throw new InvalidOperationException(
 276                        $"A component {kind} named '{name}' already exists.");
 77                }
 078                return;
 79
 80            case OpenApiComponentConflictResolution.Ignore:
 281                _ = components.TryAdd(name, value);
 282                return;
 83
 84            case OpenApiComponentConflictResolution.Overwrite:
 1985                components[name] = value;
 1986                return;
 87
 88            default:
 189                throw new ArgumentOutOfRangeException(nameof(ifExists), ifExists, null);
 90        }
 91    }
 92
 93    /// <summary>
 94    /// Tries to retrieve an OpenAPI component by name and kind.
 95    /// </summary>
 96    /// <typeparam name="T">The expected OpenAPI component type.</typeparam>
 97    /// <param name="name">The component name.</param>
 98    /// <param name="kind">The OpenAPI component kind.</param>
 99    /// <param name="value">When this method returns <c>true</c>, contains the component.</param>
 100    /// <returns><c>true</c> if the component exists; otherwise, <c>false</c>.</returns>
 101    public bool TryGetComponent<T>(
 102     string name,
 103     OpenApiComponentKind kind,
 104     out T? value)
 14105     where T : class => TryGetFromComponents(Document.Components, name, kind, out value);
 106
 107    /// <summary>
 108    /// Tries to retrieve an inline OpenAPI component by name and kind.
 109    /// </summary>
 110    /// <typeparam name="T"> The expected OpenAPI component type.</typeparam>
 111    /// <param name="name">The component name.</param>
 112    /// <param name="kind">The OpenAPI component kind.</param>
 113    /// <param name="value">When this method returns <c>true</c>, contains the component.</param>
 114    /// <returns><c>true</c> if the component exists; otherwise, <c>false</c>.</returns>
 115    public bool TryGetInline<T>(
 116        string name,
 117        OpenApiComponentKind kind,
 118        out T? value)
 18119        where T : class => TryGetFromComponents(InlineComponents, name, kind, out value);
 120
 121    /// <summary>
 122    /// Tries to retrieve an OpenAPI component from the specified components object.
 123    /// </summary>
 124    /// <typeparam name="T"> The expected OpenAPI component type.</typeparam>
 125    /// <param name="components">The OpenAPI components object.</param>
 126    /// <param name="name">The component name.</param>
 127    /// <param name="kind">The OpenAPI component kind.</param>
 128    /// <param name="value">When this method returns <c>true</c>, contains the component.</param>
 129    /// <returns><c>true</c> if the component exists; otherwise, <c>false</c>.</returns>
 130    /// <exception cref="ArgumentOutOfRangeException"></exception>
 131    private static bool TryGetFromComponents<T>(
 132         OpenApiComponents? components,
 133         string name,
 134         OpenApiComponentKind kind,
 135         out T? value)
 136         where T : class
 137    {
 32138        ArgumentNullException.ThrowIfNull(name);
 32139        value = null;
 140
 32141        if (components is null)
 142        {
 0143            return false;
 144        }
 145
 32146        ValidateComponentType<T>(kind);
 147
 148        // NOTE: We intentionally avoid trying to up-cast IDictionary<string, TSpecific>
 149        // to IDictionary<string, object> because generic dictionaries are invariant.
 30150        return kind switch
 30151        {
 0152            OpenApiComponentKind.Schemas => TryGetAndCast(components.Schemas, name, out value),
 0153            OpenApiComponentKind.Responses => TryGetAndCast(components.Responses, name, out value),
 4154            OpenApiComponentKind.Parameters => TryGetAndCast(components.Parameters, name, out value),
 14155            OpenApiComponentKind.Examples => TryGetAndCast(components.Examples, name, out value),
 2156            OpenApiComponentKind.RequestBodies => TryGetAndCast(components.RequestBodies, name, out value),
 0157            OpenApiComponentKind.Headers => TryGetAndCast(components.Headers, name, out value),
 0158            OpenApiComponentKind.SecuritySchemes => TryGetAndCast(components.SecuritySchemes, name, out value),
 8159            OpenApiComponentKind.Links => TryGetAndCast(components.Links, name, out value),
 0160            OpenApiComponentKind.Callbacks => TryGetAndCast(components.Callbacks, name, out value),
 0161            OpenApiComponentKind.PathItems => TryGetAndCast(components.PathItems, name, out value),
 2162            OpenApiComponentKind.MediaTypes => TryGetAndCast(components.MediaTypes, name, out value),
 0163            _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null)
 30164        };
 165
 166        static bool TryGetAndCast<TSpecific>(
 167            IDictionary<string, TSpecific>? dict,
 168            string componentName,
 169            out T? component)
 170            where TSpecific : class
 171        {
 30172            if (TryGet(dict, componentName, out var specific) && specific is not null)
 173            {
 11174                component = (T)(object)specific;
 11175                return true;
 176            }
 177
 19178            component = null;
 19179            return false;
 180        }
 181    }
 182
 183    /// <summary>
 184    /// Validates that the specified type T matches the expected type for the given OpenApiComponentKind.
 185    /// </summary>
 186    /// <typeparam name="T">The expected OpenAPI component type.</typeparam>
 187    /// <param name="kind">The OpenAPI component kind.</param>
 188    /// <exception cref="ArgumentOutOfRangeException"> Thrown when the specified kind is not recognized.</exception>
 189    private static void ValidateComponentType<T>(OpenApiComponentKind kind) where T : class
 190    {
 32191        var expectedType = kind switch
 32192        {
 0193            OpenApiComponentKind.Schemas => typeof(IOpenApiSchema),
 0194            OpenApiComponentKind.Responses => typeof(OpenApiResponse),
 4195            OpenApiComponentKind.Parameters => typeof(OpenApiParameter),
 15196            OpenApiComponentKind.Examples => typeof(OpenApiExample),
 2197            OpenApiComponentKind.RequestBodies => typeof(OpenApiRequestBody),
 0198            OpenApiComponentKind.Headers => typeof(OpenApiHeader),
 0199            OpenApiComponentKind.SecuritySchemes => typeof(OpenApiSecurityScheme),
 8200            OpenApiComponentKind.Links => typeof(OpenApiLink),
 0201            OpenApiComponentKind.Callbacks => typeof(OpenApiCallback),
 0202            OpenApiComponentKind.PathItems => typeof(OpenApiPathItem),
 2203            OpenApiComponentKind.MediaTypes => typeof(OpenApiMediaType),
 1204            _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null)
 32205        };
 206
 31207        if (typeof(T) != expectedType)
 208        {
 1209            ThrowTypeMismatch<T>(kind);
 210        }
 30211    }
 212
 213    /// <summary>
 214    /// Tries to get a value from a dictionary.
 215    /// </summary>
 216    /// <typeparam name="T"> The expected OpenAPI component type.</typeparam>
 217    /// <param name="dict"> The dictionary to search.</param>
 218    /// <param name="name"> The key to look for in the dictionary.</param>
 219    /// <param name="value"> The value associated with the specified key, if found; otherwise, null.</param>
 220    /// <returns>True if the key was found; otherwise, false.</returns>
 221    private static bool TryGet<T>(
 222    IDictionary<string, T>? dict,
 223    string name,
 224    out T? value)
 225    where T : class
 226    {
 30227        if (dict is not null && dict.TryGetValue(name, out var v))
 228        {
 11229            value = v;
 11230            return true;
 231        }
 232
 19233        value = null;
 19234        return false;
 235    }
 236
 237    [DoesNotReturn]
 238    private static void ThrowTypeMismatch<T>(OpenApiComponentKind kind)
 239    {
 1240        throw new InvalidOperationException(
 1241            $"Component kind '{kind}' does not match requested type '{typeof(T).Name}'.");
 242    }
 243}

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Links.cs

#LineLine coverage
 1using System.Collections;
 2using Kestrun.Hosting.Options;
 3using Microsoft.OpenApi;
 4
 5namespace Kestrun.OpenApi;
 6
 7public partial class OpenApiDocDescriptor
 8{
 9    /// <summary>
 10    /// Adds a component link to the OpenAPI document.
 11    /// </summary>
 12    /// <param name="name">The name of the link component.</param>
 13    /// <param name="link">The link component to add.</param>
 14    /// <param name="ifExists">The conflict resolution strategy if a link with the same name already exists.</param>
 15    public void AddComponentLink(
 16        string name,
 17        OpenApiLink link,
 18        OpenApiComponentConflictResolution ifExists = OpenApiComponentConflictResolution.Overwrite)
 19    {
 420        Document.Components ??= new OpenApiComponents();
 21        // Ensure Examples dictionary exists
 422        Document.Components.Links ??= new Dictionary<string, IOpenApiLink>(StringComparer.Ordinal);
 423        AddComponent(Document.Components.Links, name,
 424                        link, ifExists,
 425                        OpenApiComponentKind.Links);
 326    }
 27
 28    private bool TryAddLink(IDictionary<string, IOpenApiLink> links, OpenApiResponseLinkRefAttribute attribute)
 29    {
 430        if (TryGetInline(name: attribute.ReferenceId, kind: OpenApiComponentKind.Links, out OpenApiLink? link))
 31        {
 32            // If InlineComponents, clone the example
 133            return links.TryAdd(attribute.Key, link!.Clone());
 34        }
 335        else if (TryGetComponent(name: attribute.ReferenceId, kind: OpenApiComponentKind.Links, out link))
 36        {
 37            // if in main components, reference it or clone based on Inline flag
 138            IOpenApiLink oaLink = attribute.Inline ? link!.Clone() : new OpenApiLinkReference(attribute.ReferenceId);
 139            return links.TryAdd(attribute.Key, oaLink);
 40        }
 241        else if (attribute.Inline)
 42        {
 143            throw new InvalidOperationException($"Inline link component with ID '{attribute.ReferenceId}' not found.");
 44        }
 145        return false;
 46    }
 47
 48    /// <summary>
 49    /// Applies an OpenApiResponseLinkRefAttribute to the specified OpenAPI path metadata.
 50    /// </summary>
 51    /// <param name="metadata">The OpenAPI path metadata to which the link attribute will be applied.</param>
 52    /// <param name="attribute">The OpenApiResponseLinkRefAttribute to apply.</param>
 53    /// <exception cref="InvalidOperationException">Thrown when the attribute is missing required properties.</exception
 54    private void ApplyResponseLinkAttribute(OpenAPIPathMetadata metadata, OpenApiResponseLinkRefAttribute attribute)
 55    {
 256        if (attribute.StatusCode is null)
 57        {
 158            throw new InvalidOperationException("OpenApiLinkAttribute must have a StatusCode specified to associate the 
 59        }
 160        if (attribute.Key is null)
 61        {
 162            throw new InvalidOperationException("OpenApiLinkRefAttribute must have a Key specified to define the link na
 63        }
 064        metadata.Responses ??= [];
 065        var response = metadata.Responses.TryGetValue(attribute.StatusCode, out var value) ? value as OpenApiResponse : 
 066        if (response is not null && CreateResponseFromAttribute(attribute, response))
 67        {
 068            _ = metadata.Responses.TryAdd(attribute.StatusCode, response);
 69        }
 070    }
 71
 72    /// <summary>
 73    /// Applies an OpenApiResponseLinkRefAttribute to the specified OpenAPI response.
 74    /// </summary>
 75    /// <param name="attribute">The OpenApiResponseLinkRefAttribute to apply.</param>
 76    /// <param name="response">The OpenAPI response to which the link attribute will be applied.</param>
 77    /// <returns>True if the link was successfully applied; otherwise, false.</returns>
 78    private bool ApplyLinkRefAttribute(OpenApiResponseLinkRefAttribute attribute, OpenApiResponse response)
 79    {
 080        response.Links ??= new Dictionary<string, IOpenApiLink>();
 81        // Clone or reference the example
 082        _ = TryAddLink(response.Links, attribute);
 83
 084        return true;
 85    }
 86
 87    /// <summary>
 88    /// Creates a new OpenApiLink instance based on the provided parameters.
 89    /// </summary>
 90    /// <param name="operationRef">Operation reference string.</param>
 91    /// <param name="operationId">Operation identifier string.</param>
 92    /// <param name="description">Description of the link.</param>
 93    /// <param name="server">Server object associated with the link.</param>
 94    /// <param name="parameters">Parameters dictionary for the link.</param>
 95    /// <param name="requestBody">Request body object or expression.</param>
 96    /// <param name="extensions">Extensions dictionary for the link.</param>
 97    /// <returns>Newly created OpenApiLink instance.</returns>
 98    /// <exception cref="ArgumentException">Thrown when both operationRef and operationId are provided.</exception>
 99    public OpenApiLink NewOpenApiLink(
 100           string? operationRef,
 101           string? operationId,
 102           string? description,
 103           OpenApiServer? server,
 104           IDictionary? parameters,
 105           object? requestBody,
 106           IDictionary? extensions)
 107    {
 0108        ValidateLinkOperation(operationRef, operationId);
 109
 0110        var link = new OpenApiLink
 0111        {
 0112            Extensions = BuildExtensions(extensions)
 0113        };
 114
 0115        ApplyLinkDescription(link, description);
 0116        ApplyLinkServer(link, server);
 0117        ApplyLinkOperation(link, operationRef, operationId);
 0118        ApplyLinkRequestBody(link, requestBody);
 0119        ApplyLinkParameters(link, parameters);
 120
 0121        return link;
 122    }
 123
 124    /// <summary>
 125    /// Validates that exactly one of <paramref name="operationRef"/> or <paramref name="operationId"/> is provided.
 126    /// </summary>
 127    /// <param name="operationRef">The operation reference string.</param>
 128    /// <param name="operationId">The operation identifier string.</param>
 129    /// <exception cref="ArgumentException">Thrown when both are provided, or when neither is provided.</exception>
 130    private static void ValidateLinkOperation(string? operationRef, string? operationId)
 131    {
 132        // Match the PS safety rule.
 0133        if (!string.IsNullOrWhiteSpace(operationRef) && !string.IsNullOrWhiteSpace(operationId))
 134        {
 0135            throw new ArgumentException("OperationId and OperationRef are mutually exclusive in an OpenAPI Link.");
 136        }
 137
 0138        if (string.IsNullOrWhiteSpace(operationRef) && string.IsNullOrWhiteSpace(operationId))
 139        {
 140            // Should be prevented by parameter sets, but keep it robust.
 0141            throw new ArgumentException("Either OperationRef or OperationId must be provided.");
 142        }
 0143    }
 144
 145    /// <summary>
 146    /// Applies the description to the link when provided.
 147    /// </summary>
 148    /// <param name="link">The link to update.</param>
 149    /// <param name="description">The description value.</param>
 150    private static void ApplyLinkDescription(OpenApiLink link, string? description)
 151    {
 0152        if (!string.IsNullOrWhiteSpace(description))
 153        {
 0154            link.Description = description;
 155        }
 0156    }
 157
 158    /// <summary>
 159    /// Applies the server to the link when provided.
 160    /// </summary>
 161    /// <param name="link">The link to update.</param>
 162    /// <param name="server">The server value.</param>
 163    private static void ApplyLinkServer(OpenApiLink link, OpenApiServer? server)
 164    {
 0165        if (server is not null)
 166        {
 0167            link.Server = server;
 168        }
 0169    }
 170
 171    /// <summary>
 172    /// Applies <see cref="OpenApiLink.OperationRef"/> or <see cref="OpenApiLink.OperationId"/>.
 173    /// </summary>
 174    /// <param name="link">The link to update.</param>
 175    /// <param name="operationRef">The operation reference string.</param>
 176    /// <param name="operationId">The operation identifier string.</param>
 177    private static void ApplyLinkOperation(OpenApiLink link, string? operationRef, string? operationId)
 178    {
 0179        if (!string.IsNullOrWhiteSpace(operationRef))
 180        {
 0181            link.OperationRef = operationRef;
 0182            return;
 183        }
 184
 0185        if (!string.IsNullOrWhiteSpace(operationId))
 186        {
 0187            link.OperationId = operationId;
 188        }
 0189    }
 190
 191    /// <summary>
 192    /// Applies the request body to the link, interpreting string values as runtime expressions.
 193    /// </summary>
 194    /// <param name="link">The link to update.</param>
 195    /// <param name="requestBody">The request body value.</param>
 196    private static void ApplyLinkRequestBody(OpenApiLink link, object? requestBody)
 197    {
 0198        if (requestBody is null)
 199        {
 0200            return;
 201        }
 202
 0203        var wrapper = new RuntimeExpressionAnyWrapper();
 204
 0205        if (requestBody is string s)
 206        {
 0207            if (string.IsNullOrWhiteSpace(s))
 208            {
 0209                return;
 210            }
 211
 0212            wrapper.Expression = RuntimeExpression.Build(s);
 0213            link.RequestBody = wrapper;
 0214            return;
 215        }
 216
 0217        wrapper.Any = OpenApiJsonNodeFactory.ToNode(requestBody);
 0218        link.RequestBody = wrapper;
 0219    }
 220
 221    /// <summary>
 222    /// Applies link parameters, interpreting string values as runtime expressions.
 223    /// </summary>
 224    /// <param name="link">The link to update.</param>
 225    /// <param name="parameters">The parameters dictionary.</param>
 226    private static void ApplyLinkParameters(OpenApiLink link, IDictionary? parameters)
 227    {
 0228        if (parameters is null || parameters.Count == 0)
 229        {
 0230            return;
 231        }
 232
 0233        link.Parameters ??= new Dictionary<string, RuntimeExpressionAnyWrapper>(StringComparer.Ordinal);
 234
 0235        foreach (DictionaryEntry entry in parameters)
 236        {
 0237            if (entry.Key is null)
 238            {
 239                continue;
 240            }
 241
 0242            var key = entry.Key.ToString();
 0243            if (string.IsNullOrWhiteSpace(key))
 244            {
 245                continue;
 246            }
 247
 0248            link.Parameters[key] = ToRuntimeExpressionAnyWrapper(entry.Value);
 249        }
 0250    }
 251
 252    /// <summary>
 253    /// Converts a value into a <see cref="RuntimeExpressionAnyWrapper"/>.
 254    /// </summary>
 255    /// <param name="value">The value to wrap.</param>
 256    /// <returns>A wrapper containing either a runtime expression or an OpenAPI JSON node.</returns>
 257    private static RuntimeExpressionAnyWrapper ToRuntimeExpressionAnyWrapper(object? value)
 258    {
 0259        var wrapper = new RuntimeExpressionAnyWrapper();
 260
 0261        if (value is string expr)
 262        {
 0263            wrapper.Expression = RuntimeExpression.Build(expr);
 264        }
 265        else
 266        {
 0267            wrapper.Any = OpenApiJsonNodeFactory.ToNode(value);
 268        }
 269
 0270        return wrapper;
 271    }
 272}

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_MergeAttributes.cs

#LineLine coverage
 1namespace Kestrun.OpenApi;
 2
 3/// <summary>
 4/// Generates OpenAPI v2 (Swagger) documents from C# types decorated with OpenApiSchema attributes.
 5/// </summary>
 6public partial class OpenApiDocDescriptor
 7{
 8    #region Schemas Attribute Merging
 9
 10    /// <summary>
 11    /// Merges multiple OpenApiPropertyAttribute instances into one.
 12    /// </summary>
 13    /// <param name="attrs">An array of OpenApiPropertyAttribute instances to merge.</param>
 14    /// <returns>A single OpenApiPropertyAttribute instance representing the merged attributes.</returns>
 15    private static OpenApiPropertyAttribute? MergeSchemaAttributes(OpenApiPropertyAttribute[] attrs)
 16    {
 017        if (attrs == null || attrs.Length == 0)
 18        {
 019            return null;
 20        }
 21
 022        if (attrs.Length == 1)
 23        {
 024            return attrs[0];
 25        }
 26
 027        var m = new OpenApiPropertyAttribute();
 28
 029        foreach (var a in attrs)
 30        {
 031            MergeStringProperties(m, a);
 032            MergeEnumAndCollections(m, a);
 033            MergeNumericProperties(m, a);
 034            MergeBooleanProperties(m, a);
 035            MergeTypeAndRequired(m, a);
 036            MergeCustomFields(m, a);
 37        }
 38
 039        return m;
 40    }
 41
 42    /// <summary>
 43    /// Merges string properties where the last non-empty value wins.
 44    /// </summary>
 45    /// <param name="merged">The merged OpenApiPropertyAttribute to update.</param>
 46    /// <param name="attr">The OpenApiPropertyAttribute to merge from.</param>
 47    private static void MergeStringProperties(OpenApiPropertyAttribute merged, OpenApiPropertyAttribute attr)
 48    {
 049        if (!string.IsNullOrWhiteSpace(attr.Title))
 50        {
 051            merged.Title = attr.Title;
 52        }
 53
 054        if (!string.IsNullOrWhiteSpace(attr.Description))
 55        {
 056            merged.Description = attr.Description;
 57        }
 58
 059        if (!string.IsNullOrWhiteSpace(attr.Format))
 60        {
 061            merged.Format = attr.Format;
 62        }
 63
 064        if (!string.IsNullOrWhiteSpace(attr.Pattern))
 65        {
 066            merged.Pattern = attr.Pattern;
 67        }
 68
 069        if (!string.IsNullOrWhiteSpace(attr.Maximum))
 70        {
 071            merged.Maximum = attr.Maximum;
 72        }
 73
 074        if (!string.IsNullOrWhiteSpace(attr.Minimum))
 75        {
 076            merged.Minimum = attr.Minimum;
 77        }
 078    }
 79
 80    /// <summary>
 81    /// Merges enum and collection properties.
 82    /// </summary>
 83    /// <param name="merged">The merged OpenApiPropertyAttribute to update.</param>
 84    /// <param name="attr">The OpenApiPropertyAttribute to merge from.</param>
 85    private static void MergeEnumAndCollections(OpenApiPropertyAttribute merged, OpenApiPropertyAttribute attr)
 86    {
 087        if (attr.Enum is { Length: > 0 })
 88        {
 089            merged.Enum = [.. merged.Enum ?? [], .. attr.Enum];
 90        }
 91
 092        if (attr.Default is not null)
 93        {
 094            merged.Default = attr.Default;
 95        }
 96
 097        if (attr.Example is not null)
 98        {
 099            merged.Example = attr.Example;
 100        }
 0101    }
 102
 103    /// <summary>
 104    /// Merges numeric properties where values >= 0 are considered explicitly set.
 105    /// </summary>
 106    /// <param name="merged">The merged OpenApiPropertyAttribute to update.</param>
 107    /// <param name="attr">The OpenApiPropertyAttribute to merge from.</param>
 108    private static void MergeNumericProperties(OpenApiPropertyAttribute merged, OpenApiPropertyAttribute attr)
 109    {
 0110        if (attr.MaxLength >= 0)
 111        {
 0112            merged.MaxLength = attr.MaxLength;
 113        }
 114
 0115        if (attr.MinLength >= 0)
 116        {
 0117            merged.MinLength = attr.MinLength;
 118        }
 119
 0120        if (attr.MaxItems >= 0)
 121        {
 0122            merged.MaxItems = attr.MaxItems;
 123        }
 124
 0125        if (attr.MinItems >= 0)
 126        {
 0127            merged.MinItems = attr.MinItems;
 128        }
 129
 0130        if (attr.MultipleOf is not null)
 131        {
 0132            merged.MultipleOf = attr.MultipleOf;
 133        }
 0134    }
 135
 136    /// <summary>
 137    /// Merges boolean properties using OR logic.
 138    /// </summary>
 139    /// <param name="merged">The merged OpenApiPropertyAttribute to update.</param>
 140    /// <param name="attr">The OpenApiPropertyAttribute to merge from.</param>
 141    private static void MergeBooleanProperties(OpenApiPropertyAttribute merged, OpenApiPropertyAttribute attr)
 142    {
 0143        merged.Nullable |= attr.Nullable;
 0144        merged.ReadOnly |= attr.ReadOnly;
 0145        merged.WriteOnly |= attr.WriteOnly;
 0146        merged.Deprecated |= attr.Deprecated;
 0147        merged.UniqueItems |= attr.UniqueItems;
 0148        merged.ExclusiveMaximum |= attr.ExclusiveMaximum;
 0149        merged.ExclusiveMinimum |= attr.ExclusiveMinimum;
 0150    }
 151
 152    /// <summary>
 153    /// Merges type and required properties.
 154    /// </summary>
 155    /// <param name="merged">The merged OpenApiPropertyAttribute to update.</param>
 156    /// <param name="attr">The OpenApiPropertyAttribute to merge from.</param>
 157    private static void MergeTypeAndRequired(OpenApiPropertyAttribute merged, OpenApiPropertyAttribute attr)
 158    {
 0159        if (attr.Type != OaSchemaType.None)
 160        {
 0161            merged.Type = attr.Type;
 162        }
 163
 0164        if (attr.RequiredProperties is { Length: > 0 })
 165        {
 0166            merged.RequiredProperties = [.. (merged.RequiredProperties ?? []).Concat(attr.RequiredProperties).Distinct()
 167        }
 0168    }
 169
 170    /// <summary>
 171    /// Merges custom fields like XML metadata.
 172    /// </summary>
 173    /// <param name="merged">The merged OpenApiPropertyAttribute to update.</param>
 174    /// <param name="attr">The OpenApiPropertyAttribute to merge from.</param>
 175    private static void MergeCustomFields(OpenApiPropertyAttribute merged, OpenApiPropertyAttribute attr)
 176    {
 0177        if (!string.IsNullOrWhiteSpace(attr.XmlName))
 178        {
 0179            merged.XmlName = attr.XmlName;
 180        }
 181
 0182        if (!string.IsNullOrWhiteSpace(attr.XmlNamespace))
 183        {
 0184            merged.XmlNamespace = attr.XmlNamespace;
 185        }
 186
 0187        if (!string.IsNullOrWhiteSpace(attr.XmlPrefix))
 188        {
 0189            merged.XmlPrefix = attr.XmlPrefix;
 190        }
 191
 0192        merged.XmlAttribute |= attr.XmlAttribute;
 0193        merged.XmlWrapped |= attr.XmlWrapped;
 0194    }
 195    #endregion
 196}

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Parameter.cs

#LineLine coverage
 1using Microsoft.OpenApi;
 2
 3namespace Kestrun.OpenApi;
 4
 5/// <summary>
 6/// Generates OpenAPI v2 (Swagger) documents from C# types decorated with OpenApiSchema attributes.
 7/// </summary>
 8public partial class OpenApiDocDescriptor
 9{
 10    /// <summary>
 11    /// Creates an OpenAPI parameter from a given attribute.
 12    /// </summary>
 13    /// <param name="attr">The attribute to create the parameter from</param>
 14    /// <param name="parameter">The OpenApiParameter object to populate</param>
 15    /// <returns>True if the parameter was created successfully, otherwise false</returns>
 16    /// <exception cref="InvalidOperationException">Thrown when an example reference cannot be embedded due to missing o
 17    private bool CreateParameterFromAttribute(KestrunAnnotation attr, OpenApiParameter parameter)
 18    {
 19        switch (attr)
 20        {
 21            case OpenApiParameterAttribute param:
 022                ApplyParameterAttribute(param, parameter);
 023                break;
 24
 25            case OpenApiExampleRefAttribute exRef:
 026                ApplyExampleRefAttribute(exRef, parameter);
 027                break;
 28
 29            default:
 030                return false; // unrecognized attribute type
 31        }
 032        return true;
 33    }
 34
 35    /// <summary>
 36    /// Applies an OpenApiParameterAttribute to an OpenApiParameter.
 37    /// </summary>
 38    /// <param name="param">The OpenApiParameterAttribute to apply</param>
 39    /// <param name="parameter">The OpenApiParameter to modify</param>
 40    private static void ApplyParameterAttribute(OpenApiParameterAttribute param, OpenApiParameter parameter)
 41    {
 042        parameter.Description = param.Description;
 043        parameter.Name = string.IsNullOrEmpty(param.Name) ? param.Key : param.Name;
 044        parameter.Required = param.Required;
 045        parameter.Deprecated = param.Deprecated;
 046        parameter.AllowEmptyValue = param.AllowEmptyValue;
 047        if (param.Explode)
 48        {
 049            parameter.Explode = param.Explode;
 50        }
 051        parameter.AllowReserved = param.AllowReserved;
 052        if (!string.IsNullOrEmpty(param.In))
 53        {
 054            parameter.In = param.In.ToOpenApiParameterLocation();
 055            if (parameter.In == ParameterLocation.Path)
 56            {
 057                parameter.Required = true; // path parameters must be required
 58            }
 59        }
 60
 061        if (param.Style is not null)
 62        {
 063            parameter.Style = param.Style.ToParameterStyle();
 64        }
 065        if (param.Example is not null)
 66        {
 067            parameter.Example = OpenApiJsonNodeFactory.ToNode(param.Example);
 68        }
 069    }
 70
 71    /// <summary>
 72    /// Applies an example reference attribute to an OpenAPI parameter.
 73    /// </summary>
 74    /// <param name="exRef">The OpenApiExampleRefAttribute to apply</param>
 75    /// <param name="parameter">The OpenApiParameter to modify</param>
 76    private void ApplyExampleRefAttribute(OpenApiExampleRefAttribute exRef, OpenApiParameter parameter)
 77    {
 078        parameter.Examples ??= new Dictionary<string, IOpenApiExample>(StringComparer.Ordinal);
 079        if (exRef.Inline)
 80        {
 081            if (Document.Components?.Examples == null || !Document.Components.Examples.TryGetValue(exRef.ReferenceId, ou
 82            {
 083                throw new InvalidOperationException($"Example reference '{exRef.ReferenceId}' cannot be embedded because
 84            }
 085            if (value is not OpenApiExample example)
 86            {
 087                throw new InvalidOperationException($"Example reference '{exRef.ReferenceId}' cannot be embedded because
 88            }
 089            parameter.Examples[exRef.Key] = example.Clone();
 90        }
 91        else
 92        {
 093            parameter.Examples[exRef.Key] = new OpenApiExampleReference(exRef.ReferenceId);
 94        }
 095    }
 96
 97    #region Parameter Component Processing
 98    /// <summary>
 99    /// Processes a parameter component annotation to create or update an OpenAPI parameter.
 100    /// </summary>
 101    /// <param name="variable">The annotated variable containing metadata about the parameter</param>
 102    /// <param name="parameterDescriptor">The parameter component annotation</param>
 103    private void ProcessParameterComponent(
 104      OpenApiComponentAnnotationScanner.AnnotatedVariable variable,
 105      OpenApiParameterComponentAttribute parameterDescriptor)
 106    {
 2107        var key = parameterDescriptor.Key ?? variable.Name;
 2108        var parameter = GetOrCreateParameterItem(key, parameterDescriptor.Inline);
 109
 2110        ApplyParameterCommonFields(parameter, parameterDescriptor);
 111
 112        // Explode defaults to true for "form" and "cookie" styles
 2113        if (parameterDescriptor.Explode || (parameter.Style is ParameterStyle.Form or ParameterStyle.Cookie))
 114        {
 2115            parameter.Explode = true;
 116        }
 117        // Set the parameter name from the variable name
 2118        parameter.Name = variable.Name;
 2119        TryApplyVariableTypeSchema(parameter, variable, parameterDescriptor);
 2120    }
 121
 122    /// <summary>
 123    /// Applies common fields from a parameter component annotation to an OpenAPI parameter.
 124    /// </summary>
 125    /// <param name="parameter">The OpenApiParameter to modify</param>
 126    /// <param name="parameterAnnotation">The parameter component annotation</param>
 127    private static void ApplyParameterCommonFields(
 128        OpenApiParameter parameter,
 129        OpenApiParameterComponentAttribute parameterAnnotation)
 130    {
 2131        parameter.AllowEmptyValue = parameterAnnotation.AllowEmptyValue;
 2132        parameter.Description = parameterAnnotation.Description;
 2133        parameter.In = parameterAnnotation.In.ToOpenApi();
 2134        parameter.Style = parameterAnnotation.Style?.ToOpenApi();
 2135        parameter.AllowReserved = parameterAnnotation.AllowReserved;
 2136        parameter.Required = parameterAnnotation.Required;
 2137        parameter.Example = OpenApiJsonNodeFactory.ToNode(parameterAnnotation.Example);
 2138        parameter.Deprecated = parameterAnnotation.Deprecated;
 2139    }
 140
 141    /// <summary>
 142    /// Tries to apply the variable type schema to an OpenAPI parameter.
 143    /// </summary>
 144    /// <param name="parameter">The OpenApiParameter to modify</param>
 145    /// <param name="variable">The annotated variable containing metadata about the parameter</param>
 146    /// <param name="parameterAnnotation">The parameter component annotation</param>
 147    private void TryApplyVariableTypeSchema(
 148         OpenApiParameter parameter,
 149       OpenApiComponentAnnotationScanner.AnnotatedVariable variable,
 150        OpenApiParameterComponentAttribute parameterAnnotation)
 151    {
 2152        if (variable.VariableType is null)
 153        {
 0154            return;
 155        }
 2156        var iSchema = InferPrimitiveSchema(variable.VariableType);
 2157        if (iSchema is OpenApiSchema schema)
 158        {
 159            //Todo: add powershell attribute support
 160            //PowerShellAttributes.ApplyPowerShellAttributes(variable.PropertyInfo, schema);
 161            // Apply any schema attributes from the parameter annotation
 2162            ApplyConcreteSchemaAttributes(parameterAnnotation, schema);
 163            // Try to set default value from the variable initial value if not already set
 2164            if (!variable.NoDefault)
 165            {
 2166                schema.Default = OpenApiJsonNodeFactory.ToNode(variable.InitialValue);
 167            }
 168        }
 169        // Decide whether to use Schema or Content based on type and parameter location
 2170        if ((PrimitiveSchemaMap.ContainsKey(variable.VariableType) &&
 2171              string.IsNullOrWhiteSpace(parameterAnnotation.ContentType)) ||
 2172             ((parameter.In == ParameterLocation.Query || parameter.In == ParameterLocation.Cookie) &&
 2173              parameter.Style == ParameterStyle.Form))
 174        {
 2175            parameter.Schema = iSchema;
 2176            return;
 177        }
 0178        var contentType = string.IsNullOrWhiteSpace(parameterAnnotation.ContentType)
 0179              ? "application/json"
 0180              : parameterAnnotation.ContentType;
 181        // Use Content
 0182        parameter.Content ??= new Dictionary<string, IOpenApiMediaType>(StringComparer.Ordinal);
 0183        parameter.Content[contentType] = new OpenApiMediaType { Schema = iSchema };
 0184    }
 185
 186    /// <summary>
 187    /// Processes a parameter example reference annotation to add an example to an OpenAPI parameter.
 188    /// </summary>
 189    /// <param name="variableName">The name of the variable associated with the parameter.</param>
 190    /// <param name="exampleRef">The example reference attribute.</param>
 191    /// <exception cref="InvalidOperationException">Thrown when the parameter does not exist, lacks schema/content, or m
 192    private void ProcessParameterExampleRef(string variableName, OpenApiParameterExampleRefAttribute exampleRef)
 193    {
 0194        if (!TryGetParameterItem(variableName, out var parameter))
 195        {
 0196            throw new InvalidOperationException($"Parameter '{variableName}' not found when trying to add example refere
 197        }
 198
 0199        ValidateParameterHasSchemaOrContent(variableName, parameter);
 200
 0201        if (parameter!.Content is null)
 202        {
 0203            AddExampleToParameterExamples(parameter, exampleRef);
 0204            return;
 205        }
 206
 0207        AddExamplesToContentMediaTypes(parameter, exampleRef, variableName);
 0208    }
 209
 210    /// <summary>
 211    /// Validates that the parameter exists and has either Schema or Content defined.
 212    /// </summary>
 213    /// <param name="variableName">The variable name associated with the parameter.</param>
 214    /// <param name="parameter">The parameter to validate.</param>
 215    /// <exception cref="InvalidOperationException">Thrown if the parameter is null or lacks both Schema and Content.</e
 216    private static void ValidateParameterHasSchemaOrContent(string variableName, OpenApiParameter? parameter)
 217    {
 0218        if (parameter is null || (parameter.Schema is null && parameter.Content is null))
 219        {
 0220            throw new InvalidOperationException($"Parameter '{variableName}' must have a schema or content defined befor
 221        }
 0222    }
 223
 224    /// <summary>
 225    /// Ensures the parameter Examples dictionary exists and attempts to add the example reference.
 226    /// </summary>
 227    /// <param name="parameter">The OpenAPI parameter to modify.</param>
 228    /// <param name="exampleRef">The example reference attribute.</param>
 229    private void AddExampleToParameterExamples(OpenApiParameter parameter, OpenApiParameterExampleRefAttribute exampleRe
 230    {
 0231        parameter.Examples ??= new Dictionary<string, IOpenApiExample>(StringComparer.Ordinal);
 0232        _ = TryAddExample(parameter.Examples, exampleRef);
 0233    }
 234
 235    /// <summary>
 236    /// Iterates the parameter's content media types and adds the example reference to each concrete media type.
 237    /// </summary>
 238    /// <param name="parameter">The OpenAPI parameter with content.</param>
 239    /// <param name="exampleRef">The example reference attribute.</param>
 240    /// <param name="variableName">The variable name used for error messages.</param>
 241    /// <exception cref="InvalidOperationException">Thrown when encountering a media type reference or an unknown media 
 242    private void AddExamplesToContentMediaTypes(OpenApiParameter parameter, IOpenApiExampleAttribute exampleRef, string 
 243    {
 0244        foreach (var iMediaType in parameter.Content!.Values)
 245        {
 0246            if (iMediaType is OpenApiMediaType mediaType)
 247            {
 0248                mediaType.Examples ??= new Dictionary<string, IOpenApiExample>(StringComparer.Ordinal);
 0249                _ = TryAddExample(mediaType.Examples, exampleRef);
 0250                continue;
 251            }
 252
 0253            if (iMediaType is OpenApiMediaTypeReference)
 254            {
 0255                throw new InvalidOperationException($"Cannot add example reference to media type reference in parameter 
 256            }
 257
 0258            throw new InvalidOperationException($"Unknown media type in parameter '{variableName}'.");
 259        }
 0260    }
 261
 262    /// <summary>
 263    /// Iterates the request body's content media types and adds the example reference to each concrete media type.
 264    /// </summary>
 265    /// <param name="requestBody">The OpenAPI request body with content.</param>
 266    /// <param name="exampleRef">The example reference attribute.</param>
 267    /// <param name="variableName">The variable name used for error messages.</param>
 268    /// <exception cref="InvalidOperationException">Thrown when encountering a media type reference or an unknown media 
 269    private void AddExamplesToContentMediaTypes(OpenApiRequestBody requestBody, IOpenApiExampleAttribute exampleRef, str
 270    {
 0271        foreach (var iMediaType in requestBody.Content!.Values)
 272        {
 273            try
 274            {
 0275                if (iMediaType is OpenApiMediaType mediaType)
 276                {
 0277                    mediaType.Examples ??= new Dictionary<string, IOpenApiExample>(StringComparer.Ordinal);
 0278                    if (!TryAddExample(mediaType.Examples, exampleRef))
 279                    {
 0280                        throw new InvalidOperationException($"Failed to add example reference '{exampleRef.ReferenceId}'
 281                    }
 0282                    continue;
 283                }
 284
 0285                if (iMediaType is OpenApiMediaTypeReference)
 286                {
 0287                    throw new InvalidOperationException($"Cannot add example reference to media type reference in reques
 288                }
 289
 0290                throw new InvalidOperationException($"Unknown media type in request body '{variableName}'.");
 291            }
 0292            catch (Exception ex)
 293            {
 0294                Host.Logger.Error("Error adding example reference to request body {variableName}: {ex.Message}", variabl
 0295            }
 296        }
 0297    }
 298
 299    /// <summary>
 300    /// Processes a PowerShell attribute to add validation constraints to an OpenAPI parameter.
 301    /// </summary>
 302    /// <param name="variableName">The name of the variable associated with the parameter</param>
 303    /// <param name="powershellAttribute">The PowerShell attribute containing validation constraints</param>
 304    /// <exception cref="InvalidOperationException">Thrown if the parameter does not have a schema or content defined be
 305    private void ProcessPowerShellAttribute(string variableName, InternalPowershellAttribute powershellAttribute)
 306    {
 2307        if (TryGetParameterItem(variableName, out var parameter))
 308        {
 1309            ValidateParameterHasSchemaOrContentForPowerShell(variableName, parameter);
 1310            ApplyPowerShellAttributeToParameter(variableName, parameter!, powershellAttribute);
 1311            return;
 312        }
 313
 1314        if (TryGetRequestBodyItem(variableName, out var requestBody))
 315        {
 0316            ApplyPowerShellAttributeToRequestBody(variableName, requestBody, powershellAttribute);
 0317            return;
 318        }
 1319        Host.Logger.Error("Parameter or RequestBody '{variableName}' not found when trying to add PowerShell attribute."
 1320    }
 321
 322    /// <summary>
 323    /// Validates that the parameter exists and has either Schema or Content defined for PowerShell attribute processing
 324    /// </summary>
 325    /// <param name="variableName">The variable name associated with the parameter.</param>
 326    /// <param name="parameter">The parameter to validate.</param>
 327    /// <exception cref="InvalidOperationException">Thrown if the parameter is null or lacks both Schema and Content.</e
 328    private static void ValidateParameterHasSchemaOrContentForPowerShell(string variableName, OpenApiParameter? paramete
 329    {
 1330        if (parameter is null || (parameter.Schema is null && parameter.Content is null))
 331        {
 0332            throw new InvalidOperationException(
 0333                $"Parameter '{variableName}' must have a schema or content defined before adding the powershell property
 334        }
 1335    }
 336
 337    /// <summary>
 338    /// Applies a PowerShell attribute to a parameter schema or to all content media-type schemas.
 339    /// </summary>
 340    /// <param name="variableName">The variable name associated with the parameter.</param>
 341    /// <param name="parameter">The target parameter.</param>
 342    /// <param name="powershellAttribute">The PowerShell attribute containing validation constraints.</param>
 343    private void ApplyPowerShellAttributeToParameter(
 344        string variableName,
 345        OpenApiParameter parameter,
 346        InternalPowershellAttribute powershellAttribute)
 347    {
 1348        if (parameter.Content is not null)
 349        {
 0350            ApplyPowerShellAttributeToMediaTypeSchemas(
 0351                variableName,
 0352                parameter.Content.Values,
 0353                powershellAttribute,
 0354                subject: "parameter");
 0355            return;
 356        }
 357
 1358        var schema = (OpenApiSchema)parameter.Schema!;
 1359        ApplyPowerShellAttributesToSchema(schema, powershellAttribute);
 1360    }
 361
 362    /// <summary>
 363    /// Applies a PowerShell attribute to all request body content media-type schemas.
 364    /// </summary>
 365    /// <param name="variableName">The variable name associated with the request body.</param>
 366    /// <param name="requestBody">The request body to update.</param>
 367    /// <param name="powershellAttribute">The PowerShell attribute containing validation constraints.</param>
 368    /// <exception cref="InvalidOperationException">Thrown if the request body is null or has no content.</exception>
 369    private void ApplyPowerShellAttributeToRequestBody(
 370        string variableName,
 371        OpenApiRequestBody? requestBody,
 372        InternalPowershellAttribute powershellAttribute)
 373    {
 0374        if (requestBody?.Content is null)
 375        {
 0376            throw new InvalidOperationException(
 0377                $"RequestBody '{variableName}' must have a content defined before adding the powershell property.");
 378        }
 379
 0380        ApplyPowerShellAttributeToMediaTypeSchemas(
 0381            variableName,
 0382            requestBody.Content.Values,
 0383            powershellAttribute,
 0384            subject: "request body");
 0385    }
 386
 387    /// <summary>
 388    /// Applies a PowerShell attribute to each concrete OpenAPI media type schema.
 389    /// </summary>
 390    /// <param name="variableName">The variable name used for warning messages.</param>
 391    /// <param name="mediaTypes">The media types to inspect.</param>
 392    /// <param name="powershellAttribute">The PowerShell attribute containing validation constraints.</param>
 393    /// <param name="subject">The subject used in warning messages (e.g. "parameter" or "request body").</param>
 394    private void ApplyPowerShellAttributeToMediaTypeSchemas(
 395        string variableName,
 396        IEnumerable<IOpenApiMediaType> mediaTypes,
 397        InternalPowershellAttribute powershellAttribute,
 398        string subject)
 399    {
 0400        foreach (var mediaType in mediaTypes)
 401        {
 0402            if (mediaType.Schema is not OpenApiSchema schema)
 403            {
 0404                Host.Logger.Warning(
 0405                    $"Powershell attribute processing is not supported for {subject} '{variableName}' with non-concrete 
 0406                continue;
 407            }
 408
 0409            ApplyPowerShellAttributesToSchema(schema, powershellAttribute);
 410        }
 0411    }
 412
 413    /// <summary>
 414    /// Applies PowerShell validation attributes to an OpenAPI schema.
 415    /// </summary>
 416    /// <param name="schema">The OpenAPI schema to modify.</param>
 417    /// <param name="powershellAttribute">The PowerShell attribute containing validation constraints.</param>
 418    private static void ApplyPowerShellAttributesToSchema(OpenApiSchema schema, InternalPowershellAttribute powershellAt
 419    {
 1420        ApplyItemConstraints(schema, powershellAttribute);
 1421        ApplyRangeConstraints(schema, powershellAttribute);
 1422        ApplyLengthConstraints(schema, powershellAttribute);
 1423        ApplyPatternConstraints(schema, powershellAttribute);
 1424        ApplyAllowedValuesConstraints(schema, powershellAttribute);
 1425        ApplyNullabilityConstraints(schema, powershellAttribute);
 1426    }
 427
 428    /// <summary>
 429    /// Applies item count constraints (MinItems, MaxItems) to a schema.
 430    /// </summary>
 431    /// <param name="schema">The schema to modify.</param>
 432    /// <param name="powershellAttribute">The PowerShell attribute containing constraints.</param>
 433    private static void ApplyItemConstraints(OpenApiSchema schema, InternalPowershellAttribute powershellAttribute)
 434    {
 1435        if (powershellAttribute.MaxItems.HasValue)
 436        {
 0437            schema.MaxItems = powershellAttribute.MaxItems;
 438        }
 1439        if (powershellAttribute.MinItems.HasValue)
 440        {
 0441            schema.MinItems = powershellAttribute.MinItems;
 442        }
 1443    }
 444
 445    /// <summary>
 446    /// Applies range constraints (Minimum, Maximum) to a schema.
 447    /// </summary>
 448    /// <param name="schema">The schema to modify.</param>
 449    /// <param name="powershellAttribute">The PowerShell attribute containing constraints.</param>
 450    private static void ApplyRangeConstraints(OpenApiSchema schema, InternalPowershellAttribute powershellAttribute)
 451    {
 1452        if (!string.IsNullOrEmpty(powershellAttribute.MinRange))
 453        {
 1454            schema.Minimum = powershellAttribute.MinRange;
 455        }
 1456        if (!string.IsNullOrEmpty(powershellAttribute.MaxRange))
 457        {
 1458            schema.Maximum = powershellAttribute.MaxRange;
 459        }
 1460    }
 461
 462    /// <summary>
 463    /// Applies length constraints (MinLength, MaxLength) to a schema.
 464    /// </summary>
 465    /// <param name="schema">The schema to modify.</param>
 466    /// <param name="powershellAttribute">The PowerShell attribute containing constraints.</param>
 467    private static void ApplyLengthConstraints(OpenApiSchema schema, InternalPowershellAttribute powershellAttribute)
 468    {
 1469        if (powershellAttribute.MinLength.HasValue)
 470        {
 0471            schema.MinLength = powershellAttribute.MinLength;
 472        }
 1473        if (powershellAttribute.MaxLength.HasValue)
 474        {
 0475            schema.MaxLength = powershellAttribute.MaxLength;
 476        }
 1477    }
 478
 479    /// <summary>
 480    /// Applies pattern constraints (regex) to a schema.
 481    /// </summary>
 482    /// <param name="schema">The schema to modify.</param>
 483    /// <param name="powershellAttribute">The PowerShell attribute containing constraints.</param>
 484    private static void ApplyPatternConstraints(OpenApiSchema schema, InternalPowershellAttribute powershellAttribute)
 485    {
 1486        if (!string.IsNullOrEmpty(powershellAttribute.RegexPattern))
 487        {
 0488            schema.Pattern = powershellAttribute.RegexPattern;
 489        }
 1490    }
 491
 492    /// <summary>
 493    /// Applies allowed values (enum) constraints to a schema.
 494    /// </summary>
 495    /// <param name="schema">The schema to modify.</param>
 496    /// <param name="powershellAttribute">The PowerShell attribute containing constraints.</param>
 497    private static void ApplyAllowedValuesConstraints(OpenApiSchema schema, InternalPowershellAttribute powershellAttrib
 498    {
 1499        if (powershellAttribute.AllowedValues is not null && powershellAttribute.AllowedValues.Count > 0)
 500        {
 0501            _ = PowerShellAttributes.ApplyValidateSetAttribute(powershellAttribute.AllowedValues, schema);
 502        }
 1503    }
 504
 505    /// <summary>
 506    /// Applies nullability constraints (ValidateNotNull, ValidateNotNullOrEmpty, ValidateNotNullOrWhiteSpace) to a sche
 507    /// </summary>
 508    /// <param name="schema">The schema to modify.</param>
 509    /// <param name="powershellAttribute">The PowerShell attribute containing constraints.</param>
 510    private static void ApplyNullabilityConstraints(OpenApiSchema schema, InternalPowershellAttribute powershellAttribut
 511    {
 1512        if (powershellAttribute.ValidateNotNullOrEmptyAttribute is not null)
 513        {
 0514            _ = PowerShellAttributes.ApplyNotNullOrEmpty(schema);
 515        }
 516
 1517        if (powershellAttribute.ValidateNotNullAttribute is not null)
 518        {
 0519            _ = PowerShellAttributes.ApplyNotNull(schema);
 520        }
 521
 1522        if (powershellAttribute.ValidateNotNullOrWhiteSpaceAttribute is not null)
 523        {
 0524            _ = PowerShellAttributes.ApplyNotNullOrWhiteSpace(schema);
 525        }
 1526    }
 527
 528    #endregion
 529
 530    /// <summary>
 531    /// Gets or creates an OpenAPI parameter item in either inline or document components.
 532    /// </summary>
 533    /// <param name="parameterName">The name of the parameter.</param>
 534    /// <param name="inline">Whether to use inline components or document components.</param>
 535    /// <returns>The OpenApiParameter item.</returns>
 536    private OpenApiParameter GetOrCreateParameterItem(string parameterName, bool inline)
 537    {
 538        IDictionary<string, IOpenApiParameter> parameters;
 539        // Determine whether to use inline components or document components
 2540        if (inline)
 541        {
 542            // Use inline components
 0543            InlineComponents.Parameters ??= new Dictionary<string, IOpenApiParameter>(StringComparer.Ordinal);
 0544            parameters = InlineComponents.Parameters;
 545        }
 546        else
 547        {
 548            // Use document components
 2549            Document.Components ??= new OpenApiComponents();
 2550            Document.Components.Parameters ??= new Dictionary<string, IOpenApiParameter>(StringComparer.Ordinal);
 2551            parameters = Document.Components.Parameters;
 552        }
 553        // Retrieve or create the parameter item
 2554        if (!parameters.TryGetValue(parameterName, out var parameterInterface) || parameterInterface is null)
 555        {
 556            // Create a new OpenApiParameter if it doesn't exist
 2557            parameterInterface = new OpenApiParameter();
 2558            parameters[parameterName] = parameterInterface;
 559        }
 560        // return the parameter item
 2561        return (OpenApiParameter)parameterInterface;
 562    }
 563
 564    /// <summary>
 565    /// Tries to get a parameter by name from either inline or document components.
 566    /// </summary>
 567    /// <param name="parameterName">The name of the parameter to retrieve.</param>
 568    /// <param name="parameter">The retrieved parameter if found; otherwise, null.</param>
 569    /// <param name="isInline">Indicates whether the parameter was found in inline components.</param>
 570    /// <returns>True if the parameter was found; otherwise, false.</returns>
 571    private bool TryGetParameterItem(string parameterName, out OpenApiParameter? parameter, out bool isInline)
 572    {
 2573        if (TryGetInline(name: parameterName, kind: OpenApiComponentKind.Parameters, out parameter))
 574        {
 0575            isInline = true;
 0576            return true;
 577        }
 2578        else if (TryGetComponent(name: parameterName, kind: OpenApiComponentKind.Parameters, out parameter))
 579        {
 1580            isInline = false;
 1581            return true;
 582        }
 1583        parameter = null;
 1584        isInline = false;
 1585        return false;
 586    }
 587
 588    /// <summary>
 589    /// Tries to get a parameter by name from either inline or document components.
 590    /// </summary>
 591    /// <param name="parameterName">The name of the parameter to retrieve.</param>
 592    /// <param name="parameter">The retrieved parameter if found; otherwise, null.</param>
 593    /// <returns>True if the parameter was found; otherwise, false.</returns>
 594    private bool TryGetParameterItem(string parameterName, out OpenApiParameter? parameter) =>
 2595    TryGetParameterItem(parameterName, out parameter, out _);
 596}

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_PathOperation.cs

#LineLine coverage
 1using Kestrun.Hosting.Options;
 2using Microsoft.OpenApi;
 3
 4namespace Kestrun.OpenApi;
 5
 6/// <summary>
 7/// Helper methods for accessing OpenAPI document components.
 8/// </summary>
 9public partial class OpenApiDocDescriptor
 10{
 11    /// <summary>
 12    /// Builds an OpenApiOperation from OpenAPIPathMetadata.
 13    /// </summary>
 14    /// <param name="meta">The OpenAPIPathMetadata to build from.</param>
 15    /// <returns>The constructed OpenApiOperation.</returns>
 16    private OpenApiOperation BuildOperationFromMetadata(OpenAPIPathMetadata meta)
 17    {
 718        var op = new OpenApiOperation
 719        {
 720            OperationId = string.IsNullOrWhiteSpace(meta.OperationId) ? null : meta.OperationId,
 721            Summary = string.IsNullOrWhiteSpace(meta.Summary) ? null : meta.Summary,
 722            Description = string.IsNullOrWhiteSpace(meta.Description) ? null : meta.Description,
 723            Deprecated = meta.Deprecated,
 724            ExternalDocs = meta.ExternalDocs,
 725            RequestBody = meta.RequestBody,
 726            Responses = meta.Responses ?? new OpenApiResponses { ["200"] = new OpenApiResponse { Description = "Success"
 727        };
 28
 729        ApplyTags(op, meta);
 730        ApplyServers(op, meta);
 731        ApplyParameters(op, meta);
 732        ApplyCallbacks(op, meta);
 733        ApplySecurity(op, meta);
 734        EnsureAutoClientErrorResponses(op, meta);
 735        ApplyExtensions(op, meta);
 736        return op;
 37    }
 38
 39    /// <summary>
 40    /// Ensures that appropriate client error responses (4XX) are included in the OpenApiOperation based on the presence
 41    /// </summary>
 42    /// <param name="operation">The OpenApiOperation to modify.</param>
 43    /// <param name="meta">The OpenAPIPathMetadata containing metadata for the operation.</param>
 44    private void EnsureAutoClientErrorResponses(OpenApiOperation operation, OpenAPIPathMetadata meta)
 45    {
 746        operation.Responses ??= [];
 47
 748        if (ResponseKeyExists(operation.Responses, "4XX") || ResponseKeyExists(operation.Responses, "default"))
 49        {
 250            return;
 51        }
 52
 553        var statusesToAdd = GetAutoClientErrorStatuses(operation, meta);
 54
 555        if (statusesToAdd.Count == 0)
 56        {
 057            return;
 58        }
 59
 560        var errorSchemaId = EnsureAutoErrorSchemaComponent();
 561        var errorContentTypes = GetAutoErrorResponseContentTypes();
 62
 563        AddMissingAutoClientErrorResponses(operation, statusesToAdd, errorSchemaId, errorContentTypes);
 564    }
 65
 66    /// <summary>
 67    /// Determines which automatic client error statuses should be added for the operation.
 68    /// </summary>
 69    /// <param name="operation">The operation being generated.</param>
 70    /// <param name="meta">The metadata describing the route and request constraints.</param>
 71    /// <returns>A set of status codes to add.</returns>
 72    private static HashSet<string> GetAutoClientErrorStatuses(OpenApiOperation operation, OpenAPIPathMetadata meta)
 73    {
 574        var statusesToAdd = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 575        var hasParameters = meta.Parameters is { Count: > 0 };
 576        var hasRequestBody = meta.RequestBody is not null;
 577        var hasRequestContentTypeValidation = meta.MapOptions.AllowedRequestContentTypes.Count > 0;
 578        var responses = operation.Responses;
 579        var hasResponseNegotiation =
 580            meta.MapOptions.DefaultResponseContentType is { Count: > 0 } ||
 981            (responses is not null && responses.Values.Any(r => r.Content is { Count: > 0 }));
 82
 583        if (hasParameters || hasRequestBody)
 84        {
 385            _ = statusesToAdd.Add("400");
 386            _ = statusesToAdd.Add("422");
 87        }
 88
 589        if (hasRequestBody || hasRequestContentTypeValidation)
 90        {
 391            _ = statusesToAdd.Add("415");
 92        }
 93
 594        if (hasResponseNegotiation)
 95        {
 396            _ = statusesToAdd.Add("406");
 97        }
 98
 599        return statusesToAdd;
 100    }
 101
 102    /// <summary>
 103    /// Adds missing automatic client error responses to the OpenAPI operation.
 104    /// </summary>
 105    /// <param name="operation">The operation to modify.</param>
 106    /// <param name="statusesToAdd">The status codes to add when absent.</param>
 107    /// <param name="errorSchemaId">The schema id used for error response bodies.</param>
 108    /// <param name="errorContentTypes">The response content types for auto client errors.</param>
 109    private static void AddMissingAutoClientErrorResponses(
 110        OpenApiOperation operation,
 111        IReadOnlyCollection<string> statusesToAdd,
 112        string errorSchemaId,
 113        IReadOnlyList<string> errorContentTypes)
 114    {
 5115        operation.Responses ??= [];
 5116        var responses = operation.Responses;
 117
 34118        foreach (var status in statusesToAdd)
 119        {
 12120            if (ResponseKeyExists(responses, status))
 121            {
 122                continue;
 123            }
 124
 11125            responses[status] = CreateAutoClientErrorResponse(status, errorSchemaId, errorContentTypes);
 126        }
 5127    }
 128
 129    /// <summary>
 130    /// Checks if a response key exists in the OpenApiResponses, ignoring case.
 131    /// </summary> <param name="responses">The OpenApiResponses to check.</param>
 132    /// <param name="statusCode">The status code key to look for.</param>
 133    /// <returns>True if the key exists; otherwise, false.</returns>
 134    private static bool ResponseKeyExists(OpenApiResponses responses, string statusCode)
 60135        => responses.Keys.Any(k => string.Equals(k, statusCode, StringComparison.OrdinalIgnoreCase));
 136
 137    /// <summary>
 138    /// Ensures that a standard error response schema is defined in the OpenAPI document components. If the schema ident
 139    /// </summary>
 140    /// <returns> The ID of the error schema component. </returns>
 141    private string EnsureAutoErrorSchemaComponent()
 142    {
 5143        Document.Components ??= new OpenApiComponents();
 5144        Document.Components.Schemas ??= new Dictionary<string, IOpenApiSchema>(StringComparer.Ordinal);
 145
 5146        var autoSchemaId = string.IsNullOrWhiteSpace(AutoErrorResponseSchemaId)
 5147            ? DefaultAutoErrorResponseSchemaId
 5148            : AutoErrorResponseSchemaId;
 149
 5150        if (!Document.Components.Schemas.ContainsKey(autoSchemaId))
 151        {
 5152            Document.Components.Schemas[autoSchemaId] = new OpenApiSchema
 5153            {
 5154                Type = JsonSchemaType.Object,
 5155                Required = new HashSet<string>(StringComparer.Ordinal) { "status", "error", "reason", "timestamp" },
 5156                Properties = new Dictionary<string, IOpenApiSchema>(StringComparer.Ordinal)
 5157                {
 5158                    ["status"] = new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int32" },
 5159                    ["error"] = new OpenApiSchema { Type = JsonSchemaType.String },
 5160                    ["reason"] = new OpenApiSchema { Type = JsonSchemaType.String },
 5161                    ["timestamp"] = new OpenApiSchema { Type = JsonSchemaType.String, Format = "date-time" },
 5162                    ["details"] = new OpenApiSchema { Type = JsonSchemaType.String },
 5163                    ["exception"] = new OpenApiSchema { Type = JsonSchemaType.String },
 5164                    ["stackTrace"] = new OpenApiSchema { Type = JsonSchemaType.String },
 5165                    ["path"] = new OpenApiSchema { Type = JsonSchemaType.String },
 5166                    ["method"] = new OpenApiSchema { Type = JsonSchemaType.String },
 5167                }
 5168            };
 169        }
 170
 5171        return autoSchemaId;
 172    }
 173
 174    private IReadOnlyList<string> GetAutoErrorResponseContentTypes()
 175    {
 5176        if (AutoErrorResponseContentTypes is null || AutoErrorResponseContentTypes.Length == 0)
 177        {
 0178            return [DefaultAutoErrorResponseContentType];
 179        }
 180
 5181        var contentTypes = AutoErrorResponseContentTypes
 6182            .Where(ct => !string.IsNullOrWhiteSpace(ct))
 5183            .Distinct(StringComparer.OrdinalIgnoreCase)
 5184            .ToArray();
 185
 5186        return contentTypes.Length == 0
 5187            ? [DefaultAutoErrorResponseContentType]
 5188            : contentTypes;
 189    }
 190
 191    private static OpenApiResponse CreateAutoClientErrorResponse(string statusCode, string errorSchemaId, IReadOnlyList<
 192    {
 11193        var description = statusCode switch
 11194        {
 3195            "400" => "Bad Request",
 3196            "406" => "Not Acceptable",
 2197            "415" => "Unsupported Media Type",
 3198            "422" => "Unprocessable Entity",
 0199            _ => "Client Error"
 11200        };
 201
 11202        var content = new Dictionary<string, IOpenApiMediaType>(StringComparer.Ordinal);
 50203        foreach (var contentType in contentTypes)
 204        {
 14205            content[contentType] = new OpenApiMediaType
 14206            {
 14207                Schema = new OpenApiSchemaReference(errorSchemaId)
 14208            };
 209        }
 210
 11211        return new OpenApiResponse
 11212        {
 11213            Description = description,
 11214            Content = content
 11215        };
 216    }
 217    /// <summary>
 218    /// Applies extension information from metadata to the OpenApiOperation.
 219    /// </summary>
 220    /// <param name="op">The OpenApiOperation to modify.</param>
 221    /// <param name="meta">The OpenAPIPathMetadata containing extension information.</param>
 222    private static void ApplyExtensions(OpenApiOperation op, OpenAPIPathMetadata meta)
 223    {
 7224        if (meta.Extensions is null || meta.Extensions.Count == 0)
 225        {
 7226            return;
 227        }
 0228        op.Extensions = meta.Extensions;
 0229    }
 230
 231    /// <summary>
 232    /// Applies tags from metadata to the OpenApiOperation.
 233    /// </summary>
 234    /// <param name="op">The OpenApiOperation to modify.</param>
 235    /// <param name="meta">The OpenAPIPathMetadata containing tags.</param>
 236    private static void ApplyTags(OpenApiOperation op, OpenAPIPathMetadata meta)
 237    {
 7238        if (meta.Tags.Count > 0)
 239        {
 2240            op.Tags = new HashSet<OpenApiTagReference>();
 10241            foreach (var t in meta.Tags ?? [])
 242            {
 3243                _ = op.Tags.Add(new OpenApiTagReference(t));
 244            }
 245        }
 7246    }
 247
 248    /// <summary>
 249    /// Applies server information from metadata to the OpenApiOperation.
 250    /// </summary>
 251    /// <param name="op">The OpenApiOperation to modify.</param>
 252    /// <param name="meta">The OpenAPIPathMetadata containing server information.</param>
 253    private void ApplyServers(OpenApiOperation op, OpenAPIPathMetadata meta)
 254    {
 255        try
 256        {
 7257            if (meta.Servers is { Count: > 0 })
 258            {
 0259                dynamic d = op;
 0260                if (d.Servers == null) { d.Servers = new List<OpenApiServer>(); }
 0261                foreach (var s in meta.Servers) { d.Servers.Add(s); }
 262            }
 7263        }
 0264        catch (Exception ex)
 265        {
 0266            Host.Logger.Warning(ex, "Failed to set operation-level servers for OpenAPI operation {OperationId}", op.Oper
 0267        }
 7268    }
 269
 270    /// <summary>
 271    /// Applies parameter information from metadata to the OpenApiOperation.
 272    /// </summary>
 273    /// <param name="op">The OpenApiOperation to modify.</param>
 274    /// <param name="meta">The OpenAPIPathMetadata containing parameter information.</param>
 275    private void ApplyParameters(OpenApiOperation op, OpenAPIPathMetadata meta)
 276    {
 277        try
 278        {
 7279            if (meta.Parameters is { Count: > 0 })
 280            {
 3281                dynamic d = op;
 6282                if (d.Parameters == null) { d.Parameters = new List<IOpenApiParameter>(); }
 15283                foreach (var p in meta.Parameters) { d.Parameters.Add(p); }
 284            }
 7285        }
 0286        catch { Host.Logger?.Warning("Failed to set operation-level parameters for OpenAPI operation {OperationId}", op.
 7287    }
 288
 289    /// <summary>
 290    /// Applies security requirement information from metadata to the OpenApiOperation.
 291    /// </summary>
 292    /// <param name="op">The OpenApiOperation to modify.</param>
 293    /// <param name="meta">The OpenAPIPathMetadata containing security requirement information.</param>
 294    private void ApplySecurity(OpenApiOperation op, OpenAPIPathMetadata meta)
 295    {
 7296        if (meta.SecuritySchemes is not null && meta.SecuritySchemes.Count != 0)
 297        {
 0298            op.Security ??= [];
 299
 0300            var seen = new HashSet<string>(StringComparer.Ordinal);
 301
 0302            foreach (var schemeName in meta.SecuritySchemes
 0303                         .SelectMany(d => d.Keys)
 0304                         .Distinct())
 305            {
 0306                if (!seen.Add(schemeName))
 307                {
 308                    continue;
 309                }
 310                // Gather scopes for this scheme
 0311                var scopesForScheme = meta.SecuritySchemes
 0312                    .SelectMany(dict => dict)
 0313                    .Where(kv => kv.Key == schemeName)
 0314                    .SelectMany(kv => kv.Value)
 0315                    .Distinct()
 0316                    .ToList();
 317                // Build requirement
 0318                var requirement = new OpenApiSecurityRequirement
 0319                {
 0320                    [new OpenApiSecuritySchemeReference(schemeName, Document)] = scopesForScheme
 0321                };
 322
 0323                op.Security.Add(requirement);
 324            }
 325        }
 7326        else if (meta.SecuritySchemes is not null && meta.SecuritySchemes.Count == 0)
 327        {
 328            // Explicitly anonymous for this operation (overrides Document.Security)
 0329            op.Security = [];
 330        }
 7331    }
 332}

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_RequestBody.cs

#LineLine coverage
 1using Microsoft.OpenApi;
 2
 3namespace Kestrun.OpenApi;
 4
 5/// <summary>
 6/// Generates OpenAPI v2 (Swagger) documents from C# types decorated with OpenApiSchema attributes.
 7/// </summary>
 8public partial class OpenApiDocDescriptor
 9{
 10    /// <summary>
 11    /// Clones an example from components or throws if not found.
 12    /// </summary>
 13    /// <param name="referenceId"> The reference ID of the example to clone.</param>
 14    /// <returns>The cloned example.</returns>
 15    /// <exception cref="InvalidOperationException">Thrown if the example reference cannot be found or is not an OpenApi
 16#pragma warning disable CA1859 // Use concrete types when possible for improved performance
 17    private IOpenApiExample CloneExampleOrThrow(string referenceId)
 18#pragma warning restore CA1859 // Use concrete types when possible for improved performance
 19    {
 020        return Document.Components?.Examples == null || !Document.Components.Examples.TryGetValue(referenceId, out var v
 021            ? throw new InvalidOperationException($"Example reference '{referenceId}' cannot be embedded because it was 
 022            : value is not OpenApiExample example
 023            ? throw new InvalidOperationException($"Example reference '{referenceId}' cannot be embedded because it is n
 024            : (IOpenApiExample)example.Clone();
 25    }
 26
 27    #region Request Body Component Processing
 28    /// <summary>
 29    /// Processes a request body component annotation to create or update an OpenAPI request body.
 30    /// </summary>
 31    /// <param name="variable">The annotated variable containing metadata about the request body</param>
 32    /// <param name="requestBodyDescriptor">The request body component annotation</param>
 33    private void ProcessRequestBodyComponent(
 34      OpenApiComponentAnnotationScanner.AnnotatedVariable variable,
 35      OpenApiRequestBodyComponentAttribute requestBodyDescriptor)
 36    {
 037        var key = requestBodyDescriptor.Key ?? variable.Name;
 038        var requestBody = GetOrCreateRequestBodyItem(key, requestBodyDescriptor.Inline);
 39
 040        ApplyRequestBodyCommonFields(requestBody, requestBodyDescriptor);
 41
 042        TryApplyVariableTypeSchema(requestBody, variable, requestBodyDescriptor);
 043    }
 44
 45    /// <summary>
 46    /// Applies common fields from a request body component annotation to an OpenAPI request body.
 47    /// </summary>
 48    /// <param name="requestBody">The OpenApiRequestBody to modify</param>
 49    /// <param name="requestBodyAnnotation">The request body component annotation</param>
 50    private static void ApplyRequestBodyCommonFields(
 51        OpenApiRequestBody requestBody,
 52        OpenApiRequestBodyComponentAttribute requestBodyAnnotation)
 53    {
 054        requestBody.Description = requestBodyAnnotation.Description;
 55
 056        requestBody.Required = requestBodyAnnotation.Required;
 057    }
 58
 59    /// <summary>
 60    /// Tries to apply the variable type schema to an OpenAPI request body.
 61    /// </summary>
 62    /// <param name="requestBody">The OpenApiRequestBody to modify</param>
 63    /// <param name="variable">The annotated variable containing metadata about the request body</param>
 64    /// <param name="requestBodyAnnotation">The request body component annotation</param>
 65    private void TryApplyVariableTypeSchema(
 66        OpenApiRequestBody requestBody,
 67        OpenApiComponentAnnotationScanner.AnnotatedVariable variable,
 68        OpenApiRequestBodyComponentAttribute requestBodyAnnotation)
 69    {
 070        if (variable.VariableType is null)
 71        {
 072            return;
 73        }
 074        var iSchema = InferPrimitiveSchema(variable.VariableType);
 075        if (iSchema is OpenApiSchema schema)
 76        {
 77            //PowerShellAttributes.ApplyPowerShellAttributes(variable.PropertyInfo, schema);
 78            // Apply any schema attributes from the request body annotation
 079            ApplyConcreteSchemaAttributes(requestBodyAnnotation, schema);
 80            // Try to set default value from the variable initial value if not already set
 081            if (!variable.NoDefault)
 82            {
 083                schema.Default = OpenApiJsonNodeFactory.ToNode(variable.InitialValue);
 84            }
 85        }
 086        if (requestBodyAnnotation.ContentType is null || requestBodyAnnotation.ContentType.Length == 0)
 87        {
 88            // Fallback to application/json if no content type specified
 089            requestBodyAnnotation.ContentType = ["application/json"];
 90        }
 91
 92        // Use Content
 093        requestBody.Content ??= new Dictionary<string, IOpenApiMediaType>(StringComparer.Ordinal);
 094        foreach (var ct in requestBodyAnnotation.ContentType)
 95        {
 096            requestBody.Content[ct] = new OpenApiMediaType { Schema = iSchema };
 97        }
 098    }
 99
 100    /// <summary>
 101    /// Processes a request body example reference annotation to add an example to an OpenAPI request body.
 102    /// </summary>
 103    /// <param name="variableName">The name of the variable associated with the request body.</param>
 104    /// <param name="exampleRef">The example reference attribute.</param>
 105    /// <exception cref="InvalidOperationException">Thrown when the request body does not exist, lacks schema/content, o
 106    private void ProcessRequestBodyExampleRef(string variableName, OpenApiRequestBodyExampleRefAttribute exampleRef)
 107    {
 0108        if (!TryGetRequestBodyItem(variableName, out var requestBody))
 109        {
 0110            throw new InvalidOperationException($"Request body '{variableName}' not found when trying to add example ref
 111        }
 0112        if (requestBody is not null && requestBody.Content is not null)
 113        {
 0114            AddExamplesToContentMediaTypes(requestBody, exampleRef, variableName);
 115        }
 0116    }
 117    #endregion
 118
 119    /// <summary>
 120    /// Gets or creates an OpenAPI request body item in either inline or document components.
 121    /// </summary>
 122    /// <param name="requestBodyName">The name of the request body.</param>
 123    /// <param name="inline">Whether to use inline components or document components.</param>
 124    /// <returns>The OpenApiRequestBody item.</returns>
 125    private OpenApiRequestBody GetOrCreateRequestBodyItem(string requestBodyName, bool inline)
 126    {
 127        IDictionary<string, IOpenApiRequestBody> requestBodies;
 128        // Determine whether to use inline components or document components
 0129        if (inline)
 130        {
 131            // Use inline components
 0132            InlineComponents.RequestBodies ??= new Dictionary<string, IOpenApiRequestBody>(StringComparer.Ordinal);
 0133            requestBodies = InlineComponents.RequestBodies;
 134        }
 135        else
 136        {
 137            // Use document components
 0138            Document.Components ??= new OpenApiComponents();
 0139            Document.Components.RequestBodies ??= new Dictionary<string, IOpenApiRequestBody>(StringComparer.Ordinal);
 0140            requestBodies = Document.Components.RequestBodies;
 141        }
 142        // Retrieve or create the request body item
 0143        if (!requestBodies.TryGetValue(requestBodyName, out var requestBodyInterface) || requestBodyInterface is null)
 144        {
 145            // Create a new OpenApiRequestBody if it doesn't exist
 0146            requestBodyInterface = new OpenApiRequestBody();
 0147            requestBodies[requestBodyName] = requestBodyInterface;
 148        }
 149        // return the request body item
 0150        return (OpenApiRequestBody)requestBodyInterface;
 151    }
 152
 153    /// <summary>
 154    /// Tries to get a request body by name from either inline or document components.
 155    /// </summary>
 156    /// <param name="requestBodyName">The name of the request body to retrieve.</param>
 157    /// <param name="requestBody">The retrieved request body if found; otherwise, null.</param>
 158    /// <param name="isInline">Indicates whether the request body was found in inline components.</param>
 159    /// <returns>True if the request body was found; otherwise, false.</returns>
 160    private bool TryGetRequestBodyItem(string requestBodyName, out OpenApiRequestBody? requestBody, out bool isInline)
 161    {
 1162        if (TryGetInline(name: requestBodyName, kind: OpenApiComponentKind.RequestBodies, out requestBody))
 163        {
 0164            isInline = true;
 0165            return true;
 166        }
 1167        else if (TryGetComponent(name: requestBodyName, kind: OpenApiComponentKind.RequestBodies, out requestBody))
 168        {
 0169            isInline = false;
 0170            return true;
 171        }
 1172        requestBody = null;
 1173        isInline = false;
 1174        return false;
 175    }
 176
 177    /// <summary>
 178    /// Tries to get a request body by name from either inline or document components.
 179    /// </summary>
 180    /// <param name="requestBodyName">The name of the request body to retrieve.</param>
 181    /// <param name="requestBody">The retrieved request body if found; otherwise, null.</param>
 182    /// <returns>True if the request body was found; otherwise, false.</returns>
 183    private bool TryGetRequestBodyItem(string requestBodyName, out OpenApiRequestBody? requestBody) =>
 1184    TryGetRequestBodyItem(requestBodyName, out requestBody, out _);
 185}

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Response.cs

#LineLine coverage
 1using Microsoft.OpenApi;
 2
 3namespace Kestrun.OpenApi;
 4
 5/// <summary>
 6/// Generates OpenAPI v2 (Swagger) documents from C# types decorated with OpenApiSchema attributes.
 7/// </summary>
 8public partial class OpenApiDocDescriptor
 9{
 10    /// <summary>
 11    /// Gets the name override from an attribute, if present.
 12    /// </summary>
 13    /// <param name="attr">The attribute to inspect.</param>
 14    /// <returns>The name override, if present; otherwise, null.</returns>
 15    private static string? GetKeyOverride(object attr)
 16    {
 017        var t = attr.GetType();
 018        return t.GetProperty("Key")?.GetValue(attr) as string;
 19    }
 20
 21    /// <summary>
 22    /// Creates or modifies an OpenApiResponse based on the provided attribute.
 23    /// </summary>
 24    /// <param name="attr"> The attribute to apply.</param>
 25    /// <param name="response"> The OpenApiResponse to modify.</param>
 26    /// <param name="iSchema"> An optional schema to apply.</param>
 27    /// <returns>True if the response was modified; otherwise, false.</returns>
 28    private bool CreateResponseFromAttribute(object attr, OpenApiResponse? response, IOpenApiSchema? iSchema = null)
 29    {
 130        ArgumentNullException.ThrowIfNull(attr);
 131        ArgumentNullException.ThrowIfNull(response);
 32
 133        return attr switch
 134        {
 135            OpenApiResponseAttribute resp => ApplyResponseAttribute(resp, response, iSchema),
 036            OpenApiResponseHeaderRefAttribute href => ApplyHeaderRefAttribute(href, response),
 037            OpenApiResponseHeaderAttribute head => ApplyHeaderAttribute(head, response),
 038            OpenApiResponseLinkRefAttribute lref => ApplyLinkRefAttribute(lref, response),
 039            OpenApiExampleRefAttribute exRef => ApplyExampleRefAttribute(exRef, response),
 040            OpenApiResponseExampleRefAttribute exRef => ApplyExampleRefAttribute(exRef, response),
 041            _ => false
 142        };
 43    }
 44    // --- local helpers -------------------------------------------------------
 45
 46    /// <summary>
 47    /// Applies an OpenApiResponseAttribute to an OpenApiResponse.
 48    /// </summary>
 49    /// <param name="resp">The OpenApiResponseAttribute to apply.</param>
 50    /// <param name="response">The OpenApiResponse to modify.</param>
 51    /// <param name="schema">An optional schema to apply.</param>
 52    /// <returns>True if the response was modified; otherwise, false.</returns>
 53    private bool ApplyResponseAttribute(OpenApiResponseAttribute resp, OpenApiResponse response, IOpenApiSchema? schema)
 54    {
 155        ApplyDescription(resp, response);
 156        schema = ResolveResponseSchema(resp, schema);
 157        ApplySchemaToContentTypes(resp, response, schema);
 158        return true;
 59    }
 60
 61    private static void ApplyDescription(OpenApiResponseAttribute resp, OpenApiResponse response)
 62    {
 163        if (!string.IsNullOrEmpty(resp.Description))
 64        {
 165            response.Description = resp.Description;
 66        }
 167    }
 68
 69    private IOpenApiSchema? ResolveResponseSchema(OpenApiResponseAttribute resp, IOpenApiSchema? propertySchema)
 70    {
 71        // 1) Type-based schema
 172        if (resp.Schema is not null)
 73        {
 174            return InferPrimitiveSchema(resp.Schema, inline: resp.Inline);
 75        }
 076        if (resp.SchemaItem is not null)
 77        {
 078            return InferPrimitiveSchema(resp.SchemaItem, inline: resp.Inline);
 79        }
 80
 81        // 4) Fallback to existing property schema (primitive/concrete)
 082        return propertySchema;
 83    }
 84
 85    private void ApplySchemaToContentTypes(OpenApiResponseAttribute resp, OpenApiResponse response, IOpenApiSchema? sche
 86    {
 187        if (schema is not null && resp.ContentType is { Length: > 0 })
 88        {
 489            foreach (var ct in resp.ContentType)
 90            {
 191                var media = GetOrAddMediaType(response, ct);
 192                if (media is OpenApiMediaType mediaType)
 93                {
 194                    if (resp.SchemaItem != null)
 95                    {
 096                        mediaType.ItemSchema = schema;
 97                    }
 98                    else
 99                    {
 1100                        mediaType.Schema = schema;
 101                    }
 102                }
 103            }
 104        }
 1105    }
 106
 107    /// <summary>
 108    /// Applies a header reference attribute to an OpenAPI response.
 109    /// </summary>
 110    /// <param name="href">The header reference attribute.</param>
 111    /// <param name="response">The OpenAPI response to modify.</param>
 112    /// <returns>True if the header reference was applied; otherwise, false.</returns>
 113    private bool ApplyHeaderRefAttribute(OpenApiResponseHeaderRefAttribute href, OpenApiResponse response)
 114    {
 115        // ensure headers dictionary
 0116        response.Headers ??= new Dictionary<string, IOpenApiHeader>(StringComparer.Ordinal);
 117        // create header reference
 0118        return TryAddHeader(response.Headers, href);
 119    }
 120
 121    private bool ApplyHeaderAttribute(OpenApiResponseHeaderAttribute href, OpenApiResponse response)
 122    {
 123        // ensure headers dictionary
 0124        response.Headers ??= new Dictionary<string, IOpenApiHeader>(StringComparer.Ordinal);
 125        // create header from attribute
 0126        var header = NewOpenApiHeader(
 0127            description: href.Description,
 0128            required: href.Required,
 0129            deprecated: href.Deprecated,
 0130            allowEmptyValue: href.AllowEmptyValue,
 0131            style: href.Style != null ? ((OaParameterStyle)href.Style).ToOpenApi() : null,
 0132            explode: href.Explode,
 0133            allowReserved: href.AllowReserved,
 0134            example: href.Example,
 0135            examples: null,
 0136            schema: href.Schema,
 0137            content: null
 0138        );
 139        // add header to response
 0140        return response.Headers.TryAdd(href.Key, header);
 141    }
 142
 143    /// <summary>
 144    /// Applies an example reference attribute to an OpenAPI response.
 145    /// </summary>
 146    /// <param name="exRef">The example reference attribute.</param>
 147    /// <param name="response">The OpenAPI response to modify.</param>
 148    /// <returns>True if the example reference was applied; otherwise, false.</returns>
 149    /// <exception cref="InvalidOperationException">Thrown if the example reference cannot be embedded because it was no
 150    private bool ApplyExampleRefAttribute(OpenApiExampleRefAttribute exRef, OpenApiResponse response)
 151    {
 0152        foreach (var contentType in ResolveExampleTargets(exRef, response))
 153        {
 0154            if (GetOrAddMediaType(response, contentType) is not OpenApiMediaType media)
 155            {
 156                continue;
 157            }
 158
 0159            media.Examples ??= new Dictionary<string, IOpenApiExample>(StringComparer.Ordinal);
 0160            media.Examples[exRef.Key] = exRef.Inline
 0161                ? CloneExampleOrThrow(exRef.ReferenceId)
 0162                : new OpenApiExampleReference(exRef.ReferenceId);
 163        }
 0164        return true;
 165    }
 166
 167    private bool ApplyExampleRefAttribute(OpenApiResponseExampleRefAttribute attribute, OpenApiResponse response)
 168    {
 0169        foreach (var contentType in ResolveExampleTargets(attribute, response))
 170        {
 0171            if (GetOrAddMediaType(response, contentType) is not OpenApiMediaType media)
 172            {
 173                continue;
 174            }
 175
 0176            media.Examples ??= new Dictionary<string, IOpenApiExample>(StringComparer.Ordinal);
 177            // Clone or reference the example
 0178            _ = TryAddExample(media.Examples, attribute);
 179        }
 0180        return true;
 181    }
 182
 183    private static IEnumerable<string> ResolveExampleTargets(OpenApiExampleRefAttribute exRef, OpenApiResponse response)
 184    {
 0185        var targets = exRef.ContentType is null
 0186            ? (IEnumerable<string>)(response.Content?.Keys ?? Array.Empty<string>())
 0187            : exRef.ContentType;
 188
 0189        return targets.Any() ? targets : ["application/json"];
 190    }
 191
 192    private static IEnumerable<string> ResolveExampleTargets(OpenApiResponseExampleRefAttribute exRef, OpenApiResponse r
 193    {
 0194        var targets = exRef.ContentType is null
 0195            ? (IEnumerable<string>)(response.Content?.Keys ?? Array.Empty<string>())
 0196            : exRef.ContentType;
 197
 0198        return targets.Any() ? targets : ["application/json"];
 199    }
 200
 201    /// <summary>
 202    /// Gets or adds a media type to the response for the specified content type.
 203    /// </summary>
 204    /// <param name="resp">The OpenAPI response object.</param>
 205    /// <param name="contentType">The content type for the media type.</param>
 206    /// <returns>The media type associated with the specified content type.</returns>
 207    private static IOpenApiMediaType GetOrAddMediaType(OpenApiResponse resp, string contentType)
 208    {
 1209        resp.Content ??= new Dictionary<string, IOpenApiMediaType>(StringComparer.Ordinal);
 1210        if (!resp.Content.TryGetValue(contentType, out var media))
 211        {
 1212            media = resp.Content[contentType] = new OpenApiMediaType();
 213        }
 214
 1215        return media;
 216    }
 217
 218    private OpenApiSchema CloneSchemaOrThrow(string refId)
 219    {
 0220        if (Document.Components?.Schemas is { } schemas &&
 0221            schemas.TryGetValue(refId, out var schema))
 222        {
 223            // your existing clone semantics
 0224            return (OpenApiSchema)schema.Clone();
 225        }
 226
 0227        throw new InvalidOperationException(
 0228            $"Schema reference '{refId}' cannot be embedded because it was not found in components.");
 229    }
 230
 231    private void ProcessResponseExampleRef(string name, OpenApiResponseExampleRefAttribute attribute)
 232    {
 0233        if (attribute.StatusCode != "default")
 234        {
 0235            throw new InvalidOperationException("Response example references cannot have a status code.");
 236        }
 0237        if (attribute.Key is null)
 238        {
 0239            throw new InvalidOperationException("Response example attributes must have a Key specified to define the exa
 240        }
 0241        if (!TryGetResponseItem(name, out var response))
 242        {
 0243            throw new InvalidOperationException($"response '{name}' not found when trying to add to response.");
 244        }
 0245        _ = CreateResponseFromAttribute(attribute, response);
 0246    }
 247
 248    private void ProcessResponseLinkRef(string name, OpenApiResponseLinkRefAttribute attribute)
 249    {
 0250        if (attribute.StatusCode != "default")
 251        {
 0252            throw new InvalidOperationException("Response link references cannot have a status code.");
 253        }
 0254        if (attribute.Key is null)
 255        {
 0256            throw new InvalidOperationException("Response link attributes must have a Key specified to define the link n
 257        }
 0258        if (!TryGetResponseItem(name, out var response))
 259        {
 0260            throw new InvalidOperationException($"response '{name}' not found when trying to add to response.");
 261        }
 0262        _ = CreateResponseFromAttribute(attribute, response);
 0263    }
 264
 265    private void ProcessResponseHeaderRef(string name, OpenApiResponseHeaderRefAttribute attribute)
 266    {
 0267        if (attribute.StatusCode != "default")
 268        {
 0269            throw new InvalidOperationException("Response header references cannot have a status code.");
 270        }
 0271        if (attribute.Key is null)
 272        {
 0273            throw new InvalidOperationException("Response header attributes must have a Key specified to define the head
 274        }
 0275        if (!TryGetResponseItem(name, out var response))
 276        {
 0277            throw new InvalidOperationException($"response '{name}' not found when trying to add to response.");
 278        }
 0279        _ = CreateResponseFromAttribute(attribute, response);
 0280    }
 281
 282    private void ProcessResponseComponent(
 283      OpenApiComponentAnnotationScanner.AnnotatedVariable variable,
 284      OpenApiResponseComponentAttribute responseDescriptor)
 285    {
 0286        var response = GetOrCreateResponseItem(variable.Name, responseDescriptor.Inline);
 287
 0288        ApplyResponseCommonFields(response, responseDescriptor);
 289
 0290        TryApplyVariableTypeSchema(response, variable, responseDescriptor);
 0291    }
 292
 293    #region Response Item Helpers
 294
 295    private OpenApiResponse GetOrCreateResponseItem(string responseName, bool inline)
 296    {
 297        IDictionary<string, IOpenApiResponse> responses;
 298        // Determine whether to use inline components or document components
 0299        if (inline)
 300        {
 301            // Use inline components
 0302            InlineComponents.Responses ??= new Dictionary<string, IOpenApiResponse>(StringComparer.Ordinal);
 0303            responses = InlineComponents.Responses;
 304        }
 305        else
 306        {
 307            // Use document components
 0308            Document.Components ??= new OpenApiComponents();
 0309            Document.Components.Responses ??= new Dictionary<string, IOpenApiResponse>(StringComparer.Ordinal);
 0310            responses = Document.Components.Responses;
 311        }
 312        // Retrieve or create the response item
 0313        if (!responses.TryGetValue(responseName, out var responseInterface) || responseInterface is null)
 314        {
 315            // Create a new OpenApiResponse if it doesn't exist
 0316            responseInterface = new OpenApiResponse();
 0317            responses[responseName] = responseInterface;
 318        }
 319        // return the response item
 0320        return (OpenApiResponse)responseInterface;
 321    }
 322
 323    /// <summary>
 324    /// Tries to get a response item by name from either inline or document components.
 325    /// </summary>
 326    /// <param name="responseName"> The name of the response item to retrieve.</param>
 327    /// <param name="response">The retrieved OpenApiResponse if found; otherwise, null.</param>
 328    /// <param name="isInline">Indicates whether the response was found in inline components.</param>
 329    /// <returns>True if the response item was found; otherwise, false.</returns>
 330    private bool TryGetResponseItem(string responseName, out OpenApiResponse? response, out bool isInline)
 331    {
 332        // First, check inline components
 0333        if (TryGetInline(name: responseName, kind: OpenApiComponentKind.Responses, out response))
 334        {
 0335            isInline = true;
 0336            return true;
 337        }
 338        // Next, check document components
 0339        else if (TryGetComponent(name: responseName, kind: OpenApiComponentKind.Responses, out response))
 340        {
 0341            isInline = false;
 0342            return true;
 343        }
 0344        response = null;
 0345        isInline = false;
 0346        return false;
 347    }
 348    /// <summary>
 349    /// Tries to get a response item by name from document components only.
 350    /// </summary>
 351    /// <param name="responseName"> The name of the response item to retrieve.</param>
 352    /// <param name="response"> The retrieved OpenApiResponse if found; otherwise, null.</param>
 353    /// <returns>True if the response item was found; otherwise, false.</returns>
 354    private bool TryGetResponseItem(string responseName, out OpenApiResponse? response) =>
 0355    TryGetResponseItem(responseName, out response, out _);
 356
 357    #endregion
 358}

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Schema.cs

#LineLine coverage
 1using System.Reflection;
 2using System.Text.Json.Nodes;
 3using Microsoft.OpenApi;
 4using OpenApiXmlModel = Microsoft.OpenApi.OpenApiXml;
 5using Kestrun.Runtime;
 6using Kestrun.Forms;
 7
 8namespace Kestrun.OpenApi;
 9
 10/// <summary>
 11/// Generates OpenAPI v2 (Swagger) documents from C# types decorated with OpenApiSchema attributes.
 12/// </summary>
 13public partial class OpenApiDocDescriptor
 14{
 15    #region Schemas
 16    private static OpenApiProperties? GetSchemaIdentity(Type t)
 17    {
 18        // Prefer OpenApiPropertyAttribute (it supports operation/property-level overrides),
 19        // but fall back to any OpenApiProperties-derived attribute (e.g., OpenApiSchemaComponent).
 020        var propAttrs = (OpenApiPropertyAttribute[])t.GetCustomAttributes(typeof(OpenApiPropertyAttribute), inherit: tru
 021        if (propAttrs.Length > 0)
 22        {
 023            return propAttrs[0];
 24        }
 25
 26        // Note: OpenApiSchemaComponent is Inherited=false, so inherit:true won't climb.
 27        // We walk the base chain manually to allow schemas deriving from OpenApi* primitives
 28        // (or from a base schema component) to inherit the underlying identity.
 029        for (var current = t; current is not null && current != typeof(object); current = current.BaseType)
 30        {
 031            var schemaAttrs = current.GetCustomAttributes(inherit: false).OfType<OpenApiProperties>().ToArray();
 032            if (schemaAttrs.Length > 0)
 33            {
 034                return schemaAttrs[0];
 35            }
 36        }
 37
 038        return null;
 39    }
 40
 41    /// <summary>
 42    /// Builds and returns the schema for a given type.
 43    /// </summary>
 44    /// <param name="t">Type to build schema for</param>
 45    /// <param name="built">Set of types already built to avoid recursion</param>
 46    /// <returns>OpenApiSchema representing the type</returns>
 47    private IOpenApiSchema BuildSchemaForType(Type t, HashSet<Type>? built = null)
 48    {
 6849        built ??= [];
 50
 6851        if (TryBuildPrimitiveSchema(t, out var primitiveSchema))
 52        {
 1353            ApplyTypeAttributes(t, primitiveSchema);
 1354            return primitiveSchema;
 55        }
 56
 5557        var formSchemaParent = TryBuildFormPayloadSchemaParent(t, built);
 58
 5559        if (TryBuildDerivedSchemaFromBaseType(t, built, out var derivedSchema, out var schemaParent))
 60        {
 061            return derivedSchema;
 62        }
 63
 5564        var schema = CreateSchemaForDeclaredProperties(t);
 65
 5566        if (built.Contains(t))
 67        {
 168            return schema;
 69        }
 70
 5471        _ = built.Add(t);
 72
 5473        ApplyTypeAttributes(t, schema);
 74
 5475        if (t.IsEnum)
 76        {
 877            return RegisterEnumSchema(t);
 78        }
 79        // Extensions
 4680        ProcessExtensions(t, schema);
 81        // Properties
 4682        ProcessTypeProperties(t, schema, built);
 83        // Return composed schema if applicable
 4684        var parentToCompose = schemaParent ?? formSchemaParent;
 4685        return ComposeWithParentSchema(parentToCompose, schema);
 86    }
 87
 88    private OpenApiSchema? TryBuildFormPayloadSchemaParent(Type t, HashSet<Type> built)
 89    {
 5590        var bindAttr = t.GetCustomAttributes(inherit: true)
 16791            .FirstOrDefault(a => a.GetType().Name.Equals("KrBindFormAttribute", StringComparison.OrdinalIgnoreCase));
 92
 5593        if (bindAttr is null)
 94        {
 5195            return null;
 96        }
 97
 498        var depthProp = bindAttr.GetType().GetProperty("MaxNestingDepth", BindingFlags.Public | BindingFlags.Instance);
 499        var maxDepth = depthProp?.GetValue(bindAttr) as int? ?? 0;
 4100        var baseType = maxDepth > 0 ? typeof(KrMultipart) : typeof(KrFormData);
 101
 4102        BuildSchema(baseType, built);
 103
 4104        return new OpenApiSchema
 4105        {
 4106            AllOf = [new OpenApiSchemaReference(baseType.Name)]
 4107        };
 108    }
 109
 110    /// <summary>
 111    /// Processes OpenAPI extensions defined on a type and adds them to the schema.
 112    /// </summary>
 113    /// <param name="t">The type being processed.</param>
 114    /// <param name="schema"></param>
 115    private static void ProcessExtensions(Type t, OpenApiSchema schema)
 116    {
 92117        foreach (var attr in t.GetCustomAttributes<OpenApiExtensionAttribute>(inherit: false))
 118        {
 0119            schema.Extensions ??= new Dictionary<string, IOpenApiExtension>(StringComparer.Ordinal);
 120            // Parse string into a JsonNode tree.
 0121            var node = JsonNode.Parse(attr.Json);
 0122            if (node is null)
 123            {
 124                continue;
 125            }
 0126            schema.Extensions[attr.Name] = new JsonNodeExtension(node);
 127        }
 46128    }
 129
 130    /// <summary>
 131    /// Attempts to create a schema for types mapped as OpenAPI primitives/scalars.
 132    /// </summary>
 133    /// <param name="t">The CLR type to map.</param>
 134    /// <param name="schema">The created primitive schema.</param>
 135    /// <returns><c>true</c> if the type was mapped as a primitive/scalar; otherwise, <c>false</c>.</returns>
 136    private static bool TryBuildPrimitiveSchema(Type t, out OpenApiSchema schema)
 137    {
 68138        if (PrimitiveSchemaMap.TryGetValue(t, out var getSchema))
 139        {
 13140            schema = getSchema();
 13141            return true;
 142        }
 143
 55144        schema = null!;
 55145        return false;
 146    }
 147
 148    /// <summary>
 149    /// Attempts to resolve schema generation for types that derive from another schema component.
 150    /// This covers array-wrappers and inheritance composition via <c>allOf</c>.
 151    /// </summary>
 152    /// <param name="t">The derived type being processed.</param>
 153    /// <param name="built">The recursion guard set passed through schema-building.</param>
 154    /// <param name="resolved">The resolved schema to return if handled.</param>
 155    /// <param name="schemaParent">The parent schema, if composition should be applied later.</param>
 156    /// <returns><c>true</c> if schema generation was fully handled and <paramref name="resolved"/> is set.</returns>
 157    private bool TryBuildDerivedSchemaFromBaseType(
 158        Type t,
 159        HashSet<Type> built,
 160        out IOpenApiSchema resolved,
 161        out OpenApiSchema? schemaParent)
 162    {
 55163        resolved = null!;
 55164        schemaParent = null;
 165
 55166        if (!HasComposableBaseType(t))
 167        {
 46168            return false;
 169        }
 170
 9171        var baseSchema = BuildBaseTypeSchema(t);
 9172        if (baseSchema is null)
 173        {
 0174            return false;
 175        }
 176
 177        // If we emit a $ref to the base type (inheritance via allOf), ensure the base type schema exists.
 178        // Limit this to form payload inheritance to avoid registering unrelated base types (e.g., ValueType/Enum).
 9179        if (t.BaseType is not null && t.BaseType != typeof(object)
 9180            && (typeof(KrFormData).IsAssignableFrom(t) || typeof(KrMultipart).IsAssignableFrom(t)))
 181        {
 0182            BuildSchema(t.BaseType, built);
 183        }
 184
 9185        if (TryResolveSimpleOrReferenceBaseSchema(t, baseSchema, out resolved))
 186        {
 0187            return true;
 188        }
 189
 9190        if (baseSchema is OpenApiSchema arraySchema && TryResolveArrayWrapperDerivedSchema(t, built, arraySchema, out re
 191        {
 0192            return true;
 193        }
 194
 195        // Defer composition until after properties are processed.
 9196        schemaParent = baseSchema as OpenApiSchema;
 9197        return false;
 198    }
 199
 200    /// <summary>
 201    /// Determines whether a type has a base type that can participate in schema composition.
 202    /// </summary>
 203    /// <param name="t">The type being processed.</param>
 204    /// <returns><c>true</c> if the type derives from something other than <see cref="object"/>; otherwise <c>false</c>.
 205    private static bool HasComposableBaseType(Type t)
 55206        => t.BaseType is not null && t.BaseType != typeof(object);
 207
 208    /// <summary>
 209    /// Attempts to resolve a derived type immediately when its base schema is a simple schema or a reference.
 210    /// </summary>
 211    /// <param name="derivedType">The derived type being processed.</param>
 212    /// <param name="baseSchema">The schema resolved from the base type.</param>
 213    /// <param name="resolved">The resolved schema to return.</param>
 214    /// <returns><c>true</c> if the schema was resolved immediately; otherwise <c>false</c>.</returns>
 215    private bool TryResolveSimpleOrReferenceBaseSchema(
 216        Type derivedType,
 217        IOpenApiSchema baseSchema,
 218        out IOpenApiSchema resolved)
 219    {
 9220        resolved = null!;
 221
 9222        if (!IsSimpleSchemaOrReference(baseSchema))
 223        {
 9224            return false;
 225        }
 226
 0227        if (baseSchema is OpenApiSchema openApiSchema)
 228        {
 0229            ApplyTypeAttributes(derivedType, openApiSchema);
 0230            resolved = openApiSchema;
 0231            return true;
 232        }
 233
 0234        resolved = baseSchema;
 0235        return true;
 236    }
 237
 238    /// <summary>
 239    /// Determines whether a schema represents a "simple" base schema (not composed via <c>allOf</c>)
 240    /// or a schema reference.
 241    /// </summary>
 242    /// <param name="schema">The schema to check.</param>
 243    /// <returns><c>true</c> if the schema is simple or a reference; otherwise <c>false</c>.</returns>
 244    private static bool IsSimpleSchemaOrReference(IOpenApiSchema schema)
 245    {
 9246        return schema is OpenApiSchemaReference
 9247            || (schema.AllOf is null && schema.Type != JsonSchemaType.Array);
 248    }
 249
 250    /// <summary>
 251    /// Resolves the special case where a derived type represents an array wrapper and the array items
 252    /// may themselves be composed via <c>allOf</c>.
 253    /// </summary>
 254    /// <param name="derivedType">The derived type being processed.</param>
 255    /// <param name="built">The recursion guard set passed through schema-building.</param>
 256    /// <param name="arraySchema">The schema resolved from the base type.</param>
 257    /// <param name="resolved">The resolved schema to return.</param>
 258    /// <returns><c>true</c> if handled; otherwise <c>false</c>.</returns>
 259    private bool TryResolveArrayWrapperDerivedSchema(
 260        Type derivedType,
 261        HashSet<Type> built,
 262        OpenApiSchema arraySchema,
 263        out IOpenApiSchema resolved)
 264    {
 9265        resolved = null!;
 266
 9267        if (arraySchema.Type != JsonSchemaType.Array || arraySchema.Items is null)
 268        {
 9269            return false;
 270        }
 271
 0272        ApplyTypeAttributes(derivedType, arraySchema);
 273
 0274        if (arraySchema.Items is not OpenApiSchema itemSchema)
 275        {
 0276            resolved = arraySchema;
 0277            return true;
 278        }
 279
 280        // Preserve existing behavior (type-level attributes applied twice in this branch).
 0281        ApplyTypeAttributes(derivedType, arraySchema);
 282
 0283        if (itemSchema.AllOf is null)
 284        {
 0285            resolved = arraySchema;
 0286            return true;
 287        }
 288
 0289        var additional = CreateAllOfAdditionalObjectSchema(derivedType, built);
 0290        itemSchema.AllOf.Add(additional);
 0291        resolved = arraySchema;
 0292        return true;
 293    }
 294
 295    /// <summary>
 296    /// Creates the additional object schema appended to an existing <c>allOf</c> list for a derived type.
 297    /// </summary>
 298    /// <param name="t">The derived type whose properties should be added.</param>
 299    /// <param name="built">The recursion guard set passed through schema-building.</param>
 300    /// <returns>An <see cref="OpenApiSchema"/> containing only properties declared on <paramref name="t"/>.</returns>
 301    private OpenApiSchema CreateAllOfAdditionalObjectSchema(Type t, HashSet<Type> built)
 302    {
 0303        var additional = new OpenApiSchema
 0304        {
 0305            Type = JsonSchemaType.Object,
 0306            Properties = new Dictionary<string, IOpenApiSchema>(StringComparer.Ordinal)
 0307        };
 308
 0309        ProcessTypeProperties(t, additional, built);
 0310        return additional;
 311    }
 312
 313    /// <summary>
 314    /// Creates the base schema instance for a type, initializing object properties only when
 315    /// the type declares at least one public instance property.
 316    /// </summary>
 317    /// <param name="t">The CLR type being processed.</param>
 318    /// <returns>An <see cref="OpenApiSchema"/> ready for property population.</returns>
 319    private static OpenApiSchema CreateSchemaForDeclaredProperties(Type t)
 320    {
 55321        var schema = new OpenApiSchema();
 55322        var declaredPropsCount =
 55323            t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
 223324             .Count(p => p.DeclaringType == t);
 325
 55326        if (declaredPropsCount > 0)
 327        {
 47328            schema.Type = JsonSchemaType.Object;
 47329            schema.Properties = new Dictionary<string, IOpenApiSchema>(StringComparer.Ordinal);
 330        }
 331
 55332        return schema;
 333    }
 334
 335    /// <summary>
 336    /// Composes a child schema into a parent schema when inheritance composition is active.
 337    /// </summary>
 338    /// <param name="schemaParent">The parent schema built from the base type (if any).</param>
 339    /// <param name="schema">The derived schema with properties populated.</param>
 340    /// <returns>The composed schema to return from schema generation.</returns>
 341    private static IOpenApiSchema ComposeWithParentSchema(OpenApiSchema? schemaParent, OpenApiSchema schema)
 342    {
 46343        if (schemaParent is null)
 344        {
 41345            return schema;
 346        }
 347
 5348        if (schemaParent.AllOf is not null)
 349        {
 5350            schemaParent.AllOf.Add(schema);
 5351            schemaParent.Type = null; // Clear type when using allOf
 5352            return schemaParent;
 353        }
 354
 0355        if (schemaParent.Type == JsonSchemaType.Array)
 356        {
 0357            if (schemaParent.Items is OpenApiSchema items && items.AllOf is not null)
 358            {
 0359                items.AllOf.Add(schema);
 360            }
 361
 0362            return schemaParent;
 363        }
 364
 0365        return schema;
 366    }
 367
 368    /// <summary>
 369    /// Builds schema for custom base type derivations.
 370    /// </summary>
 371    ///  <param name="t">Type to build schema for</param>
 372    /// <returns>OpenApiSchema representing the base type derivation, or null if not applicable</returns>
 373    private static IOpenApiSchema? BuildBaseTypeSchema(Type t)
 374    {
 9375        if (PrimitiveSchemaMap.TryGetValue(t.BaseType!, out var value))
 376        {
 0377            return value();
 378        }
 379
 380        // Fallback to custom base type schema building
 9381        return BuildCustomBaseTypeSchema(t);
 382    }
 383
 384    /// <summary>
 385    /// Builds schema for types with custom base types.
 386    /// </summary>
 387    /// <param name="t">Type to build schema for</param>
 388    /// <returns>OpenApiSchema representing the custom base type derivation</returns>
 389    private static IOpenApiSchema BuildCustomBaseTypeSchema(Type t)
 390    {
 9391        var attributes = t.CustomAttributes.ToArray();
 392        // Count declared properties
 9393        var declaredPropsCount =
 9394            t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
 11395             .Count(p => p.DeclaringType == t);
 396
 397        // Check for the special case where the derived type only adds array semantics
 9398        var hasArray =
 9399            attributes.Length > 0 &&
 9400            attributes[0].NamedArguments.Any(na =>
 9401                na.MemberName == "Array" &&
 9402                na.TypedValue.ArgumentType == typeof(bool) &&
 9403                na.TypedValue.Value is bool b && b);
 404        // If so, we can represent this as a simple reference to the base type
 9405        if (declaredPropsCount == 0 && attributes.Length == 1)
 406        {
 0407            if (hasArray)
 408            {
 0409                return new OpenApiSchema
 0410                {
 0411                    Type = JsonSchemaType.Array,
 0412                    Items = new OpenApiSchemaReference(t.BaseType!.Name)
 0413                };
 414            }
 415            // If the derived type has AdditionalProperties, we can't use allOf
 0416            return new OpenApiSchemaReference(t.BaseType!.Name);
 417        }
 418        // Otherwise, build an allOf schema referencing the base type
 9419        var schema = new OpenApiSchema
 9420        {
 9421            AllOf = [new OpenApiSchemaReference(t.BaseType!.Name)]
 9422        };
 423        // Apply array semantics if specified
 9424        return hasArray
 9425            ? new OpenApiSchema
 9426            {
 9427                Type = JsonSchemaType.Array,
 9428                Items = schema
 9429            }
 9430            : schema;
 431    }
 432
 433    /// <summary>
 434    /// Registers an enum type schema in the document components.
 435    /// </summary>
 436    /// <returns>The registered enum schema.</returns>
 437    private OpenApiSchema RegisterEnumSchema(Type enumType)
 438    {
 10439        var enumSchema = new OpenApiSchema
 10440        {
 10441            Type = JsonSchemaType.String,
 120442            Enum = [.. enumType.GetEnumNames().Select(n => (JsonNode)n)]
 10443        };
 10444        if (Document.Components?.Schemas is not null)
 445        {
 10446            Document.Components.Schemas[enumType.Name] = enumSchema;
 447        }
 10448        return enumSchema;
 449    }
 450
 451    /// <summary>
 452    /// Applies type-level attributes to a schema.
 453    /// </summary>
 454    /// <param name="t">The type being processed.</param>
 455    /// <param name="schema">The schema to apply attributes to.</param>
 456    private void ApplyTypeAttributes(Type t, OpenApiSchema schema)
 457    {
 67458        ApplySchemaComponentAttributes(t, schema);
 67459        ApplyGeneratedRequiredPropertiesMetadata(t, schema);
 67460        ApplyPatternProperties(t, schema);
 67461    }
 462
 463    /// <summary>
 464    /// Applies required properties from generated PowerShell class metadata when available.
 465    /// </summary>
 466    /// <param name="t">The type being processed.</param>
 467    /// <param name="schema">The schema to update.</param>
 468    private static void ApplyGeneratedRequiredPropertiesMetadata(Type t, OpenApiSchema schema)
 469    {
 67470        var requiredProp = t.GetProperty("RequiredProperties", BindingFlags.Public | BindingFlags.NonPublic | BindingFla
 67471        if (requiredProp?.GetValue(null) is not System.Collections.IEnumerable requiredValues)
 472        {
 67473            return;
 474        }
 475
 0476        var required = requiredValues
 0477            .Cast<object?>()
 0478            .Select(v => v?.ToString())
 0479            .Where(v => !string.IsNullOrWhiteSpace(v))
 0480            .Cast<string>()
 0481            .Distinct(StringComparer.Ordinal)
 0482            .ToArray();
 483
 0484        if (required.Length == 0)
 485        {
 0486            return;
 487        }
 488
 0489        schema.Required ??= new HashSet<string>(StringComparer.Ordinal);
 0490        foreach (var name in required)
 491        {
 0492            _ = schema.Required.Add(name);
 493        }
 0494    }
 495
 496    /// <summary>
 497    /// Applies OpenApiSchemaComponent attributes and related examples to the schema.
 498    /// </summary>
 499    /// <param name="t">The type being processed.</param>
 500    /// <param name="schema">The schema to apply attributes to.</param>
 501    private void ApplySchemaComponentAttributes(Type t, OpenApiSchema schema)
 502    {
 67503        var schemaAttribute = t.GetCustomAttributes(true)
 67504            .OfType<OpenApiSchemaComponent>()
 67505            .FirstOrDefault();
 506
 67507        if (schemaAttribute is null)
 508        {
 41509            return;
 510        }
 511
 26512        ApplySchemaAttr(schemaAttribute, schema);
 26513        ApplySchemaComponentExamples(schemaAttribute, schema);
 26514    }
 515
 516    /// <summary>
 517    /// Applies OpenApiSchemaComponent example metadata to the schema.
 518    /// </summary>
 519    /// <param name="schemaAttribute">The schema component attribute.</param>
 520    /// <param name="schema">The schema to update.</param>
 521    private static void ApplySchemaComponentExamples(OpenApiSchemaComponent schemaAttribute, OpenApiSchema schema)
 522    {
 26523        if (schemaAttribute.Examples is null)
 524        {
 26525            return;
 526        }
 527
 0528        schema.Examples ??= [];
 0529        var node = OpenApiJsonNodeFactory.ToNode(schemaAttribute.Examples);
 0530        if (node is not null)
 531        {
 0532            schema.Examples.Add(node);
 533        }
 0534    }
 535
 536    /// <summary>
 537    /// Applies OpenApiPatternProperties attributes to the schema.
 538    /// </summary>
 539    /// <param name="t">The type being processed.</param>
 540    /// <param name="schema">The schema to update.</param>
 541    private void ApplyPatternProperties(Type t, OpenApiSchema schema)
 542    {
 142543        foreach (var pattern in t.GetCustomAttributes(true)
 67544                     .OfType<OpenApiPatternPropertiesAttribute>())
 545        {
 4546            var patternSchema = BuildPatternSchema(pattern);
 4547            if (patternSchema is null || string.IsNullOrWhiteSpace(pattern.KeyPattern))
 548            {
 549                continue;
 550            }
 551
 4552            schema.PatternProperties ??= new Dictionary<string, IOpenApiSchema>(StringComparer.Ordinal);
 4553            schema.PatternProperties[pattern.KeyPattern] = patternSchema;
 554        }
 67555    }
 556
 557    /// <summary>
 558    /// Builds the pattern schema for a pattern properties attribute.
 559    /// </summary>
 560    /// <param name="pattern">The pattern properties attribute.</param>
 561    /// <returns>The resolved pattern schema or <c>null</c> when no schema type is specified.</returns>
 562    private IOpenApiSchema? BuildPatternSchema(OpenApiPatternPropertiesAttribute pattern)
 563    {
 4564        if (pattern.SchemaType is null)
 565        {
 0566            return null;
 567        }
 568
 4569        var schemaType = pattern.SchemaType;
 4570        HashSet<Type>? built = null;
 4571        if (!schemaType.IsArray)
 572        {
 4573            return BuildSchemaForType(schemaType, built);
 574        }
 575
 0576        var item = schemaType.GetElementType()!;
 0577        var itemSchema = BuildSchemaForType(item, built);
 0578        return new OpenApiSchema
 0579        {
 0580            Type = JsonSchemaType.Array,
 0581            Items = itemSchema
 0582        };
 583    }
 584    /// <summary>
 585    /// Processes all properties of a type and builds their schemas.
 586    /// </summary>
 587    /// <param name="t">The type being processed.</param>
 588    /// <param name="schema">The schema to populate with properties.</param>
 589    /// <param name="built">The recursion guard set passed through schema-building.</param>
 590    private void ProcessTypeProperties(Type t, OpenApiSchema schema, HashSet<Type> built)
 591    {
 46592        var instance = TryCreateTypeInstance(t);
 46593        var isFormModel = t.GetCustomAttributes(inherit: true)
 156594            .Any(a => a.GetType().Name.Equals("KrBindFormAttribute", StringComparison.OrdinalIgnoreCase));
 595
 422596        foreach (var prop in t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
 212597                      .Where(p => p.DeclaringType == t))
 598        {
 165599            if (ShouldSkipRuntimeFormPayloadStorageProperty(t, prop))
 600            {
 601                continue;
 602            }
 603
 161604            var propSchema = BuildPropertySchema(prop, built);
 161605            CapturePropertyDefault(instance, prop, propSchema);
 606
 161607            if (isFormModel && prop.GetCustomAttribute<KrPartAttribute>(inherit: false) is { Required: true })
 608            {
 8609                schema.Required ??= new HashSet<string>(StringComparer.Ordinal);
 8610                _ = schema.Required.Add(prop.Name);
 611            }
 161612            schema.Properties?.Add(prop.Name, propSchema);
 613        }
 46614    }
 615
 616    /// <summary>
 617    /// Returns <c>true</c> when a property should be excluded from OpenAPI schema generation because
 618    /// it represents runtime storage on Kestrun form payload base types.
 619    /// </summary>
 620    /// <remarks>
 621    /// <para>
 622    /// <see cref="KrFormData"/> and <see cref="KrMultipart"/> are runtime containers.
 623    /// Their storage properties (<c>Fields</c>/<c>Files</c>/<c>Parts</c>) are not part of the public request/response c
 624    /// Concrete models should declare expected parts as properties.
 625    /// </para>
 626    /// </remarks>
 627    private static bool ShouldSkipRuntimeFormPayloadStorageProperty(Type declaringType, PropertyInfo prop)
 628    {
 165629        return declaringType == typeof(KrFormData)
 165630            ? prop.Name is nameof(KrFormData.Fields) or nameof(KrFormData.Files)
 165631            : declaringType == typeof(KrMultipart) && prop.Name == nameof(KrMultipart.Parts);
 632    }
 633
 634    /// <summary>
 635    /// Attempts to create an instance of a type to capture default values.
 636    /// </summary>
 637    private static object? TryCreateTypeInstance(Type t)
 638    {
 639        try
 640        {
 46641            return Activator.CreateInstance(t);
 642        }
 4643        catch
 644        {
 4645            return null;
 646        }
 46647    }
 648
 649    /// <summary>
 650    /// Captures the default value of a property if not already set.
 651    /// </summary>
 652    private static void CapturePropertyDefault(object? instance, PropertyInfo prop, IOpenApiSchema propSchema)
 653    {
 161654        if (instance is null || propSchema is not OpenApiSchema concrete || concrete.Default is not null)
 655        {
 36656            return;
 657        }
 658
 659        try
 660        {
 125661            var value = prop.GetValue(instance);
 125662            if (!IsIntrinsicDefault(value, prop.PropertyType))
 663            {
 53664                concrete.Default = OpenApiJsonNodeFactory.ToNode(value);
 665            }
 125666        }
 0667        catch
 668        {
 669            // Ignore failures when capturing defaults
 0670        }
 125671    }
 672
 673    /// <summary>
 674    /// Determines if a value is the intrinsic default for its declared type.
 675    /// </summary>
 676    /// <param name="value">The value to check.</param>
 677    /// <param name="declaredType">The declared type of the value.</param>
 678    /// <returns>True if the value is the intrinsic default for its declared type; otherwise, false.</returns>
 679    private static bool IsIntrinsicDefault(object? value, Type declaredType)
 680    {
 138681        if (value is null)
 682        {
 26683            return true;
 684        }
 685
 686        // Unwrap Nullable<T>
 112687        var t = Nullable.GetUnderlyingType(declaredType) ?? declaredType;
 688
 689        // Reference types: null is the only intrinsic default
 112690        if (!t.IsValueType)
 691        {
 49692            return false;
 693        }
 694
 695        // Special-cases for common structs
 63696        if (t == typeof(Guid))
 697        {
 5698            return value.Equals(Guid.Empty);
 699        }
 700
 58701        if (t == typeof(TimeSpan))
 702        {
 1703            return value.Equals(TimeSpan.Zero);
 704        }
 705
 57706        if (t == typeof(DateTime))
 707        {
 5708            return value.Equals(default(DateTime));
 709        }
 710
 52711        if (t == typeof(DateTimeOffset))
 712        {
 1713            return value.Equals(default(DateTimeOffset));
 714        }
 715
 716        // Enums: 0 is intrinsic default
 51717        if (t.IsEnum)
 718        {
 2719            return Convert.ToInt64(value) == 0;
 720        }
 721
 722        // Primitive/value types: compare to default(T)
 49723        var def = Activator.CreateInstance(t);
 49724        return value.Equals(def);
 725    }
 726
 727    /// <summary>
 728    /// Makes an OpenApiSchema nullable if specified.
 729    /// </summary>
 730    /// <param name="schema">The OpenApiSchema to modify.</param>
 731    /// <param name="isNullable">Indicates whether the schema should be nullable.</param>
 732    /// <returns>The modified OpenApiSchema.</returns>
 733    private static OpenApiSchema MakeNullable(OpenApiSchema schema, bool isNullable)
 734    {
 25735        if (isNullable)
 736        {
 2737            schema.Type |= JsonSchemaType.Null;
 738        }
 25739        return schema;
 740    }
 741
 742    /// <summary>
 743    /// Infers a primitive OpenApiSchema from a .NET type.
 744    /// </summary>
 745    /// <param name="type">The .NET type to infer from.</param>
 746    /// <param name="inline">Indicates if the schema should be inlined.</param>
 747    /// <returns>The inferred OpenApiSchema.</returns>
 748    public IOpenApiSchema InferPrimitiveSchema(Type type, bool inline = false)
 749    {
 25750        var nullable = false;
 25751        if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)
 25752            && type.GetGenericArguments().Length == 1)
 753        {
 1754            type = type.GetGenericArguments()[0];
 1755            nullable = true;
 756        }
 757        // Direct type mappings
 25758        if (PrimitiveSchemaMap.TryGetValue(type, out var schemaFactory))
 759        {
 23760            return MakeNullable(schemaFactory(), nullable);
 761        }
 762
 763        // Array type handling
 2764        if (type.Name.EndsWith("[]"))
 765        {
 1766            return InferArraySchema(type, inline);
 767        }
 768
 769        // Special handling for PowerShell OpenAPI classes
 1770        if (PowerShellOpenApiClassExporter.ValidClassNames.Contains(type.Name))
 771        {
 0772            return InferPowerShellClassSchema(type, inline);
 773        }
 774
 775        // Fallback
 1776        return new OpenApiSchema { Type = JsonSchemaType.String };
 777    }
 778
 779    /// <summary>
 780    /// Infers an array OpenApiSchema from a .NET array type.
 781    /// </summary>
 782    /// <param name="type">The .NET array type to infer from.</param>
 783    /// <param name="inline">Indicates if the schema should be inlined.</param>
 784    /// <returns>The inferred OpenApiSchema.</returns>
 785    private OpenApiSchema InferArraySchema(Type type, bool inline)
 786    {
 2787        var typeName = type.Name[..^2];
 2788        if (ComponentSchemasExists(typeName))
 789        {
 0790            var items = inline ? GetSchema(typeName).Clone() : new OpenApiSchemaReference(typeName);
 0791            return new OpenApiSchema { Type = JsonSchemaType.Array, Items = items };
 792        }
 793
 2794        return new OpenApiSchema { Type = JsonSchemaType.Array, Items = InferPrimitiveSchema(type.GetElementType() ?? ty
 795    }
 796
 797    /// <summary>
 798    /// Infers a PowerShell OpenAPI class schema.
 799    /// </summary>
 800    /// <param name="type">The .NET type representing the PowerShell OpenAPI class.</param>
 801    /// <param name="inline">Indicates if the schema should be inlined.</param>
 802    /// <returns>The inferred OpenApiSchema.</returns>
 803    private IOpenApiSchema InferPowerShellClassSchema(Type type, bool inline)
 804    {
 0805        if (TryGetSchemaItem(type.Name, out var schema, out var isInline))
 806        {
 0807            if (inline || isInline)
 808            {
 0809                if (schema is OpenApiSchema concreteSchema)
 810                {
 0811                    return concreteSchema.Clone();
 812                }
 813            }
 814            else
 815            {
 0816                return new OpenApiSchemaReference(type.Name);
 817            }
 818        }
 819
 0820        Host.Logger.Warning("Schema for PowerShell OpenAPI class '{typeName}' not found. Defaulting to string schema.", 
 0821        return new OpenApiSchema { Type = JsonSchemaType.String };
 822    }
 823
 824    /// <summary>
 825    /// Mapping of .NET primitive types to OpenAPI schema definitions.
 826    /// </summary>
 827    /// <remarks>
 828    /// This dictionary maps common .NET primitive types to their corresponding OpenAPI schema representations.
 829    /// Each entry consists of a .NET type as the key and a function that returns an OpenApiSchema as the value.
 830    /// </remarks>
 1831    private static readonly Dictionary<Type, Func<OpenApiSchema>> PrimitiveSchemaMap = new()
 1832    {
 61833        [typeof(string)] = () => new OpenApiSchema { Type = JsonSchemaType.String },
 24834        [typeof(bool)] = () => new OpenApiSchema { Type = JsonSchemaType.Boolean },
 0835        [typeof(long)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int64" },
 5836        [typeof(DateTime)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "date-time" },
 1837        [typeof(DateTimeOffset)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "date-time" },
 0838        [typeof(TimeSpan)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "duration" },
 3839        [typeof(byte[])] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "binary" },
 1840        [typeof(Uri)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "uri" },
 7841        [typeof(Guid)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "uuid" },
 8842        [typeof(object)] = () => new OpenApiSchema { Type = JsonSchemaType.Object },
 0843        [typeof(void)] = () => new OpenApiSchema { Type = JsonSchemaType.Null },
 3844        [typeof(char)] = () => new OpenApiSchema { Type = JsonSchemaType.String, MaxLength = 1, MinLength = 1 },
 3845        [typeof(sbyte)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int32" },
 3846        [typeof(byte)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int32" },
 3847        [typeof(short)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int32" },
 3848        [typeof(ushort)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int32" },
 22849        [typeof(int)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int32" },
 3850        [typeof(uint)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int32" },
 9851        [typeof(long)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int64" },
 3852        [typeof(ulong)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int64" },
 3853        [typeof(float)] = () => new OpenApiSchema { Type = JsonSchemaType.Number, Format = "float" },
 5854        [typeof(double)] = () => new OpenApiSchema { Type = JsonSchemaType.Number, Format = "double" },
 4855        [typeof(decimal)] = () => new OpenApiSchema { Type = JsonSchemaType.Number, Format = "decimal" },
 4856        [typeof(OpenApiString)] = () => new OpenApiSchema { Type = JsonSchemaType.String },
 0857        [typeof(OpenApiUuid)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "uuid" },
 0858        [typeof(OpenApiDate)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "date" },
 0859        [typeof(OpenApiDateTime)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "date-time" },
 0860        [typeof(OpenApiEmail)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "email" },
 0861        [typeof(OpenApiBinary)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "binary" },
 0862        [typeof(OpenApiHostname)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "hostname" },
 0863        [typeof(OpenApiIpv4)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "ipv4" },
 0864        [typeof(OpenApiIpv6)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "ipv6" },
 0865        [typeof(OpenApiUri)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "uri" },
 0866        [typeof(OpenApiUrl)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "url" },
 0867        [typeof(OpenApiByte)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "byte" },
 0868        [typeof(OpenApiPassword)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "password" },
 0869        [typeof(OpenApiRegex)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "regex" },
 0870        [typeof(OpenApiJson)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "json" },
 0871        [typeof(OpenApiXmlModel)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "xml" },
 0872        [typeof(OpenApiYaml)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "yaml" },
 1873
 3874        [typeof(OpenApiInteger)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer },
 0875        [typeof(OpenApiInt32)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int32" },
 0876        [typeof(OpenApiInt64)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int64" },
 1877
 3878        [typeof(OpenApiNumber)] = () => new OpenApiSchema { Type = JsonSchemaType.Number },
 0879        [typeof(OpenApiFloat)] = () => new OpenApiSchema { Type = JsonSchemaType.Number, Format = "float" },
 0880        [typeof(OpenApiDouble)] = () => new OpenApiSchema { Type = JsonSchemaType.Number, Format = "double" },
 1881
 3882        [typeof(OpenApiBoolean)] = () => new OpenApiSchema { Type = JsonSchemaType.Boolean, Format = "boolean" },
 1883    };
 884
 885    /// <summary>
 886    /// Applies schema attributes to an OpenAPI schema.
 887    /// </summary>
 888    /// <param name="oaProperties">The OpenApiProperties containing attributes to apply.</param>
 889    /// <param name="ioaSchema">The OpenAPI schema to apply attributes to.</param>
 890    private void ApplySchemaAttr(OpenApiProperties? oaProperties, IOpenApiSchema ioaSchema)
 891    {
 222892        if (oaProperties is null)
 893        {
 164894            return;
 895        }
 896
 897        // Most models implement OpenApiSchema (concrete) OR OpenApiSchemaReference.
 898        // We set common metadata when possible (Description/Title apply only to concrete schema).
 58899        if (ioaSchema is OpenApiSchema concreteSchema)
 900        {
 50901            ApplyConcreteSchemaAttributes(oaProperties, concreteSchema);
 50902            return;
 903        }
 904
 8905        if (ioaSchema is OpenApiSchemaReference refSchema)
 906        {
 8907            ApplyReferenceSchemaAttributes(oaProperties, refSchema);
 908        }
 8909    }
 910
 911    /// <summary>
 912    /// Applies concrete schema attributes to an OpenApiSchema.
 913    /// </summary>
 914    /// <param name="properties">The OpenApiProperties containing attributes to apply.</param>
 915    /// <param name="schema">The OpenApiSchema to apply attributes to.</param>
 916    private void ApplyConcreteSchemaAttributes(OpenApiProperties properties, OpenApiSchema schema)
 917    {
 52918        ApplyTitleAndDescription(properties, schema);
 52919        ApplySchemaType(properties, schema);
 52920        ApplyFormatAndNumericBounds(properties, schema);
 52921        ApplyLengthAndPattern(properties, schema);
 52922        ApplyCollectionConstraints(properties, schema);
 52923        ApplyFlags(properties, schema);
 52924        ApplyExamplesAndDefaults(properties, schema);
 52925        ApplyXmlMetadata(properties, schema);
 52926        if (schema.Type == null && (schema.AdditionalProperties is not null || schema.AdditionalPropertiesAllowed || sch
 927        {
 0928            schema.Type = JsonSchemaType.Object;
 929        }
 52930    }
 931
 932    /// <summary>
 933    /// Applies title and description to an OpenApiSchema.
 934    /// </summary>
 935    /// <param name="properties">The OpenApiProperties containing attributes to apply.</param>
 936    /// <param name="schema">The OpenApiSchema to apply attributes to.</param>
 937    private static void ApplyTitleAndDescription(OpenApiProperties properties, OpenApiSchema schema)
 938    {
 52939        if (properties.Title is not null)
 940        {
 0941            schema.Title = properties.Title;
 942        }
 52943        if (properties is not OpenApiParameterComponentAttribute && properties.Description is not null)
 944        {
 35945            schema.Description = properties.Description;
 946        }
 52947    }
 948
 949    /// <summary>
 950    /// Applies schema type and nullability to an OpenApiSchema.
 951    /// </summary>
 952    /// <param name="properties">The OpenApiProperties containing attributes to apply.</param>
 953    /// <param name="schema">The OpenApiSchema to apply attributes to.</param>
 954    private static void ApplySchemaType(OpenApiProperties properties, OpenApiSchema schema)
 955    {
 52956        if (properties.Type != OaSchemaType.None)
 957        {
 4958            schema.Type = properties.Type switch
 4959            {
 0960                OaSchemaType.String => JsonSchemaType.String,
 0961                OaSchemaType.Number => JsonSchemaType.Number,
 0962                OaSchemaType.Integer => JsonSchemaType.Integer,
 0963                OaSchemaType.Boolean => JsonSchemaType.Boolean,
 0964                OaSchemaType.Array => JsonSchemaType.Array,
 4965                OaSchemaType.Object => JsonSchemaType.Object,
 0966                OaSchemaType.Null => JsonSchemaType.Null,
 0967                _ => schema.Type
 4968            };
 969        }
 970
 52971        if (properties.Nullable)
 972        {
 0973            schema.Type |= JsonSchemaType.Null;
 974        }
 52975    }
 976
 977    /// <summary>
 978    /// Applies format and numeric bounds to an OpenApiSchema.
 979    /// </summary>
 980    /// <param name="properties">The OpenApiProperties containing attributes to apply.</param>
 981    /// <param name="schema"></param>
 982    private static void ApplyFormatAndNumericBounds(OpenApiProperties properties, OpenApiSchema schema)
 983    {
 52984        if (!string.IsNullOrWhiteSpace(properties.Format))
 985        {
 0986            schema.Format = properties.Format;
 987        }
 988
 52989        if (properties.MultipleOf.HasValue)
 990        {
 0991            schema.MultipleOf = properties.MultipleOf;
 992        }
 993
 52994        if (!string.IsNullOrWhiteSpace(properties.Maximum))
 995        {
 0996            schema.Maximum = properties.Maximum;
 0997            if (properties.ExclusiveMaximum)
 998            {
 0999                schema.ExclusiveMaximum = properties.Maximum;
 1000            }
 1001        }
 1002
 521003        if (!string.IsNullOrWhiteSpace(properties.Minimum))
 1004        {
 01005            schema.Minimum = properties.Minimum;
 01006            if (properties.ExclusiveMinimum)
 1007            {
 01008                schema.ExclusiveMinimum = properties.Minimum;
 1009            }
 1010        }
 521011    }
 1012
 1013    /// <summary>
 1014    /// Applies length and pattern constraints to an OpenApiSchema.
 1015    /// </summary>
 1016    /// <param name="properties">The OpenApiProperties containing attributes to apply.</param>
 1017    /// <param name="schema"></param>
 1018    private static void ApplyLengthAndPattern(OpenApiProperties properties, OpenApiSchema schema)
 1019    {
 521020        if (properties.MaxLength >= 0)
 1021        {
 01022            schema.MaxLength = properties.MaxLength;
 1023        }
 1024
 521025        if (properties.MinLength >= 0)
 1026        {
 01027            schema.MinLength = properties.MinLength;
 1028        }
 1029
 521030        if (!string.IsNullOrWhiteSpace(properties.Pattern))
 1031        {
 01032            schema.Pattern = properties.Pattern;
 1033        }
 521034    }
 1035
 1036    /// <summary>
 1037    /// Applies collection constraints to an OpenApiSchema.
 1038    /// </summary>
 1039    /// <param name="properties">The OpenApiProperties containing attributes to apply.</param>
 1040    /// <param name="schema">The OpenApiSchema to apply attributes to.</param>
 1041    private static void ApplyCollectionConstraints(OpenApiProperties properties, OpenApiSchema schema)
 1042    {
 521043        if (properties.MaxItems >= 0)
 1044        {
 01045            schema.MaxItems = properties.MaxItems;
 1046        }
 1047
 521048        if (properties.MinItems >= 0)
 1049        {
 01050            schema.MinItems = properties.MinItems;
 1051        }
 1052
 521053        if (properties.UniqueItems)
 1054        {
 01055            schema.UniqueItems = true;
 1056        }
 1057
 521058        if (properties.MaxProperties >= 0)
 1059        {
 01060            schema.MaxProperties = properties.MaxProperties;
 1061        }
 1062
 521063        if (properties.MinProperties >= 0)
 1064        {
 01065            schema.MinProperties = properties.MinProperties;
 1066        }
 521067    }
 1068
 1069    /// <summary>
 1070    /// Applies flags to an OpenApiSchema.
 1071    /// </summary>
 1072    /// <param name="properties"> The OpenApiProperties containing flags to apply.</param>
 1073    /// <param name="schema">The OpenApiSchema to apply flags to.</param>
 1074    private void ApplyFlags(OpenApiProperties properties, OpenApiSchema schema)
 1075    {
 521076        schema.ReadOnly = properties.ReadOnly;
 521077        schema.WriteOnly = properties.WriteOnly;
 521078        if (IsObjectSchemaType(schema.Type))
 1079        {
 261080            schema.AdditionalPropertiesAllowed = properties.AdditionalPropertiesAllowed;
 261081            ApplyAdditionalProperties(properties, schema);
 1082        }
 1083        else
 1084        {
 261085            schema.AdditionalPropertiesAllowed = true; // Non-object schemas must allow additional properties to be vali
 1086        }
 521087        schema.UnevaluatedProperties = properties.UnevaluatedProperties;
 521088        if (properties is not OpenApiParameterComponentAttribute)
 1089        {
 501090            schema.Deprecated = properties.Deprecated;
 1091        }
 521092    }
 1093
 1094    /// <summary>
 1095    /// Applies additional properties schema settings when enabled.
 1096    /// </summary>
 1097    /// <param name="properties">The OpenApiProperties containing flags to apply.</param>
 1098    /// <param name="schema">The OpenApiSchema to apply additional properties to.</param>
 1099    private void ApplyAdditionalProperties(OpenApiProperties properties, OpenApiSchema schema)
 1100    {
 261101        if (!properties.AdditionalPropertiesAllowed || properties.AdditionalProperties is null)
 1102        {
 221103            return;
 1104        }
 1105
 41106        HashSet<Type>? built = null;
 41107        if (properties.AdditionalProperties.IsArray)
 1108        {
 01109            ApplyArrayAdditionalProperties(properties, schema, built);
 01110            return;
 1111        }
 1112
 41113        schema.AdditionalProperties = BuildSchemaForType(properties.AdditionalProperties, built);
 41114        EnsureAdditionalPropertiesAllowed(schema.AdditionalProperties, schema);
 41115    }
 1116
 1117    /// <summary>
 1118    /// Applies array-based additional properties schema settings.
 1119    /// </summary>
 1120    /// <param name="properties">The OpenApiProperties containing array metadata.</param>
 1121    /// <param name="schema">The OpenApiSchema to apply additional properties to.</param>
 1122    /// <param name="built">The recursion guard set passed through schema-building.</param>
 1123    private void ApplyArrayAdditionalProperties(OpenApiProperties properties, OpenApiSchema schema, HashSet<Type>? built
 1124    {
 01125        var item = properties.AdditionalProperties!.GetElementType()!;
 1126
 01127        var itemSchema = BuildSchemaForType(item, built);
 01128        EnsureAdditionalPropertiesAllowed(itemSchema, schema);
 01129        schema.AdditionalProperties = new OpenApiSchema
 01130        {
 01131            Type = JsonSchemaType.Array,
 01132            Items = itemSchema
 01133        };
 01134    }
 1135
 1136    /// <summary>
 1137    /// Ensures additional properties are allowed on non-object additional property schemas when applicable.
 1138    /// </summary>
 1139    /// <param name="additionalSchema">The additional properties schema.</param>
 1140    /// <param name="targetSchema">The target schema.</param>
 1141    private static void EnsureAdditionalPropertiesAllowed(IOpenApiSchema? additionalSchema, OpenApiSchema targetSchema)
 1142    {
 41143        if (additionalSchema is OpenApiSchema apiSchema
 41144            && !IsObjectSchemaType(apiSchema.Type)
 41145            && !apiSchema.AdditionalPropertiesAllowed
 41146            && targetSchema.AdditionalProperties is OpenApiSchema targetAdditional)
 1147        {
 01148            targetAdditional.AdditionalPropertiesAllowed = true;
 1149        }
 41150    }
 1151
 1152    /// <summary>
 1153    /// Determines whether a schema type includes object semantics, including nullable object unions.
 1154    /// </summary>
 1155    /// <param name="schemaType">The schema type value to evaluate.</param>
 1156    /// <returns><c>true</c> when object is present in the schema type flags; otherwise, <c>false</c>.</returns>
 1157    private static bool IsObjectSchemaType(JsonSchemaType? schemaType)
 561158        => schemaType is not null && (schemaType.Value & JsonSchemaType.Object) == JsonSchemaType.Object;
 1159
 1160    /// <summary>
 1161    /// Applies examples and default values to an OpenApiSchema.
 1162    /// </summary>
 1163    /// <param name="properties">The OpenApiProperties containing example and default values.</param>
 1164    /// <param name="schema">The OpenApiSchema to apply examples and defaults to.</param>
 1165    private static void ApplyExamplesAndDefaults(OpenApiProperties properties, OpenApiSchema schema)
 1166    {
 521167        if (properties.Default is not null)
 1168        {
 01169            schema.Default = OpenApiJsonNodeFactory.ToNode(properties.Default);
 1170        }
 521171        if (properties.Example is not null && properties is not OpenApiParameterComponentAttribute)
 1172        {
 01173            schema.Example = OpenApiJsonNodeFactory.ToNode(properties.Example);
 1174        }
 1175
 521176        if (properties.Enum is { Length: > 0 })
 1177        {
 01178            schema.Enum = [.. properties.Enum.Select(OpenApiJsonNodeFactory.ToNode).OfType<JsonNode>()];
 1179        }
 1180
 521181        if (properties.RequiredProperties is { Length: > 0 })
 1182        {
 01183            schema.Required ??= new HashSet<string>(StringComparer.Ordinal);
 01184            foreach (var r in properties.RequiredProperties)
 1185            {
 01186                _ = schema.Required.Add(r);
 1187            }
 1188        }
 521189    }
 1190
 1191    /// <summary>
 1192    /// Applies XML metadata to an OpenApiSchema.
 1193    /// </summary>
 1194    /// <param name="properties">The OpenApiProperties containing XML attributes to apply.</param>
 1195    /// <param name="schema">The OpenApiSchema to apply XML metadata to.</param>
 1196    private static void ApplyXmlMetadata(OpenApiProperties properties, OpenApiSchema schema)
 1197    {
 1198        // Check if any XML properties are set
 521199        var hasXmlMetadata = !string.IsNullOrWhiteSpace(properties.XmlName) ||
 521200                             !string.IsNullOrWhiteSpace(properties.XmlNamespace) ||
 521201                             !string.IsNullOrWhiteSpace(properties.XmlPrefix) ||
 521202                             properties.XmlAttribute ||
 521203                             properties.XmlWrapped;
 1204
 521205        if (!hasXmlMetadata)
 1206        {
 441207            return;
 1208        }
 1209
 1210        // Create XML object if it doesn't exist
 81211        schema.Xml ??= new OpenApiXmlModel();
 1212
 1213        // Apply standard XML properties (supported by Microsoft.OpenApi 3.1.2)
 81214        if (!string.IsNullOrWhiteSpace(properties.XmlName))
 1215        {
 21216            schema.Xml.Name = properties.XmlName;
 1217        }
 1218
 81219        if (!string.IsNullOrWhiteSpace(properties.XmlNamespace))
 1220        {
 21221            schema.Xml.Namespace = new Uri(properties.XmlNamespace);
 1222        }
 1223
 81224        if (!string.IsNullOrWhiteSpace(properties.XmlPrefix))
 1225        {
 11226            schema.Xml.Prefix = properties.XmlPrefix;
 1227        }
 1228
 1229        // Set NodeType based on XmlAttribute and XmlWrapped properties
 1230        // OpenAPI 3.2 uses NodeType to specify attribute vs element vs text nodes
 81231        if (properties.XmlAttribute)
 1232        {
 21233            schema.Xml.NodeType = OpenApiXmlNodeType.Attribute;
 1234        }
 61235        else if (properties.XmlWrapped)
 1236        {
 21237            schema.Xml.NodeType = OpenApiXmlNodeType.Element;
 1238        }
 61239    }
 1240
 1241    /// <summary>
 1242    /// Applies reference schema attributes to an OpenApiSchemaReference.
 1243    /// </summary>
 1244    /// <param name="properties">The OpenApiProperties containing attributes to apply.</param>
 1245    /// <param name="reference">The OpenApiSchemaReference to apply attributes to.</param>
 1246    private static void ApplyReferenceSchemaAttributes(OpenApiProperties properties, OpenApiSchemaReference reference)
 1247    {
 1248        // Description/Title can live on a reference proxy in v2 (and serialize alongside $ref)
 81249        if (!string.IsNullOrWhiteSpace(properties.Description))
 1250        {
 81251            reference.Description = properties.Description;
 1252        }
 1253
 81254        if (!string.IsNullOrWhiteSpace(properties.Title))
 1255        {
 01256            reference.Title = properties.Title;
 1257        }
 1258
 1259        // Example/Default/Enum aren’t typically set on the ref node itself;
 1260        // attach such metadata to the component target instead if you need it.
 81261    }
 1262    #endregion
 1263}

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Security.cs

#LineLine coverage
 1using Microsoft.OpenApi;
 2using Kestrun.Authentication;
 3
 4namespace Kestrun.OpenApi;
 5/// <summary>
 6/// Methods for applying security schemes to the OpenAPI document.
 7/// </summary>
 8public partial class OpenApiDocDescriptor
 9{
 10    /// <summary>
 11    /// Applies a security scheme to the OpenAPI document based on the provided authentication options.
 12    /// </summary>
 13    /// <param name="scheme">The name of the security scheme.</param>
 14    /// <param name="options">The authentication options.</param>
 15    /// <exception cref="NotSupportedException">Thrown when the authentication options type is not supported.</exception
 16    public void ApplySecurityScheme(string scheme, IOpenApiAuthenticationOptions options)
 17    {
 2318        var securityScheme = options switch
 2319        {
 620            ApiKeyAuthenticationOptions apiKeyOptions => GetSecurityScheme(apiKeyOptions),
 821            BasicAuthenticationOptions basicOptions => GetSecurityScheme(basicOptions),
 222            CookieAuthOptions cookieOptions => GetSecurityScheme(cookieOptions),
 323            JwtAuthOptions jwtOptions => GetSecurityScheme(jwtOptions),
 024            OAuth2Options oauth2Options => GetSecurityScheme(oauth2Options),
 025            OidcOptions oidcOptions => GetSecurityScheme(oidcOptions),
 126            WindowsAuthOptions windowsOptions => GetSecurityScheme(windowsOptions),
 327            ClientCertificateAuthenticationOptions clientCertificateOptions => GetSecurityScheme(clientCertificateOption
 028            _ => throw new NotSupportedException($"Unsupported authentication options type: {options.GetType().FullName}
 2329        };
 2330        AddSecurityComponent(scheme: scheme, globalScheme: options.GlobalScheme, securityScheme: securityScheme);
 2331    }
 32
 33    /// <summary>
 34    /// Gets the OpenAPI security scheme for mutual TLS (client certificate) authentication.
 35    /// </summary>
 36    /// <param name="options">The client certificate authentication options.</param>
 37    /// <returns>The OpenAPI security scheme for mutual TLS authentication.</returns>
 38    private static OpenApiSecurityScheme GetSecurityScheme(ClientCertificateAuthenticationOptions options)
 39    {
 340        return new OpenApiSecurityScheme
 341        {
 342            Type = SecuritySchemeType.MutualTLS,
 343            Description = options.Description ?? options.DisplayName,
 344            Deprecated = options.Deprecated
 345        };
 46    }
 47
 48    /// <summary>
 49    /// Gets the OpenAPI security scheme for Windows authentication.
 50    /// </summary>
 51    /// <param name="options">The Windows authentication options.</param>
 52    /// <returns>The OpenAPI security scheme for Windows authentication.</returns>
 53    private static OpenApiSecurityScheme GetSecurityScheme(WindowsAuthOptions options)
 54    {
 155        return new OpenApiSecurityScheme()
 156        {
 157            Type = SecuritySchemeType.Http,
 158            Scheme = options.Protocol == WindowsAuthProtocol.Ntlm ? "ntlm" : "negotiate",
 159            Description = options.Description,
 160            Deprecated = options.Deprecated
 161        };
 62    }
 63
 64    /// <summary>
 65    /// Gets the OpenAPI security scheme for OIDC authentication.
 66    /// </summary>
 67    /// <param name="options">The OIDC authentication options.</param>
 68    /// <returns></returns>
 69    /// <exception cref="InvalidOperationException">Thrown when neither Authority nor MetadataAddress is set.</exception
 70    private static OpenApiSecurityScheme GetSecurityScheme(OidcOptions options)
 71    {
 72        // Prefer explicit MetadataAddress if set
 073        var discoveryUrl = options.MetadataAddress
 074                           ?? (options.Authority is null
 075                               ? throw new InvalidOperationException(
 076                                   "Either Authority or MetadataAddress must be set to build OIDC OpenAPI scheme.")
 077                               : $"{options.Authority.TrimEnd('/')}/.well-known/openid-configuration");
 78
 079        return new OpenApiSecurityScheme
 080        {
 081            Type = SecuritySchemeType.OpenIdConnect,
 082            OpenIdConnectUrl = new Uri(discoveryUrl, UriKind.Absolute),
 083            // Description comes from AuthenticationSchemeOptions base class
 084            Description = options.Description,
 085            Deprecated = options.Deprecated
 086        };
 87    }
 88
 89    /// <summary>
 90    /// Gets the OpenAPI security scheme for OAuth2 authentication.
 91    /// </summary>
 92    /// <param name="options">The OAuth2 authentication options.</param>
 93    /// <returns></returns>
 94    private static OpenApiSecurityScheme GetSecurityScheme(OAuth2Options options)
 95    {
 96        // Build OAuth flows
 097        var flows = new OpenApiOAuthFlows
 098        {
 099            // Client Credentials flow
 0100            AuthorizationCode = new OpenApiOAuthFlow
 0101            {
 0102                AuthorizationUrl = new Uri(options.AuthorizationEndpoint, UriKind.Absolute),
 0103            }
 0104        };
 105        // Scopes
 0106        if (options.ClaimPolicy is not null && options.ClaimPolicy.Policies is not null && options.ClaimPolicy.Policies.
 107        {
 0108            var scopes = new Dictionary<string, string>();
 0109            var policies = options.ClaimPolicy.Policies;
 0110            foreach (var item in policies)
 111            {
 0112                scopes.Add(item.Key, item.Value.Description ?? string.Empty);
 113            }
 0114            flows.AuthorizationCode.Scopes = scopes;
 115        }
 116        // Token endpoint
 0117        if (options.TokenEndpoint is not null)
 118        {
 0119            flows.AuthorizationCode.TokenUrl = new Uri(options.TokenEndpoint, UriKind.Absolute);
 120        }
 121
 0122        return new OpenApiSecurityScheme()
 0123        {
 0124            Type = SecuritySchemeType.OAuth2,
 0125            Flows = flows,
 0126            Description = options.Description,
 0127            Deprecated = options.Deprecated
 0128        };
 129    }
 130    /// <summary>
 131    /// Gets the OpenAPI security scheme for API key authentication.
 132    /// </summary>
 133    /// <param name="options">The API key authentication options.</param>
 134    private static OpenApiSecurityScheme GetSecurityScheme(ApiKeyAuthenticationOptions options)
 135    {
 6136        return new OpenApiSecurityScheme()
 6137        {
 6138            Type = SecuritySchemeType.ApiKey,
 6139            Name = options.ApiKeyName,
 6140            In = options.In,
 6141            Description = options.Description,
 6142            Deprecated = options.Deprecated
 6143        };
 144    }
 145
 146    /// <summary>
 147    /// Gets the OpenAPI security scheme for cookie authentication.
 148    /// </summary>
 149    /// <param name="options">The cookie authentication options.</param>
 150    /// <returns></returns>
 151    private static OpenApiSecurityScheme GetSecurityScheme(CookieAuthOptions options)
 152    {
 2153        return new OpenApiSecurityScheme()
 2154        {
 2155            Type = SecuritySchemeType.ApiKey,
 2156            Name = options.Cookie.Name,
 2157            In = ParameterLocation.Cookie,
 2158            Description = options.Description,
 2159            Deprecated = options.Deprecated
 2160        };
 161    }
 162
 163    /// <summary>
 164    /// Gets the OpenAPI security scheme for JWT authentication.
 165    /// </summary>
 166    /// <param name="options">The JWT authentication options.</param>
 167    /// <returns></returns>
 168    private static OpenApiSecurityScheme GetSecurityScheme(JwtAuthOptions options)
 169    {
 3170        return new OpenApiSecurityScheme()
 3171        {
 3172            Type = SecuritySchemeType.Http,
 3173            Scheme = "bearer",
 3174            BearerFormat = "JWT",
 3175            Description = options.Description,
 3176            Deprecated = options.Deprecated
 3177        };
 178    }
 179
 180    /// <summary>
 181    ///  Gets the OpenAPI security scheme for basic authentication.
 182    /// </summary>
 183    /// <param name="options">The basic authentication options.</param>
 184    private static OpenApiSecurityScheme GetSecurityScheme(BasicAuthenticationOptions options)
 185    {
 8186        return new OpenApiSecurityScheme()
 8187        {
 8188            Type = SecuritySchemeType.Http,
 8189            Scheme = "basic",
 8190            Description = options.Description,
 8191            Deprecated = options.Deprecated
 8192        };
 193    }
 194
 195    /// <summary>
 196    /// Adds a security component to the OpenAPI document.
 197    /// </summary>
 198    /// <param name="scheme">The name of the security component.</param>
 199    /// <param name="globalScheme">Indicates whether the security scheme should be applied globally.</param>
 200    /// <param name="securityScheme">The security scheme to add.</param>
 201    private void AddSecurityComponent(string scheme, bool globalScheme, OpenApiSecurityScheme securityScheme)
 202    {
 23203        _ = Document.AddComponent(scheme, securityScheme);
 204
 205        // Reference it by NAME in the requirement (no .Reference in v2)
 23206        var requirement = new OpenApiSecurityRequirement
 23207        {
 23208            {
 23209                new OpenApiSecuritySchemeReference(scheme,Document), new List<string>()
 23210            }
 23211        };
 23212        SecurityRequirement.Add(scheme, requirement);
 213
 214        // Apply globally if specified
 23215        if (globalScheme)
 216        {
 217            // Apply globally
 1218            Document.Security ??= [];
 1219            Document.Security.Add(requirement);
 220        }
 23221    }
 222}

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Tags.cs

#LineLine coverage
 1using System.Collections;
 2using Microsoft.OpenApi;
 3
 4namespace Kestrun.OpenApi;
 5
 6/// <summary>
 7/// Helper methods for accessing OpenAPI document components.
 8/// </summary>
 9public partial class OpenApiDocDescriptor
 10{
 11    /// <summary>
 12    /// Adds a tag if it doesn't exist and returns the existing or newly created tag.
 13    /// </summary>
 14    /// <param name="name">The name of the tag.</param>
 15    /// <param name="description">Optional description of the tag.</param>
 16    /// <param name="summary">Optional summary of the tag.</param>
 17    /// <param name="parent">Optional parent tag name.</param>
 18    /// <param name="kind">Optional kind of the tag.</param>
 19    /// <param name="externalDocs">Optional external documentation for the tag.</param>
 20    /// <param name="extensions">Optional OpenAPI extensions for the tag.</param>
 21    /// <returns>The existing or newly created OpenApiTag.</returns>
 22    public OpenApiTag AddTag(
 23        string name,
 24        string? description = null,
 25        string? summary = null,
 26        string? parent = null,
 27        string? kind = null,
 28        OpenApiExternalDocs? externalDocs = null,
 29        IDictionary? extensions = null
 30     )
 31    {
 32        // Reuse your existing logic (it also ensures the tag is added)
 933        var tag = GetOrCreateTagItem(name);
 34
 35        // Optional: update metadata when provided
 936        if (!string.IsNullOrWhiteSpace(description))
 37        {
 738            tag.Description = description;
 39        }
 40
 941        if (!string.IsNullOrWhiteSpace(summary))
 42        {
 543            tag.Summary = summary;
 44        }
 945        if (externalDocs is not null)
 46        {
 547            tag.ExternalDocs = externalDocs;
 48        }
 949        if (!string.IsNullOrWhiteSpace(parent))
 50        {
 351            tag.Parent = new OpenApiTagReference(parent);
 52        }
 953        if (!string.IsNullOrWhiteSpace(kind))
 54        {
 355            tag.Kind = kind;
 56        }
 57
 958        tag.Extensions = BuildExtensions(extensions);
 59
 960        return tag;
 61    }
 62    /// <summary>
 63    /// Adds a tag only if it doesn't already exist (by comparer). Returns true if added.
 64    /// </summary>
 65    private bool AddTagIfMissing(OpenApiTag tag)
 66    {
 267        Document.Tags ??= new HashSet<OpenApiTag>();
 268        return Document.Tags.Add(tag); // HashSet with comparer prevents duplicates
 69    }
 70    /// <summary>
 71    /// Removes a tag by name. Returns true if removed.
 72    /// </summary>
 73    private bool RemoveTag(string name)
 74    {
 275        if (Document.Tags is null)
 76        {
 077            return false;
 78        }
 79
 80        // Uses comparer-based removal (fast path for HashSet)
 281        return Document.Tags.Remove(new OpenApiTag { Name = name });
 82    }
 83    /// <summary>
 84    /// Removes a tag by instance. Returns true if removed.
 85    /// </summary>
 86    public bool RemoveTag(OpenApiTag tag) =>
 287        Document.Tags is not null && Document.Tags.Remove(tag);
 88
 89    /// <summary>
 90    /// Gets or creates a tag item in the OpenAPI document by name.
 91    /// </summary>
 92    /// <param name="name">The name of the tag to get or create.</param>
 93    /// <returns>The retrieved or newly created OpenApiTag.</returns>
 94    private OpenApiTag GetOrCreateTagItem(string name)
 95    {
 1196        Document.Tags ??= new HashSet<OpenApiTag>();
 97
 1198        var probe = new OpenApiTag { Name = name };
 99
 11100        if (!Document.Tags.Contains(probe))
 101        {
 8102            _ = Document.Tags.Add(probe);
 8103            return probe;
 104        }
 105
 3106        return Document.Tags.First(t =>
 6107            string.Equals(t.Name, name, StringComparison.Ordinal));
 108    }
 109
 110    /// <summary>
 111    /// Tries to get a tag item by name from the OpenAPI document.
 112    /// </summary>
 113    /// <param name="name"> The name of the tag to retrieve.</param>
 114    /// <param name="tag"> The retrieved OpenApiTag if found; otherwise, null.</param>
 115    /// <returns>True if the tag was found; otherwise, false.</returns>
 116    public bool TryGetTag(string name, out OpenApiTag? tag)
 117    {
 3118        tag = Document.Tags?.FirstOrDefault(t =>
 4119            string.Equals(t.Name, name, StringComparison.Ordinal));
 120
 3121        return tag is not null;
 122    }
 123
 124    /// <summary>
 125    /// Determines whether the OpenAPI document contains a tag with the specified name.
 126    /// </summary>
 127    /// <param name="name">The name of the tag to check for existence.</param>
 128    /// <returns>True if the tag exists; otherwise, false.</returns>
 129    public bool ContainsTag(string name)
 130    {
 4131        return Document.Tags?.Any(t =>
 6132            string.Equals(t.Name, name, StringComparison.Ordinal)) ?? false;
 133    }
 134}

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor_Webhook.cs

#LineLine coverage
 1using Kestrun.Hosting.Options;
 2using Kestrun.Utilities;
 3using Microsoft.OpenApi;
 4
 5namespace Kestrun.OpenApi;
 6
 7/// <summary>
 8/// Helper methods for accessing OpenAPI document components.
 9/// </summary>
 10public partial class OpenApiDocDescriptor
 11{
 12    /// <summary>
 13    /// Populates Document.Webhooks from the registered webhooks using OpenAPI metadata on each webhook.
 14    /// </summary>
 15    /// <param name="Metadata"> The dictionary containing webhook patterns, HTTP methods, and their associated OpenAPI m
 16    private void BuildWebhooks(Dictionary<(string Pattern, HttpVerb Method), OpenAPIPathMetadata> Metadata)
 17    {
 218        if (Metadata is null || Metadata.Count == 0)
 19        {
 220            return;
 21        }
 022        Document.Webhooks = new Dictionary<string, IOpenApiPathItem>();
 23
 024        var groups = Metadata
 025            .GroupBy(kvp => kvp.Key.Pattern, StringComparer.Ordinal)
 026            .Where(g => !string.IsNullOrWhiteSpace(g.Key));
 27
 028        foreach (var grp in groups)
 29        {
 030            ProcessWebhookGroup(grp);
 31        }
 032    }
 33
 34    /// <summary>
 35    /// Processes a group of webhooks sharing the same pattern to build the corresponding OpenAPI webhook path item.
 36    /// </summary>
 37    /// <param name="grp">The group of webhooks sharing the same pattern. </param>
 38    private void ProcessWebhookGroup(IGrouping<string, KeyValuePair<(string Pattern, HttpVerb Method), OpenAPIPathMetada
 39    {
 040        var pattern = grp.Key;
 041        var webhookPathItem = GetOrCreateWebhookItem(pattern);
 42
 043        foreach (var kvp in grp)
 44        {
 045            if (kvp.Value.DocumentId is not null && !kvp.Value.DocumentId.Contains(DocumentId))
 46            {
 47                continue;
 48            }
 049            ProcessWebhookOperation(kvp, webhookPathItem);
 50        }
 051    }
 52
 53    /// <summary>
 54    /// Processes a single webhook operation and adds it to the OpenApiPathItem.
 55    /// </summary>
 56    /// <param name="kvp"> The key-value pair representing the webhook pattern, HTTP method, and OpenAPI metadata.</para
 57    /// <param name="webhookPathItem"> The OpenApiPathItem to which the operation will be added.</param>
 58    private void ProcessWebhookOperation(KeyValuePair<(string Pattern, HttpVerb Method), OpenAPIPathMetadata> kvp, OpenA
 59    {
 060        var method = kvp.Key.Method;
 061        var openapiMetadata = kvp.Value;
 62
 063        var op = BuildOperationFromMetadata(openapiMetadata);
 064        webhookPathItem.AddOperation(HttpMethod.Parse(method.ToMethodString()), op);
 065    }
 66
 67    /// <summary>
 68    /// Gets or creates the OpenApiPathItem for the specified webhook pattern.
 69    /// </summary>
 70    /// <param name="pattern">The webhook pattern.</param>
 71    /// <returns>The corresponding OpenApiPathItem.</returns>
 72    private OpenApiPathItem GetOrCreateWebhookItem(string pattern)
 73    {
 074        Document.Webhooks ??= new Dictionary<string, IOpenApiPathItem>(StringComparer.Ordinal);
 075        if (!Document.Webhooks.TryGetValue(pattern, out var pathInterface) || pathInterface is null)
 76        {
 077            pathInterface = new OpenApiPathItem();
 078            Document.Webhooks[pattern] = pathInterface;
 79        }
 080        return (OpenApiPathItem)pathInterface;
 81    }
 82}

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiDocDescriptor.cs

#LineLine coverage
 1using Microsoft.OpenApi;
 2using Kestrun.Hosting;
 3using Microsoft.OpenApi.Reader;
 4using System.Text;
 5using Kestrun.Hosting.Options;
 6using Kestrun.Utilities;
 7using System.Collections;
 8using System.Text.Json.Nodes;
 9using Kestrun.Forms;
 10using System.Reflection;
 11
 12namespace Kestrun.OpenApi;
 13
 14/// <summary>
 15/// Generates OpenAPI v2 (Swagger) documents from C# types decorated with OpenApiSchema attributes.
 16/// </summary>
 17public partial class OpenApiDocDescriptor
 18{
 19    /// <summary>
 20    /// Default documentation identifier.
 21    /// </summary>
 22    public const string DefaultDocumentationId = "Default";
 23
 24    /// <summary>
 25    /// Default documentation identifiers for OpenAPI authentication schemes.
 26    /// </summary>
 127    public static readonly string[] DefaultDocumentationIds = ["Default"];
 28
 29    /// <summary>
 30    /// Default schema identifier used for autogenerated OpenAPI client-error responses.
 31    /// </summary>
 32    public const string DefaultAutoErrorResponseSchemaId = "KestrunErrorResponse";
 33
 34    /// <summary>
 35    /// Default content type used for autogenerated OpenAPI client-error responses.
 36    /// </summary>
 37    public const string DefaultAutoErrorResponseContentType = "application/problem+json";
 38    /// <summary>
 39    /// The Kestrun host providing registered routes.
 40    /// </summary>
 35241    public KestrunHost Host { get; init; }
 42
 43    /// <summary>
 44    /// The ID of the OpenAPI document being generated.
 45    /// </summary>
 13846    public string DocumentId { get; init; }
 47
 48    /// <summary>
 49    /// The OpenAPI document being generated.
 50    /// </summary>
 72551    public OpenApiDocument Document { get; private set; } = new OpenApiDocument { Components = new OpenApiComponents() }
 52
 53    /// <summary>
 54    /// Gets or sets the schema identifier used for autogenerated OpenAPI client-error responses.
 55    /// </summary>
 14856    public string AutoErrorResponseSchemaId { get; set; } = DefaultAutoErrorResponseSchemaId;
 57
 58    /// <summary>
 59    /// Gets or sets the content types used for autogenerated OpenAPI client-error responses.
 60    /// </summary>
 15461    public string[] AutoErrorResponseContentTypes { get; set; } = [DefaultAutoErrorResponseContentType];
 62
 63    /// <summary>
 64    /// Security requirements for the OpenAPI document.
 65    /// </summary>
 16166    public IDictionary<string, OpenApiSecurityRequirement> SecurityRequirement { get; private set; } = new Dictionary<st
 67
 68    /// <summary>
 69    /// Inline components specific to this OpenAPI document.
 70    /// </summary>
 2671    public OpenApiComponents InlineComponents { get; }
 72
 73    /// <summary>
 74    /// OpenAPI metadata for webhooks associated with this document.
 75    /// </summary>
 14076    public Dictionary<(string Pattern, HttpVerb Method), OpenAPIPathMetadata> WebHook { get; set; } = [];
 77
 78    /// <summary>
 79    /// OpenAPI metadata for callbacks associated with this document.
 80    /// </summary>
 13881    public Dictionary<(string Pattern, HttpVerb Method), OpenAPIPathMetadata> Callbacks { get; set; } = [];
 82
 83    /// <summary>
 84    /// Initializes a new instance of the OpenApiDocDescriptor.
 85    /// </summary>
 86    /// <param name="host">The Kestrun host.</param>
 87    /// <param name="docId">The ID of the OpenAPI document being generated.</param>
 88    /// <exception cref="ArgumentNullException">Thrown if host or docId is null.</exception>
 13889    public OpenApiDocDescriptor(KestrunHost host, string docId)
 90    {
 13891        ArgumentNullException.ThrowIfNull(host);
 13892        ArgumentNullException.ThrowIfNull(docId);
 13893        Host = host;
 13894        DocumentId = docId;
 13895        HasBeenGenerated = false;
 13896        InlineComponents = new OpenApiComponents();
 13897    }
 98
 99    /// <summary>
 100    /// Indicates whether the OpenAPI document has been generated at least once.
 101    /// </summary>
 140102    public bool HasBeenGenerated { get; private set; }
 103
 104    /// <summary>
 105    /// Generates an OpenAPI document from the provided schema types.
 106    /// </summary>
 107    /// <param name="components">The set of discovered OpenAPI component types.</param>
 108    /// <returns>The generated OpenAPI document.</returns>
 109    internal void GenerateComponents(OpenApiComponentSet components)
 110    {
 4111        Document.Components ??= new OpenApiComponents();
 29112        ProcessComponentTypes(components.SchemaTypes, () => Document.Components.Schemas ??= new Dictionary<string, IOpen
 4113    }
 114
 115    /// <summary>
 116    /// Processes a list of component types and builds them into the OpenAPI document.
 117    /// </summary>
 118    /// <param name="types">The list of component types to process.</param>
 119    /// <param name="ensureDictionary">An action to ensure the corresponding dictionary is initialized.</param>
 120    /// <param name="buildAction">An action to build each component type.</param>
 121    private static void ProcessComponentTypes(
 122        IReadOnlyList<Type>? types,
 123        Action ensureDictionary,
 124        Action<Type> buildAction)
 125    {
 4126        if (types is null || types.Count == 0)
 127        {
 0128            return;
 129        }
 130
 4131        ensureDictionary();
 50132        foreach (var type in types)
 133        {
 21134            buildAction(type);
 135        }
 4136    }
 137
 138    /// <summary>
 139    /// Generates the OpenAPI document by auto-discovering component types.
 140    /// </summary>
 141    public void GenerateComponents()
 142    {
 143        // Auto-discover OpenAPI component types
 3144        var components = OpenApiSchemaDiscovery.GetOpenApiTypesAuto();
 145
 146        // Generate components from the discovered types
 3147        GenerateComponents(components);
 148
 3149        AddFormOptions(components);
 150
 151        // Process variable annotations from the host
 3152        ProcessVariableAnnotations(Host.ComponentAnnotations);
 3153    }
 154
 155    private void AddFormOptions(OpenApiComponentSet components)
 156    {
 50157        foreach (var type in components.SchemaTypes)
 158        {
 21159            if (type is null || !type.IsDefined(typeof(KrBindFormAttribute), inherit: false))
 160            {
 161                continue;
 162            }
 163
 4164            var formOptions = BuildFormOptionsSchema(type.FullName, type);
 4165            if (formOptions is null)
 166            {
 167                continue;
 168            }
 169
 4170            var rules = FormHelper.BuildFormPartRulesFromType(type);
 4171            AddFormPartRules(formOptions, rules);
 172
 173            // Register the option in the host.
 4174            _ = Host.AddFormOption(formOptions);
 175
 176            // Register part rules in the host runtime (best-effort: host rule store is keyed by name).
 40177            foreach (var rule in rules)
 178            {
 16179                _ = Host.AddFormPartRule(rule);
 180            }
 181        }
 4182    }
 183
 184    private static void AddFormPartRules(KrFormOptions options, IEnumerable<KrFormPartRule> rules)
 185    {
 4186        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 40187        foreach (var rule in rules)
 188        {
 16189            if (string.IsNullOrWhiteSpace(rule.Name))
 190            {
 191                continue;
 192            }
 193
 16194            var key = string.IsNullOrWhiteSpace(rule.Scope)
 16195                ? rule.Name
 16196                : $"{rule.Scope}::{rule.Name}";
 197
 16198            if (!seen.Add(key))
 199            {
 200                continue;
 201            }
 202
 16203            options.Rules.Add(rule);
 204        }
 4205    }
 206
 207    private KrFormOptions? BuildFormOptionsSchema(string? typeName, Type type)
 208    {
 4209        if (typeName is null)
 210        {
 0211            return null;
 212        }
 213
 12214        foreach (var attr in type.GetCustomAttributes<KrBindFormAttribute>(inherit: false))
 215        {
 4216            var formOptions = FormHelper.ApplyKrPartAttributes(attr);
 4217            formOptions.Name = typeName;
 4218            return formOptions;
 219        }
 0220        return null;
 4221    }
 222
 223    /// <summary>
 224    /// Processes variable annotations to build OpenAPI components.
 225    /// </summary>
 226    /// <param name="annotations">A dictionary of variable names to their annotated variables.</param>
 227    private void ProcessVariableAnnotations(Dictionary<string, OpenApiComponentAnnotationScanner.AnnotatedVariable>? ann
 228    {
 8229        if (annotations is null || annotations.Count == 0)
 230        {
 5231            Host.Logger.Warning("No OpenAPI component annotations were found in the host.");
 5232            return;
 233        }
 12234        foreach (var variable in annotations.Values)
 235        {
 3236            if (variable?.Annotations is null || variable.Annotations.Count == 0)
 237            {
 238                continue;
 239            }
 240
 2241            DispatchComponentAnnotations(variable);
 242        }
 3243    }
 244
 245    /// <summary>
 246    /// Dispatches component annotations for a given variable.
 247    /// </summary>
 248    /// <param name="variable">The annotated variable containing annotations.</param>
 249    private void DispatchComponentAnnotations(OpenApiComponentAnnotationScanner.AnnotatedVariable variable)
 250    {
 12251        foreach (var annotation in variable.Annotations)
 252        {
 253            switch (annotation)
 254            {
 255                case OpenApiParameterComponentAttribute paramComponent:
 2256                    ProcessParameterComponent(variable, paramComponent);
 2257                    break;
 258                case OpenApiRequestBodyComponentAttribute requestBodyComponent:
 0259                    ProcessRequestBodyComponent(variable, requestBodyComponent);
 0260                    break;
 261                case OpenApiParameterExampleRefAttribute parameterExampleRef:
 0262                    ProcessParameterExampleRef(variable.Name, parameterExampleRef);
 0263                    break;
 264                case OpenApiRequestBodyExampleRefAttribute requestBodyExampleRef:
 0265                    ProcessRequestBodyExampleRef(variable.Name, requestBodyExampleRef);
 0266                    break;
 267                case OpenApiExtensionAttribute extensionAttribute:
 0268                    ProcessVariableExtension(variable, extensionAttribute);
 0269                    break;
 270                case InternalPowershellAttribute powershellAttribute:
 271                    // Process PowerShell attribute to modify the schema
 2272                    ProcessPowerShellAttribute(variable.Name, powershellAttribute);
 2273                    break;
 274                case OpenApiResponseComponentAttribute responseComponent:
 0275                    ProcessResponseComponent(variable, responseComponent);
 0276                    break;
 277                case OpenApiResponseHeaderRefAttribute headerRef:
 0278                    ProcessResponseHeaderRef(variable.Name, headerRef);
 0279                    break;
 280                case OpenApiResponseLinkRefAttribute linkRef:
 0281                    ProcessResponseLinkRef(variable.Name, linkRef);
 0282                    break;
 283                case OpenApiResponseExampleRefAttribute exampleRef:
 0284                    ProcessResponseExampleRef(variable.Name, exampleRef);
 285                    break;
 286                default:
 287                    break;
 288            }
 289        }
 2290    }
 291
 292    /// <summary>
 293    /// Processes an OpenAPI extension annotation for a given variable.
 294    /// </summary>
 295    /// <param name="variable"> The annotated variable containing annotations.</param>
 296    /// <param name="extensionAttribute"> The OpenAPI extension attribute to process.</param>
 297    private void ProcessVariableExtension(OpenApiComponentAnnotationScanner.AnnotatedVariable variable, OpenApiExtension
 298    {
 0299        var extensions = new Dictionary<string, IOpenApiExtension>(StringComparer.Ordinal);
 300
 0301        if (Host.Logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 302        {
 0303            Host.Logger.Debug("Applying OpenApiExtension '{extensionName}' to function metadata", extensionAttribute.Nam
 304        }
 305        // Parse string into a JsonNode tree.
 0306        var node = JsonNode.Parse(extensionAttribute.Json);
 0307        if (node is null)
 308        {
 0309            Host.Logger.Error("Error parsing OpenAPI extension '{extensionName}': JSON is null", extensionAttribute.Name
 0310            return;
 311        }
 0312        extensions[extensionAttribute.Name] = new JsonNodeExtension(node);
 0313        if (variable.Annotations.Any(a => a is OpenApiParameterComponentAttribute))
 314        {
 0315            var param = GetOrCreateParameterItem(variable.Name, false);
 0316            param.Extensions = extensions;
 317        }
 0318        else if (variable.Annotations.Any(a => a is OpenApiRequestBodyComponentAttribute))
 319        {
 0320            var requestBody = GetOrCreateRequestBodyItem(variable.Name, false);
 0321            requestBody.Extensions = extensions;
 322        }
 0323        else if (variable.Annotations.Any(a => a is OpenApiResponseComponentAttribute))
 324        {
 0325            var response = GetOrCreateResponseItem(variable.Name, false);
 0326            response.Extensions = extensions;
 327        }
 328        else
 329        {
 0330            Host.Logger.Error("OpenApiExtension '{extensionName}' could not be applied: no matching component found for 
 0331            return;
 332        }
 333    }
 334
 335    /// <summary>
 336    /// Tries to apply the variable type schema to the given OpenAPI response.
 337    /// </summary>
 338    /// <param name="response"> The OpenAPI response to apply the schema to.</param>
 339    /// <param name="variable"> The annotated variable containing annotations.</param>
 340    /// <param name="responseDescriptor"> The response component attribute describing the response.</param>
 341    /// <exception cref="InvalidOperationException"> Thrown if the response component does not specify any ContentType.<
 342    private void TryApplyVariableTypeSchema(
 343        OpenApiResponse response,
 344        OpenApiComponentAnnotationScanner.AnnotatedVariable variable,
 345        OpenApiResponseComponentAttribute responseDescriptor)
 346    {
 0347        if (variable.VariableType is null)
 348        {
 0349            return;
 350        }
 0351        var iSchema = InferPrimitiveSchema(variable.VariableType);
 0352        if (iSchema is OpenApiSchema schema)
 353        {
 354            // Apply any schema attributes from the parameter annotation
 0355            ApplyConcreteSchemaAttributes(responseDescriptor, schema);
 356            // Try to set default value from the variable initial value if not already set
 0357            if (!variable.NoDefault)
 358            {
 0359                schema.Default = OpenApiJsonNodeFactory.ToNode(variable.InitialValue);
 360            }
 361        }
 362
 363        // Either Schema OR Content, depending on ContentType
 0364        if (responseDescriptor.ContentType.Length == 0)
 365        {
 0366            throw new InvalidOperationException($"Response component '{variable.Name}' must specify at least one Content
 367        }
 368        // Use Content
 0369        response.Content ??= new Dictionary<string, IOpenApiMediaType>(StringComparer.Ordinal);
 0370        foreach (var contentType in responseDescriptor.ContentType)
 371        {
 0372            response.Content[contentType] = new OpenApiMediaType { Schema = iSchema };
 373        }
 0374    }
 375
 376    private static void ApplyResponseCommonFields(
 377        OpenApiResponse response,
 378        OpenApiResponseComponentAttribute responseDescriptor)
 379    {
 0380        if (responseDescriptor.Summary is not null)
 381        {
 0382            response.Summary = responseDescriptor.Summary;
 383        }
 0384        if (responseDescriptor.Description is not null)
 385        {
 0386            response.Description = responseDescriptor.Description;
 387        }
 0388    }
 389
 390    /// <summary>
 391    /// Generates the OpenAPI document by processing components and building paths and webhooks.
 392    /// </summary>
 393    /// <remarks>BuildCallbacks is already handled elsewhere.</remarks>
 394    /// <remarks>This method sets HasBeenGenerated to true after generation.</remarks>
 395    public void GenerateDoc()
 396    {
 397        // Then, generate webhooks
 2398        BuildWebhooks(WebHook);
 399
 400        // Finally, build paths from registered routes
 2401        BuildPathsFromRegisteredRoutes(Host.RegisteredRoutes);
 402
 2403        HasBeenGenerated = true;
 2404    }
 405
 406    /// <summary>
 407    /// Reads and diagnoses the OpenAPI document by serializing and re-parsing it.
 408    /// </summary>
 409    /// <param name="version">The OpenAPI specification version to read as.</param>
 410    /// <returns>A tuple containing the OpenAPI document and any diagnostics.</returns>
 411    public ReadResult ReadAndDiagnose(OpenApiSpecVersion version)
 412    {
 0413        using var sw = new StringWriter();
 0414        var w = new OpenApiJsonWriter(sw);
 0415        Document.SerializeAs(version, w);
 0416        using var ms = new MemoryStream(Encoding.UTF8.GetBytes(sw.ToString()));
 417        // format must be "json" or "yaml"
 0418        return OpenApiDocument.Load(ms);
 0419    }
 420
 421    /// <summary>
 422    /// Serializes the OpenAPI document to a JSON string.
 423    /// </summary>
 424    /// <param name="version">The OpenAPI specification version to serialize as.</param>
 425    /// <returns>The serialized JSON string.</returns>
 426    public string ToJson(OpenApiSpecVersion version)
 427    {
 6428        using var sw = new StringWriter();
 6429        var w = new OpenApiJsonWriter(sw);
 6430        Document.SerializeAs(version, w);
 6431        return sw.ToString();
 6432    }
 433
 434    /// <summary>
 435    /// Serializes the OpenAPI document to a YAML string.
 436    /// </summary>
 437    /// <param name="version">The OpenAPI specification version to serialize as.</param>
 438    /// <returns>The serialized YAML string.</returns>
 439    public string ToYaml(OpenApiSpecVersion version)
 440    {
 0441        using var sw = new StringWriter();
 0442        var w = new OpenApiYamlWriter(sw);
 0443        Document.SerializeAs(version, w);
 0444        return sw.ToString();
 0445    }
 446
 447    /// <summary>
 448    /// Creates an OpenAPI extension in the document from the provided extensions dictionary.
 449    /// </summary>
 450    /// <param name="extensions">A dictionary containing the extensions.</param>
 451    /// <exception cref="ArgumentException">Thrown when the specified extension name is not found in the provided extens
 452    public void AddOpenApiExtension(IDictionary? extensions)
 453    {
 0454        var built = BuildExtensions(extensions);
 455
 0456        if (built is null)
 457        {
 0458            return;
 459        }
 460
 0461        Document.Extensions ??= new Dictionary<string, IOpenApiExtension>(StringComparer.Ordinal);
 462
 0463        foreach (var kvp in built)
 464        {
 0465            Document.Extensions[kvp.Key] = kvp.Value;
 466        }
 0467    }
 468}

Methods/Properties

LoadAnnotatedFunctions(System.Collections.Generic.List`1<System.Management.Automation.FunctionInfo>)
ProcessFunction(System.Management.Automation.FunctionInfo)
ProcessFunctionAttributes(System.Management.Automation.FunctionInfo,System.Management.Automation.Language.CommentHelpInfo,System.Collections.Generic.IReadOnlyCollection`1<System.Attribute>,Kestrun.Hosting.Options.MapRouteOptions,Kestrun.Hosting.Options.OpenAPIPathMetadata)
ApplyFormBindingAttribute(Kestrun.Hosting.Options.MapRouteOptions,KrBindFormAttribute)
ApplyExtensionAttribute(Kestrun.Hosting.Options.OpenAPIPathMetadata,OpenApiExtensionAttribute)
ApplyPathAttribute(System.Management.Automation.FunctionInfo,System.Management.Automation.Language.CommentHelpInfo,Kestrun.Hosting.Options.MapRouteOptions,Kestrun.Hosting.Options.OpenAPIPathMetadata,Kestrun.Utilities.HttpVerb,IOpenApiPathAttribute)
ApplyPathLikePath(System.Management.Automation.FunctionInfo,Kestrun.Hosting.Options.MapRouteOptions,Kestrun.Hosting.Options.OpenAPIPathMetadata,OpenApiPathAttribute,System.String)
AddQueryParametersFromTemplate(Kestrun.Hosting.Options.OpenAPIPathMetadata,System.Collections.Generic.IReadOnlyList`1<System.String>)
ApplyPathLikeWebhook(System.Management.Automation.FunctionInfo,Kestrun.Hosting.Options.OpenAPIPathMetadata,OpenApiWebhookAttribute,System.String)
ApplyPathLikeCallback(System.Management.Automation.FunctionInfo,Kestrun.Hosting.Options.OpenAPIPathMetadata,OpenApiCallbackAttribute,System.String,System.String)
ChooseFirstNonEmpty(System.String[])
NormalizeNewlines(System.String)
ApplyResponseRefAttribute(Kestrun.Hosting.Options.OpenAPIPathMetadata,OpenApiResponseRefAttribute,Kestrun.Hosting.Options.MapRouteOptions)
ApplyResponseAttribute(Kestrun.Hosting.Options.OpenAPIPathMetadata,IOpenApiResponseAttribute,Kestrun.Hosting.Options.MapRouteOptions)
SetDefaultResponseContentType(Microsoft.OpenApi.OpenApiResponses,Kestrun.Hosting.Options.MapRouteOptions,System.String,System.String)
TryInferClrTypeFromSchema(Microsoft.OpenApi.IOpenApiSchema)
InferNonArrayClrType(Microsoft.OpenApi.OpenApiSchema,Microsoft.OpenApi.JsonSchemaType)
InferIntegerClrType(System.String)
InferNumberClrType(System.String)
InferStringClrType(System.String)
SelectDefaultSuccessResponse(Microsoft.OpenApi.OpenApiResponses)
TryParseStatusCode(System.String)
HasContent(Microsoft.OpenApi.IOpenApiResponse)
ApplyPropertyAttribute(Kestrun.Hosting.Options.OpenAPIPathMetadata,OpenApiPropertyAttribute)
ApplyAuthorizationAttribute(Kestrun.Hosting.Options.MapRouteOptions,Kestrun.Hosting.Options.OpenAPIPathMetadata,OpenApiAuthorizationAttribute)
BuildPolicyList(System.String)
ProcessParameters(System.Management.Automation.FunctionInfo,System.Management.Automation.Language.CommentHelpInfo,Kestrun.Hosting.Options.MapRouteOptions,Kestrun.Hosting.Options.OpenAPIPathMetadata)
ApplyParameterAttribute(System.Management.Automation.FunctionInfo,System.Management.Automation.Language.CommentHelpInfo,Kestrun.Hosting.Options.MapRouteOptions,Kestrun.Hosting.Options.OpenAPIPathMetadata,System.Management.Automation.ParameterMetadata,OpenApiParameterAttribute)
ApplyParameterRefAttribute(System.Management.Automation.Language.CommentHelpInfo,Kestrun.Hosting.Options.MapRouteOptions,Kestrun.Hosting.Options.OpenAPIPathMetadata,System.Management.Automation.ParameterMetadata,OpenApiParameterRefAttribute)
ApplyParameterExampleRefAttribute(Kestrun.Hosting.Options.OpenAPIPathMetadata,System.Management.Automation.ParameterMetadata,OpenApiParameterExampleRefAttribute)
RemoveExistingParameter(Kestrun.Hosting.Options.OpenAPIPathMetadata,System.String)
ApplyRequestBodyRefAttribute(System.Management.Automation.Language.CommentHelpInfo,Kestrun.Hosting.Options.MapRouteOptions,Kestrun.Hosting.Options.OpenAPIPathMetadata,System.Management.Automation.ParameterMetadata,OpenApiRequestBodyRefAttribute)
ResolveRequestBodyReferenceId(OpenApiRequestBodyRefAttribute,System.Management.Automation.ParameterMetadata)
FindReferenceIdForParameter(System.String,System.Management.Automation.ParameterMetadata)
TryGetFirstRequestBodySchema(System.String,Microsoft.OpenApi.IOpenApiSchema&)
IsRequestBodySchemaMatchForParameter(Microsoft.OpenApi.IOpenApiSchema,System.Type)
ApplyRequestBodyAttribute(System.Management.Automation.Language.CommentHelpInfo,Kestrun.Hosting.Options.MapRouteOptions,Kestrun.Hosting.Options.OpenAPIPathMetadata,System.Management.Automation.ParameterMetadata,OpenApiRequestBodyAttribute)
ResolveFormOptions(Kestrun.Hosting.Options.MapRouteOptions,System.Management.Automation.ParameterMetadata)
ApplyFormRequestBody(System.Management.Automation.Language.CommentHelpInfo,Kestrun.Hosting.Options.MapRouteOptions,Kestrun.Hosting.Options.OpenAPIPathMetadata,System.Management.Automation.ParameterMetadata,OpenApiRequestBodyAttribute)
ApplyRequestBodyExampleRefAttribute(Kestrun.Hosting.Options.OpenAPIPathMetadata,OpenApiRequestBodyExampleRefAttribute)
BuildFormRequestBodyWithSchema(Microsoft.OpenApi.IOpenApiSchema,System.String[],Kestrun.Forms.KrFormOptions,OpenApiRequestBodyAttribute)
ResolveFormContentTypes(OpenApiRequestBodyAttribute,Kestrun.Forms.KrFormOptions)
IsMultipartContentType(System.String)
BuildMultipartEncoding(Kestrun.Forms.KrFormOptions)
IsProbablyFileRule(Kestrun.Forms.KrFormPartRule)
ApplyPreferredRequestBody(System.Management.Automation.Language.CommentHelpInfo,Kestrun.Hosting.Options.MapRouteOptions,Kestrun.Hosting.Options.OpenAPIPathMetadata,System.Management.Automation.ParameterMetadata,OpenApiRequestBodyAttribute)
EnsureDefaultResponses(Kestrun.Hosting.Options.OpenAPIPathMetadata)
FinalizeRouteOptions(System.Management.Automation.FunctionInfo,System.Management.Automation.ScriptBlock,Kestrun.Hosting.Options.OpenAPIPathMetadata,Kestrun.Hosting.Options.MapRouteOptions,Kestrun.Utilities.HttpVerb)
FinalizePathRouteOptions(System.Management.Automation.FunctionInfo,System.Management.Automation.ScriptBlock,Kestrun.Hosting.Options.OpenAPIPathMetadata,Kestrun.Hosting.Options.MapRouteOptions,Kestrun.Utilities.HttpVerb)
RegisterWebhook(System.Management.Automation.FunctionInfo,System.Management.Automation.ScriptBlock,Kestrun.Hosting.Options.OpenAPIPathMetadata,Kestrun.Utilities.HttpVerb,System.Collections.Generic.IEnumerable`1<System.String>)
RegisterCallback(System.Management.Automation.FunctionInfo,System.Management.Automation.ScriptBlock,Kestrun.Hosting.Options.OpenAPIPathMetadata,Kestrun.Utilities.HttpVerb,System.Collections.Generic.IEnumerable`1<System.String>)
GetDocDescriptorOrThrow(System.String,System.String)
EnsureParamOnlyScriptBlock(System.Management.Automation.FunctionInfo,System.Management.Automation.ScriptBlock,System.String)
CreateRequestBodyFromAttribute(KestrunAnnotation,Microsoft.OpenApi.OpenApiRequestBody,Microsoft.OpenApi.IOpenApiSchema)
BuildPathsFromRegisteredRoutes(System.Collections.Generic.Dictionary`2<System.ValueTuple`2<System.String,Kestrun.Utilities.HttpVerb>,Kestrun.Hosting.Options.MapRouteOptions>)
CreateOpenApiRouteEntries()
ProcessOpenApiRouteGroup(System.Linq.IGrouping`2<System.String,Kestrun.OpenApi.OpenApiDocDescriptor/OpenApiRouteEntry>)
GetOrCreatePathItem(System.String)
ProcessOpenApiRouteEntry(Kestrun.OpenApi.OpenApiDocDescriptor/OpenApiRouteEntry,Microsoft.OpenApi.OpenApiPathItem,Kestrun.Hosting.Options.OpenAPICommonMetadata)
.ctor(System.String,Kestrun.Utilities.HttpVerb,Kestrun.Hosting.Options.MapRouteOptions,Kestrun.Hosting.Options.OpenAPIPathMetadata)
get_Pattern()
get_Method()
get_Map()
get_Metadata()
ApplyPathLevelMetadata(Microsoft.OpenApi.OpenApiPathItem,Kestrun.Hosting.Options.OpenAPICommonMetadata,System.String)
ApplyPathLevelServers(Microsoft.OpenApi.OpenApiPathItem,Kestrun.Hosting.Options.OpenAPICommonMetadata)
ApplyPathLevelParameters(Microsoft.OpenApi.OpenApiPathItem,Kestrun.Hosting.Options.OpenAPICommonMetadata)
.ctor(Kestrun.Hosting.KestrunHost,System.String)
MergeXmlAttributes(System.Reflection.PropertyInfo)
BuildSchema(System.Type,System.Collections.Generic.HashSet`1<System.Type>)
BuildPropertySchema(System.Reflection.PropertyInfo,System.Collections.Generic.HashSet`1<System.Type>)
UnwrapNullableType(System.Type)
BuildFilePartSchema(System.Reflection.PropertyInfo,System.Boolean)
ApplyKrPartScope(System.Reflection.PropertyInfo,System.Type,System.Boolean&)
BuildPropertyTypeSchema(System.Type,System.Reflection.PropertyInfo,System.Collections.Generic.HashSet`1<System.Type>)
ApplyNullableSchema(Microsoft.OpenApi.IOpenApiSchema,System.Boolean)
ShouldPushNestedScope(System.Type)
BuildComplexTypeSchema(System.Type,System.Reflection.PropertyInfo,System.Collections.Generic.HashSet`1<System.Type>)
BuildEnumSchema(System.Type,System.Reflection.PropertyInfo)
BuildArraySchema(System.Type,System.Reflection.PropertyInfo,System.Collections.Generic.HashSet`1<System.Type>)
BuildPrimitiveSchema(System.Type,System.Reflection.PropertyInfo)
GetOrCreateSchemaItem(System.String,System.Boolean)
TryGetSchemaItem(System.String,Microsoft.OpenApi.IOpenApiSchema&,System.Boolean&)
TryGetSchemaItem(System.String,Microsoft.OpenApi.IOpenApiSchema&)
ApplyCallbackRefAttribute(Kestrun.Hosting.Options.OpenAPIPathMetadata,OpenApiCallbackRefAttribute)
BuildCallbacks(System.Collections.Generic.Dictionary`2<System.ValueTuple`2<System.String,Kestrun.Utilities.HttpVerb>,Kestrun.Hosting.Options.OpenAPIPathMetadata>)
ProcessCallbacksGroup(System.Linq.IGrouping`2<System.String,System.Collections.Generic.KeyValuePair`2<System.ValueTuple`2<System.String,Kestrun.Utilities.HttpVerb>,Kestrun.Hosting.Options.OpenAPIPathMetadata>>)
ProcessCallbackOperation(System.Collections.Generic.KeyValuePair`2<System.ValueTuple`2<System.String,Kestrun.Utilities.HttpVerb>,Kestrun.Hosting.Options.OpenAPIPathMetadata>,Microsoft.OpenApi.OpenApiCallback)
GetOrCreateCallbackItem(System.String,System.Boolean)
ApplyCallbacks(Microsoft.OpenApi.OpenApiOperation,Kestrun.Hosting.Options.OpenAPIPathMetadata)
AddComponentExample(System.String,Microsoft.OpenApi.OpenApiExample,Kestrun.OpenApi.OpenApiComponentConflictResolution)
TryAddExample(System.Collections.Generic.IDictionary`2<System.String,Microsoft.OpenApi.IOpenApiExample>,IOpenApiExampleAttribute)
NewOpenApiExample(System.String,System.String,System.Collections.IDictionary)
NewOpenApiExample(System.String,System.String,System.Object,System.Collections.IDictionary)
NewOpenApiExternalExample(System.String,System.String,System.String,System.Collections.IDictionary)
NewOpenApiExample(System.String,System.String,System.Object,System.String,System.Collections.IDictionary)
NewOpenApiHeader(System.String,System.Boolean,System.Boolean,System.Boolean,System.Nullable`1<Microsoft.OpenApi.ParameterStyle>,System.Boolean,System.Boolean,System.Object,System.Collections.Hashtable,System.Type,System.Collections.IDictionary,System.Collections.IDictionary)
ResolveHeaderSchema(System.Type,System.Collections.IDictionary)
ThrowIfBothSchemaAndContentProvided(System.Type,System.Collections.IDictionary)
ApplyHeaderSchema(Microsoft.OpenApi.OpenApiHeader,System.Type)
ApplyHeaderExamples(Microsoft.OpenApi.OpenApiHeader,System.Collections.Hashtable)
ResolveHeaderExampleValue(System.Object)
ApplyHeaderContent(Microsoft.OpenApi.OpenApiHeader,System.Collections.IDictionary)
ResolveHeaderMediaTypeValue(System.Object)
AddComponentHeader(System.String,Microsoft.OpenApi.OpenApiHeader,Kestrun.OpenApi.OpenApiComponentConflictResolution)
ApplyResponseHeaderAttribute(Kestrun.Hosting.Options.OpenAPIPathMetadata,IOpenApiResponseHeaderAttribute)
TryAddHeader(System.Collections.Generic.IDictionary`2<System.String,Microsoft.OpenApi.IOpenApiHeader>,OpenApiResponseHeaderRefAttribute)
TryGetHeaderItem(System.String,Microsoft.OpenApi.OpenApiHeader&)
GetSchema(System.String)
GetParameter(System.String)
GetRequestBody(System.String)
GetHeader(System.String)
GetResponse(System.String)
ComponentSchemasExists(System.String)
ComponentRequestBodiesExists(System.String)
ComponentResponsesExists(System.String)
ComponentParametersExists(System.String)
ComponentExamplesExists(System.String)
ComponentHeadersExists(System.String)
ComponentCallbacksExists(System.String)
ComponentLinksExists(System.String)
ComponentPathItemsExists(System.String)
BuildExtensions(System.Collections.IDictionary)
CreateExternalDocs(System.Uri,System.String,System.Collections.IDictionary)
CreateExternalDocs(System.String,System.String,System.Collections.IDictionary)
CreateInfoContact(System.String,System.Uri,System.String,System.Collections.IDictionary)
AddInlineExample(System.String,Microsoft.OpenApi.OpenApiExample,Kestrun.OpenApi.OpenApiComponentConflictResolution)
AddInlineLink(System.String,Microsoft.OpenApi.OpenApiLink,Kestrun.OpenApi.OpenApiComponentConflictResolution)
AddComponent(System.Collections.Generic.IDictionary`2<System.String,T>,System.String,T,Kestrun.OpenApi.OpenApiComponentConflictResolution,Kestrun.OpenApi.OpenApiComponentKind)
TryGetComponent(System.String,Kestrun.OpenApi.OpenApiComponentKind,T&)
TryGetInline(System.String,Kestrun.OpenApi.OpenApiComponentKind,T&)
TryGetFromComponents(Microsoft.OpenApi.OpenApiComponents,System.String,Kestrun.OpenApi.OpenApiComponentKind,T&)
TryGetAndCast()
ValidateComponentType(Kestrun.OpenApi.OpenApiComponentKind)
TryGet(System.Collections.Generic.IDictionary`2<System.String,T>,System.String,T&)
ThrowTypeMismatch(Kestrun.OpenApi.OpenApiComponentKind)
AddComponentLink(System.String,Microsoft.OpenApi.OpenApiLink,Kestrun.OpenApi.OpenApiComponentConflictResolution)
TryAddLink(System.Collections.Generic.IDictionary`2<System.String,Microsoft.OpenApi.IOpenApiLink>,OpenApiResponseLinkRefAttribute)
ApplyResponseLinkAttribute(Kestrun.Hosting.Options.OpenAPIPathMetadata,OpenApiResponseLinkRefAttribute)
ApplyLinkRefAttribute(OpenApiResponseLinkRefAttribute,Microsoft.OpenApi.OpenApiResponse)
NewOpenApiLink(System.String,System.String,System.String,Microsoft.OpenApi.OpenApiServer,System.Collections.IDictionary,System.Object,System.Collections.IDictionary)
ValidateLinkOperation(System.String,System.String)
ApplyLinkDescription(Microsoft.OpenApi.OpenApiLink,System.String)
ApplyLinkServer(Microsoft.OpenApi.OpenApiLink,Microsoft.OpenApi.OpenApiServer)
ApplyLinkOperation(Microsoft.OpenApi.OpenApiLink,System.String,System.String)
ApplyLinkRequestBody(Microsoft.OpenApi.OpenApiLink,System.Object)
ApplyLinkParameters(Microsoft.OpenApi.OpenApiLink,System.Collections.IDictionary)
ToRuntimeExpressionAnyWrapper(System.Object)
MergeSchemaAttributes(OpenApiPropertyAttribute[])
MergeStringProperties(OpenApiPropertyAttribute,OpenApiPropertyAttribute)
MergeEnumAndCollections(OpenApiPropertyAttribute,OpenApiPropertyAttribute)
MergeNumericProperties(OpenApiPropertyAttribute,OpenApiPropertyAttribute)
MergeBooleanProperties(OpenApiPropertyAttribute,OpenApiPropertyAttribute)
MergeTypeAndRequired(OpenApiPropertyAttribute,OpenApiPropertyAttribute)
MergeCustomFields(OpenApiPropertyAttribute,OpenApiPropertyAttribute)
CreateParameterFromAttribute(KestrunAnnotation,Microsoft.OpenApi.OpenApiParameter)
ApplyParameterAttribute(OpenApiParameterAttribute,Microsoft.OpenApi.OpenApiParameter)
ApplyExampleRefAttribute(OpenApiExampleRefAttribute,Microsoft.OpenApi.OpenApiParameter)
ProcessParameterComponent(Kestrun.OpenApi.OpenApiComponentAnnotationScanner/AnnotatedVariable,OpenApiParameterComponentAttribute)
ApplyParameterCommonFields(Microsoft.OpenApi.OpenApiParameter,OpenApiParameterComponentAttribute)
TryApplyVariableTypeSchema(Microsoft.OpenApi.OpenApiParameter,Kestrun.OpenApi.OpenApiComponentAnnotationScanner/AnnotatedVariable,OpenApiParameterComponentAttribute)
ProcessParameterExampleRef(System.String,OpenApiParameterExampleRefAttribute)
ValidateParameterHasSchemaOrContent(System.String,Microsoft.OpenApi.OpenApiParameter)
AddExampleToParameterExamples(Microsoft.OpenApi.OpenApiParameter,OpenApiParameterExampleRefAttribute)
AddExamplesToContentMediaTypes(Microsoft.OpenApi.OpenApiParameter,IOpenApiExampleAttribute,System.String)
AddExamplesToContentMediaTypes(Microsoft.OpenApi.OpenApiRequestBody,IOpenApiExampleAttribute,System.String)
ProcessPowerShellAttribute(System.String,InternalPowershellAttribute)
ValidateParameterHasSchemaOrContentForPowerShell(System.String,Microsoft.OpenApi.OpenApiParameter)
ApplyPowerShellAttributeToParameter(System.String,Microsoft.OpenApi.OpenApiParameter,InternalPowershellAttribute)
ApplyPowerShellAttributeToRequestBody(System.String,Microsoft.OpenApi.OpenApiRequestBody,InternalPowershellAttribute)
ApplyPowerShellAttributeToMediaTypeSchemas(System.String,System.Collections.Generic.IEnumerable`1<Microsoft.OpenApi.IOpenApiMediaType>,InternalPowershellAttribute,System.String)
ApplyPowerShellAttributesToSchema(Microsoft.OpenApi.OpenApiSchema,InternalPowershellAttribute)
ApplyItemConstraints(Microsoft.OpenApi.OpenApiSchema,InternalPowershellAttribute)
ApplyRangeConstraints(Microsoft.OpenApi.OpenApiSchema,InternalPowershellAttribute)
ApplyLengthConstraints(Microsoft.OpenApi.OpenApiSchema,InternalPowershellAttribute)
ApplyPatternConstraints(Microsoft.OpenApi.OpenApiSchema,InternalPowershellAttribute)
ApplyAllowedValuesConstraints(Microsoft.OpenApi.OpenApiSchema,InternalPowershellAttribute)
ApplyNullabilityConstraints(Microsoft.OpenApi.OpenApiSchema,InternalPowershellAttribute)
GetOrCreateParameterItem(System.String,System.Boolean)
TryGetParameterItem(System.String,Microsoft.OpenApi.OpenApiParameter&,System.Boolean&)
TryGetParameterItem(System.String,Microsoft.OpenApi.OpenApiParameter&)
BuildOperationFromMetadata(Kestrun.Hosting.Options.OpenAPIPathMetadata)
EnsureAutoClientErrorResponses(Microsoft.OpenApi.OpenApiOperation,Kestrun.Hosting.Options.OpenAPIPathMetadata)
GetAutoClientErrorStatuses(Microsoft.OpenApi.OpenApiOperation,Kestrun.Hosting.Options.OpenAPIPathMetadata)
AddMissingAutoClientErrorResponses(Microsoft.OpenApi.OpenApiOperation,System.Collections.Generic.IReadOnlyCollection`1<System.String>,System.String,System.Collections.Generic.IReadOnlyList`1<System.String>)
ResponseKeyExists(Microsoft.OpenApi.OpenApiResponses,System.String)
EnsureAutoErrorSchemaComponent()
GetAutoErrorResponseContentTypes()
CreateAutoClientErrorResponse(System.String,System.String,System.Collections.Generic.IReadOnlyList`1<System.String>)
ApplyExtensions(Microsoft.OpenApi.OpenApiOperation,Kestrun.Hosting.Options.OpenAPIPathMetadata)
ApplyTags(Microsoft.OpenApi.OpenApiOperation,Kestrun.Hosting.Options.OpenAPIPathMetadata)
ApplyServers(Microsoft.OpenApi.OpenApiOperation,Kestrun.Hosting.Options.OpenAPIPathMetadata)
ApplyParameters(Microsoft.OpenApi.OpenApiOperation,Kestrun.Hosting.Options.OpenAPIPathMetadata)
ApplySecurity(Microsoft.OpenApi.OpenApiOperation,Kestrun.Hosting.Options.OpenAPIPathMetadata)
CloneExampleOrThrow(System.String)
ProcessRequestBodyComponent(Kestrun.OpenApi.OpenApiComponentAnnotationScanner/AnnotatedVariable,OpenApiRequestBodyComponentAttribute)
ApplyRequestBodyCommonFields(Microsoft.OpenApi.OpenApiRequestBody,OpenApiRequestBodyComponentAttribute)
TryApplyVariableTypeSchema(Microsoft.OpenApi.OpenApiRequestBody,Kestrun.OpenApi.OpenApiComponentAnnotationScanner/AnnotatedVariable,OpenApiRequestBodyComponentAttribute)
ProcessRequestBodyExampleRef(System.String,OpenApiRequestBodyExampleRefAttribute)
GetOrCreateRequestBodyItem(System.String,System.Boolean)
TryGetRequestBodyItem(System.String,Microsoft.OpenApi.OpenApiRequestBody&,System.Boolean&)
TryGetRequestBodyItem(System.String,Microsoft.OpenApi.OpenApiRequestBody&)
GetKeyOverride(System.Object)
CreateResponseFromAttribute(System.Object,Microsoft.OpenApi.OpenApiResponse,Microsoft.OpenApi.IOpenApiSchema)
ApplyResponseAttribute(OpenApiResponseAttribute,Microsoft.OpenApi.OpenApiResponse,Microsoft.OpenApi.IOpenApiSchema)
ApplyDescription(OpenApiResponseAttribute,Microsoft.OpenApi.OpenApiResponse)
ResolveResponseSchema(OpenApiResponseAttribute,Microsoft.OpenApi.IOpenApiSchema)
ApplySchemaToContentTypes(OpenApiResponseAttribute,Microsoft.OpenApi.OpenApiResponse,Microsoft.OpenApi.IOpenApiSchema)
ApplyHeaderRefAttribute(OpenApiResponseHeaderRefAttribute,Microsoft.OpenApi.OpenApiResponse)
ApplyHeaderAttribute(OpenApiResponseHeaderAttribute,Microsoft.OpenApi.OpenApiResponse)
ApplyExampleRefAttribute(OpenApiExampleRefAttribute,Microsoft.OpenApi.OpenApiResponse)
ApplyExampleRefAttribute(OpenApiResponseExampleRefAttribute,Microsoft.OpenApi.OpenApiResponse)
ResolveExampleTargets(OpenApiExampleRefAttribute,Microsoft.OpenApi.OpenApiResponse)
ResolveExampleTargets(OpenApiResponseExampleRefAttribute,Microsoft.OpenApi.OpenApiResponse)
GetOrAddMediaType(Microsoft.OpenApi.OpenApiResponse,System.String)
CloneSchemaOrThrow(System.String)
ProcessResponseExampleRef(System.String,OpenApiResponseExampleRefAttribute)
ProcessResponseLinkRef(System.String,OpenApiResponseLinkRefAttribute)
ProcessResponseHeaderRef(System.String,OpenApiResponseHeaderRefAttribute)
ProcessResponseComponent(Kestrun.OpenApi.OpenApiComponentAnnotationScanner/AnnotatedVariable,OpenApiResponseComponentAttribute)
GetOrCreateResponseItem(System.String,System.Boolean)
TryGetResponseItem(System.String,Microsoft.OpenApi.OpenApiResponse&,System.Boolean&)
TryGetResponseItem(System.String,Microsoft.OpenApi.OpenApiResponse&)
GetSchemaIdentity(System.Type)
BuildSchemaForType(System.Type,System.Collections.Generic.HashSet`1<System.Type>)
TryBuildFormPayloadSchemaParent(System.Type,System.Collections.Generic.HashSet`1<System.Type>)
ProcessExtensions(System.Type,Microsoft.OpenApi.OpenApiSchema)
TryBuildPrimitiveSchema(System.Type,Microsoft.OpenApi.OpenApiSchema&)
TryBuildDerivedSchemaFromBaseType(System.Type,System.Collections.Generic.HashSet`1<System.Type>,Microsoft.OpenApi.IOpenApiSchema&,Microsoft.OpenApi.OpenApiSchema&)
HasComposableBaseType(System.Type)
TryResolveSimpleOrReferenceBaseSchema(System.Type,Microsoft.OpenApi.IOpenApiSchema,Microsoft.OpenApi.IOpenApiSchema&)
IsSimpleSchemaOrReference(Microsoft.OpenApi.IOpenApiSchema)
TryResolveArrayWrapperDerivedSchema(System.Type,System.Collections.Generic.HashSet`1<System.Type>,Microsoft.OpenApi.OpenApiSchema,Microsoft.OpenApi.IOpenApiSchema&)
CreateAllOfAdditionalObjectSchema(System.Type,System.Collections.Generic.HashSet`1<System.Type>)
CreateSchemaForDeclaredProperties(System.Type)
ComposeWithParentSchema(Microsoft.OpenApi.OpenApiSchema,Microsoft.OpenApi.OpenApiSchema)
BuildBaseTypeSchema(System.Type)
BuildCustomBaseTypeSchema(System.Type)
RegisterEnumSchema(System.Type)
ApplyTypeAttributes(System.Type,Microsoft.OpenApi.OpenApiSchema)
ApplyGeneratedRequiredPropertiesMetadata(System.Type,Microsoft.OpenApi.OpenApiSchema)
ApplySchemaComponentAttributes(System.Type,Microsoft.OpenApi.OpenApiSchema)
ApplySchemaComponentExamples(OpenApiSchemaComponent,Microsoft.OpenApi.OpenApiSchema)
ApplyPatternProperties(System.Type,Microsoft.OpenApi.OpenApiSchema)
BuildPatternSchema(OpenApiPatternPropertiesAttribute)
ProcessTypeProperties(System.Type,Microsoft.OpenApi.OpenApiSchema,System.Collections.Generic.HashSet`1<System.Type>)
ShouldSkipRuntimeFormPayloadStorageProperty(System.Type,System.Reflection.PropertyInfo)
TryCreateTypeInstance(System.Type)
CapturePropertyDefault(System.Object,System.Reflection.PropertyInfo,Microsoft.OpenApi.IOpenApiSchema)
IsIntrinsicDefault(System.Object,System.Type)
MakeNullable(Microsoft.OpenApi.OpenApiSchema,System.Boolean)
InferPrimitiveSchema(System.Type,System.Boolean)
InferArraySchema(System.Type,System.Boolean)
InferPowerShellClassSchema(System.Type,System.Boolean)
.cctor()
ApplySchemaAttr(OpenApiProperties,Microsoft.OpenApi.IOpenApiSchema)
ApplyConcreteSchemaAttributes(OpenApiProperties,Microsoft.OpenApi.OpenApiSchema)
ApplyTitleAndDescription(OpenApiProperties,Microsoft.OpenApi.OpenApiSchema)
ApplySchemaType(OpenApiProperties,Microsoft.OpenApi.OpenApiSchema)
ApplyFormatAndNumericBounds(OpenApiProperties,Microsoft.OpenApi.OpenApiSchema)
ApplyLengthAndPattern(OpenApiProperties,Microsoft.OpenApi.OpenApiSchema)
ApplyCollectionConstraints(OpenApiProperties,Microsoft.OpenApi.OpenApiSchema)
ApplyFlags(OpenApiProperties,Microsoft.OpenApi.OpenApiSchema)
ApplyAdditionalProperties(OpenApiProperties,Microsoft.OpenApi.OpenApiSchema)
ApplyArrayAdditionalProperties(OpenApiProperties,Microsoft.OpenApi.OpenApiSchema,System.Collections.Generic.HashSet`1<System.Type>)
EnsureAdditionalPropertiesAllowed(Microsoft.OpenApi.IOpenApiSchema,Microsoft.OpenApi.OpenApiSchema)
IsObjectSchemaType(System.Nullable`1<Microsoft.OpenApi.JsonSchemaType>)
ApplyExamplesAndDefaults(OpenApiProperties,Microsoft.OpenApi.OpenApiSchema)
ApplyXmlMetadata(OpenApiProperties,Microsoft.OpenApi.OpenApiSchema)
ApplyReferenceSchemaAttributes(OpenApiProperties,Microsoft.OpenApi.OpenApiSchemaReference)
ApplySecurityScheme(System.String,Kestrun.Authentication.IOpenApiAuthenticationOptions)
GetSecurityScheme(Kestrun.Authentication.ClientCertificateAuthenticationOptions)
GetSecurityScheme(Kestrun.Authentication.WindowsAuthOptions)
GetSecurityScheme(Kestrun.Authentication.OidcOptions)
GetSecurityScheme(Kestrun.Authentication.OAuth2Options)
GetSecurityScheme(Kestrun.Authentication.ApiKeyAuthenticationOptions)
GetSecurityScheme(Kestrun.Authentication.CookieAuthOptions)
GetSecurityScheme(Kestrun.Authentication.JwtAuthOptions)
GetSecurityScheme(Kestrun.Authentication.BasicAuthenticationOptions)
AddSecurityComponent(System.String,System.Boolean,Microsoft.OpenApi.OpenApiSecurityScheme)
AddTag(System.String,System.String,System.String,System.String,System.String,Microsoft.OpenApi.OpenApiExternalDocs,System.Collections.IDictionary)
AddTagIfMissing(Microsoft.OpenApi.OpenApiTag)
RemoveTag(System.String)
RemoveTag(Microsoft.OpenApi.OpenApiTag)
GetOrCreateTagItem(System.String)
TryGetTag(System.String,Microsoft.OpenApi.OpenApiTag&)
ContainsTag(System.String)
BuildWebhooks(System.Collections.Generic.Dictionary`2<System.ValueTuple`2<System.String,Kestrun.Utilities.HttpVerb>,Kestrun.Hosting.Options.OpenAPIPathMetadata>)
ProcessWebhookGroup(System.Linq.IGrouping`2<System.String,System.Collections.Generic.KeyValuePair`2<System.ValueTuple`2<System.String,Kestrun.Utilities.HttpVerb>,Kestrun.Hosting.Options.OpenAPIPathMetadata>>)
ProcessWebhookOperation(System.Collections.Generic.KeyValuePair`2<System.ValueTuple`2<System.String,Kestrun.Utilities.HttpVerb>,Kestrun.Hosting.Options.OpenAPIPathMetadata>,Microsoft.OpenApi.OpenApiPathItem)
GetOrCreateWebhookItem(System.String)
.cctor()
get_Host()
get_DocumentId()
get_Document()
get_AutoErrorResponseSchemaId()
get_AutoErrorResponseContentTypes()
get_SecurityRequirement()
get_InlineComponents()
get_WebHook()
get_Callbacks()
.ctor(Kestrun.Hosting.KestrunHost,System.String)
get_HasBeenGenerated()
GenerateComponents(Kestrun.OpenApi.OpenApiComponentSet)
ProcessComponentTypes(System.Collections.Generic.IReadOnlyList`1<System.Type>,System.Action,System.Action`1<System.Type>)
GenerateComponents()
AddFormOptions(Kestrun.OpenApi.OpenApiComponentSet)
AddFormPartRules(Kestrun.Forms.KrFormOptions,System.Collections.Generic.IEnumerable`1<Kestrun.Forms.KrFormPartRule>)
BuildFormOptionsSchema(System.String,System.Type)
ProcessVariableAnnotations(System.Collections.Generic.Dictionary`2<System.String,Kestrun.OpenApi.OpenApiComponentAnnotationScanner/AnnotatedVariable>)
DispatchComponentAnnotations(Kestrun.OpenApi.OpenApiComponentAnnotationScanner/AnnotatedVariable)
ProcessVariableExtension(Kestrun.OpenApi.OpenApiComponentAnnotationScanner/AnnotatedVariable,OpenApiExtensionAttribute)
TryApplyVariableTypeSchema(Microsoft.OpenApi.OpenApiResponse,Kestrun.OpenApi.OpenApiComponentAnnotationScanner/AnnotatedVariable,OpenApiResponseComponentAttribute)
ApplyResponseCommonFields(Microsoft.OpenApi.OpenApiResponse,OpenApiResponseComponentAttribute)
GenerateDoc()
ReadAndDiagnose(Microsoft.OpenApi.OpenApiSpecVersion)
ToJson(Microsoft.OpenApi.OpenApiSpecVersion)
ToYaml(Microsoft.OpenApi.OpenApiSpecVersion)
AddOpenApiExtension(System.Collections.IDictionary)