< 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@5f1d2b981c9d7292c11fd448428c6ab6c811c5de
Line coverage
67%
Covered lines: 1749
Uncovered lines: 829
Coverable lines: 2578
Total lines: 7463
Line coverage: 67.8%
Branch coverage
58%
Covered branches: 1159
Total branches: 1972
Branch coverage: 58.7%
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@8aa46e1988031758b311143cd39bf5749fbcd39e03/26/2026 - 03:54:59 Line coverage: 67.8% (1748/2577) Branch coverage: 58.7% (1159/1972) Total lines: 7462 Tag: Kestrun/Kestrun@844b5179fb0492dc6b1182bae3ff65fa7365521d04/23/2026 - 14:35:41 Line coverage: 67.8% (1749/2578) Branch coverage: 58.7% (1159/1972) Total lines: 7463 Tag: Kestrun/Kestrun@2fdbb120ca2faaa9acf2b8d2a34a7d64b067edbe 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@8aa46e1988031758b311143cd39bf5749fbcd39e03/26/2026 - 03:54:59 Line coverage: 67.8% (1748/2577) Branch coverage: 58.7% (1159/1972) Total lines: 7462 Tag: Kestrun/Kestrun@844b5179fb0492dc6b1182bae3ff65fa7365521d04/23/2026 - 14:35:41 Line coverage: 67.8% (1749/2578) Branch coverage: 58.7% (1159/1972) Total lines: 7463 Tag: Kestrun/Kestrun@2fdbb120ca2faaa9acf2b8d2a34a7d64b067edbe

Coverage delta

Coverage delta 26 -26

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(...)75%5462.5%
File 1: ApplyExtensionAttribute(...)100%66100%
File 1: ApplyPathAttribute(...)68.75%201674.07%
File 1: ApplyPathLikePath(...)0%4260%
File 1: AddQueryParametersFromTemplate(...)87.5%88100%
File 1: ApplyPathLikeWebhook(...)25%44100%
File 1: ApplyPathLikeCallback(...)75%44100%
File 1: ChooseFirstNonEmpty(...)100%44100%
File 1: NormalizeNewlines(...)50%22100%
File 1: ApplyResponseRefAttribute(...)0%342180%
File 1: ApplyResponseAttribute(...)58.33%141275%
File 1: SetDefaultResponseContentType(...)60%121075%
File 1: TryInferClrTypeFromSchema(...)60%121072.73%
File 1: InferNonArrayClrType(...)58.33%151272.73%
File 1: InferIntegerClrType(...)50%22100%
File 1: InferNumberClrType(...)50%22100%
File 1: InferStringClrType(...)25%371244.44%
File 1: SelectDefaultSuccessResponse(...)100%44100%
File 1: TryParseStatusCode(...)50%22100%
File 1: HasContent(...)75%5466.67%
File 1: ApplyPropertyAttribute(...)0%342180%
File 1: ApplyAuthorizationAttribute(...)0%620%
File 1: BuildPolicyList(...)50%22100%
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(...)100%1212100%
File 1: FindReferenceIdForParameter(...)75%4471.43%
File 1: TryGetFirstRequestBodySchema(...)62.5%8885.71%
File 1: IsRequestBodySchemaMatchForParameter(...)75%8883.33%
File 1: ApplyRequestBodyAttribute(...)0%506220%
File 1: ResolveFormOptions(...)0%7280%
File 1: ApplyFormRequestBody(...)0%620%
File 1: ApplyRequestBodyExampleRefAttribute(...)37.5%12860%
File 1: BuildFormRequestBodyWithSchema(...)100%88100%
File 1: ResolveFormContentTypes(...)100%66100%
File 1: IsMultipartContentType(...)100%11100%
File 1: BuildMultipartEncoding(...)100%1010100%
File 1: IsProbablyFileRule(...)100%1414100%
File 1: ApplyPreferredRequestBody(...)0%2040%
File 1: EnsureDefaultResponses(...)66.67%6687.5%
File 1: FinalizeRouteOptions(...)87.5%88100%
File 1: FinalizePathRouteOptions(...)83.33%6690.91%
File 1: RegisterWebhook(...)100%22100%
File 1: RegisterCallback(...)100%22100%
File 1: GetDocDescriptorOrThrow(...)100%22100%
File 1: EnsureParamOnlyScriptBlock(...)100%22100%
File 1: CreateRequestBodyFromAttribute(...)100%88100%
File 2: BuildPathsFromRegisteredRoutes(...)100%66100%
File 2: CreateOpenApiRouteEntries()91.67%121291.67%
File 2: ProcessOpenApiRouteGroup(...)100%44100%
File 2: GetOrCreatePathItem(...)66.67%66100%
File 2: ProcessOpenApiRouteEntry(...)85.71%141492.31%
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%2260%
File 2: ApplyPathLevelServers(...)100%2020100%
File 2: ApplyPathLevelParameters(...)100%2020100%
File 3: .ctor(...)100%11100%
File 3: MergeXmlAttributes(...)100%1414100%
File 3: BuildSchema(...)100%88100%
File 3: BuildPropertySchema(...)75%4492.31%
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.12%
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(...)83.33%131284.62%
File 4: BuildCallbacks(...)66.67%6687.5%
File 4: ProcessCallbacksGroup(...)83.33%66100%
File 4: ProcessCallbackOperation(...)91.67%121294.44%
File 4: GetOrCreateCallbackItem(...)66.67%131280%
File 4: ApplyCallbacks(...)50%5466.67%
File 5: AddComponentExample(...)75%44100%
File 5: TryAddExample(...)80%101090%
File 5: NewOpenApiExample(...)100%22100%
File 5: NewOpenApiExample(...)100%11100%
File 5: NewOpenApiExternalExample(...)100%11100%
File 5: NewOpenApiExample(...)100%22100%
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.73%
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(...)50%88100%
File 7: GetParameter(...)50%88100%
File 7: GetRequestBody(...)50%88100%
File 7: GetHeader(...)50%88100%
File 7: GetResponse(...)50%88100%
File 7: ComponentSchemasExists(...)50%44100%
File 7: ComponentRequestBodiesExists(...)50%44100%
File 7: ComponentResponsesExists(...)50%44100%
File 7: ComponentParametersExists(...)50%44100%
File 7: ComponentExamplesExists(...)50%44100%
File 7: ComponentHeadersExists(...)50%44100%
File 7: ComponentCallbacksExists(...)50%44100%
File 7: ComponentLinksExists(...)50%44100%
File 7: ComponentPathItemsExists(...)50%44100%
File 7: BuildExtensions(...)93.75%1616100%
File 8: CreateExternalDocs(...)100%11100%
File 8: CreateExternalDocs(...)50%2266.67%
File 8: CreateInfoContact(...)100%66100%
File 9: AddInlineExample(...)100%22100%
File 9: AddInlineLink(...)100%22100%
File 9: AddComponent(...)83.33%6692.31%
File 9: TryGetComponent(...)100%11100%
File 9: TryGetInline(...)100%11100%
File 9: TryGetFromComponents(...)57.14%191470%
File 9: TryGetAndCast()100%44100%
File 9: ValidateComponentType(...)71.43%161477.78%
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%11100%
File 10: ValidateLinkOperation(...)87.5%9880%
File 10: ApplyLinkDescription(...)100%22100%
File 10: ApplyLinkServer(...)100%22100%
File 10: ApplyLinkOperation(...)75%5466.67%
File 10: ApplyLinkRequestBody(...)50%9658.33%
File 10: ApplyLinkParameters(...)83.33%121288.89%
File 10: ToRuntimeExpressionAnyWrapper(...)100%22100%
File 11: MergeSchemaAttributes(...)100%88100%
File 11: MergeStringProperties(...)100%1212100%
File 11: MergeEnumAndCollections(...)100%1010100%
File 11: MergeNumericProperties(...)100%1010100%
File 11: MergeBooleanProperties(...)100%11100%
File 11: MergeTypeAndRequired(...)100%88100%
File 11: MergeCustomFields(...)100%66100%
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.16%
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(...)100%44100%
File 12: ValidateParameterHasSchemaOrContentForPowerShell(...)100%66100%
File 12: ApplyPowerShellAttributeToParameter(...)50%3240%
File 12: ApplyPowerShellAttributeToRequestBody(...)75%44100%
File 12: ApplyPowerShellAttributeToMediaTypeSchemas(...)75%5457.14%
File 12: ApplyPowerShellAttributesToSchema(...)100%11100%
File 12: ApplyItemConstraints(...)100%44100%
File 12: ApplyRangeConstraints(...)100%44100%
File 12: ApplyLengthConstraints(...)100%44100%
File 12: ApplyPatternConstraints(...)100%22100%
File 12: ApplyAllowedValuesConstraints(...)100%44100%
File 12: ApplyNullabilityConstraints(...)83.33%6685.71%
File 12: GetOrCreateParameterItem(...)58.33%131280%
File 12: TryGetParameterItem(...)75%4477.78%
File 12: TryGetParameterItem(...)100%11100%
File 13: BuildOperationFromMetadata(...)87.5%88100%
File 13: EnsureAutoClientErrorResponses(...)87.5%88100%
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.89%
File 13: CreateAutoClientErrorResponse(...)90%101094.74%
File 13: ApplyExtensions(...)50%6450%
File 13: ApplyTags(...)83.33%66100%
File 13: ApplyServers(...)15%1392033.33%
File 13: ApplyParameters(...)90.91%232285.71%
File 13: ApplySecurity(...)28.57%1371414.29%
File 14: CloneExampleOrThrow(...)0%7280%
File 14: ProcessRequestBodyComponent(...)100%22100%
File 14: ApplyRequestBodyCommonFields(...)100%11100%
File 14: TryApplyVariableTypeSchema(...)78.57%151484.62%
File 14: ProcessRequestBodyExampleRef(...)0%4260%
File 14: GetOrCreateRequestBodyItem(...)58.33%131280%
File 14: TryGetRequestBodyItem(...)75%4477.78%
File 14: TryGetRequestBodyItem(...)100%11100%
File 15: GetKeyOverride(...)50%22100%
File 15: CreateResponseFromAttribute(...)58.33%221258.33%
File 15: ApplyResponseAttribute(...)100%11100%
File 15: ApplyDescription(...)100%22100%
File 15: ResolveResponseSchema(...)25%7440%
File 15: ApplySchemaToContentTypes(...)91.67%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(...)83.33%66100%
File 15: ProcessResponseExampleRef(...)50%11650%
File 15: ProcessResponseLinkRef(...)16.67%21625%
File 15: ProcessResponseHeaderRef(...)66.67%8662.5%
File 15: ProcessResponseComponent(...)100%11100%
File 15: GetOrCreateResponseItem(...)58.33%131280%
File 15: TryGetResponseItem(...)75%4477.78%
File 15: TryGetResponseItem(...)100%11100%
File 16: GetSchemaIdentity(...)0%7280%
File 16: BuildSchemaForType(...)91.67%121294.44%
File 16: TryBuildFormPayloadSchemaParent(...)66.67%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.55%
File 16: BuildBaseTypeSchema(...)50%2266.67%
File 16: BuildCustomBaseTypeSchema(...)31.25%201675.86%
File 16: RegisterEnumSchema(...)75%44100%
File 16: ApplyTypeAttributes(...)100%11100%
File 16: ApplyGeneratedRequiredPropertiesMetadata(...)16.67%891218.75%
File 16: ApplySchemaComponentAttributes(...)100%22100%
File 16: ApplySchemaComponentExamples(...)16.67%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.78%
File 16: IsIntrinsicDefault(...)100%1616100%
File 16: MakeNullable(...)75%44100%
File 16: InferPrimitiveSchema(...)91.67%121291.67%
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.91%
File 16: ApplyTitleAndDescription(...)83.33%6680%
File 16: ApplySchemaType(...)28.57%441446.67%
File 16: ApplyFormatAndNumericBounds(...)50%461238.46%
File 16: ApplyLengthAndPattern(...)50%9657.14%
File 16: ApplyCollectionConstraints(...)50%191054.55%
File 16: ApplyFlags(...)100%44100%
File 16: ApplyAdditionalProperties(...)83.33%6677.78%
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(...)93.75%161692.86%
File 17: GetSecurityScheme(...)100%22100%
File 17: GetSecurityScheme(...)50%22100%
File 17: GetSecurityScheme(...)75%44100%
File 17: GetSecurityScheme(...)91.67%1212100%
File 17: GetOptionalAbsoluteUri(...)75%44100%
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.67%
File 18: RemoveTag(...)50%22100%
File 18: GetOrCreateTagItem(...)100%44100%
File 18: TryGetTag(...)100%22100%
File 18: ContainsTag(...)100%22100%
File 19: BuildWebhooks(...)100%66100%
File 19: ProcessWebhookGroup(...)100%66100%
File 19: ProcessWebhookOperation(...)100%11100%
File 19: GetOrCreateWebhookItem(...)66.67%66100%
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.67%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.67%1212100%
File 20: DispatchComponentAnnotations(...)45.45%1982228.57%
File 20: ProcessVariableExtension(...)0%110100%
File 20: TryApplyVariableTypeSchema(...)83.33%131284.62%
File 20: ApplyResponseCommonFields(...)75%4480%
File 20: GenerateDoc()100%11100%
File 20: ReadAndDiagnose(...)100%210%
File 20: ToJson(...)100%11100%
File 20: ToYaml(...)100%11100%
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    {
 2168        if (formAttr.Template is not null)
 169        {
 2170            if (!Host.Runtime.FormOptions.TryGetValue(formAttr.Template, out var template))
 171            {
 1172                throw new InvalidOperationException($"Form options template '{formAttr.Template}' not found.");
 173            }
 174
 175            // Clone the template to avoid modifying the original
 1176            routeOptions.FormOptions = new KrFormOptions(template);
 1177            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    {
 5194        if (Host.Logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 195        {
 5196            Host.Logger.Debug("Applying OpenApiExtension '{extensionName}' to function metadata", extensionAttr.Name);
 197        }
 5198        openApiMetadata.Extensions ??= [];
 199
 200        // Parse string into a JsonNode tree.
 5201        var node = JsonNode.Parse(extensionAttr.Json);
 5202        if (node is null)
 203        {
 2204            Host.Logger.Error("Error parsing OpenAPI extension '{extensionName}': JSON is null", extensionAttr.Name);
 2205            return;
 206        }
 3207        openApiMetadata.Extensions[extensionAttr.Name] = new JsonNodeExtension(node);
 3208    }
 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    {
 4228        var httpVerb = oaPath.HttpVerb ?? string.Empty;
 4229        if (!string.IsNullOrWhiteSpace(httpVerb))
 230        {
 4231            parsedVerb = HttpVerbExtensions.FromMethodString(httpVerb);
 4232            routeOptions.HttpVerbs.Add(parsedVerb);
 233        }
 234
 4235        var pattern = oaPath.Pattern;
 4236        if (string.IsNullOrWhiteSpace(pattern))
 237        {
 1238            throw new InvalidOperationException("OpenApiPath attribute must specify a non-empty Pattern property.");
 239        }
 3240        var openApiPattern = pattern;
 3241        var routePattern = pattern;
 3242        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
 3255        routeOptions.Pattern = routePattern;
 3256        metadata.Summary = ChooseFirstNonEmpty(oaPath.Summary, help.GetSynopsis());
 3257        metadata.Description = ChooseFirstNonEmpty(oaPath.Description, help.GetDescription());
 3258        metadata.Tags = [.. oaPath.Tags];
 259
 260        // Apply deprecated flag if specified
 3261        metadata.Deprecated |= oaPath.Deprecated;
 262        // Apply document ID if specified
 3263        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:
 1270                ApplyPathLikeWebhook(func, metadata, oaWebhook, pattern);
 1271                break;
 272            case OpenApiCallbackAttribute oaCallback:
 2273                ApplyPathLikeCallback(func, metadata, oaCallback, httpVerb, pattern);
 274                break;
 275        }
 276
 2277        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    {
 2317        if (queryParameterNames.Count == 0)
 318        {
 1319            return;
 320        }
 321
 1322        metadata.Parameters ??= [];
 8323        foreach (var name in queryParameterNames.Distinct(StringComparer.OrdinalIgnoreCase))
 324        {
 7325            if (metadata.Parameters.Any(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase)))
 326            {
 327                continue;
 328            }
 329
 2330            metadata.Parameters.Add(new OpenApiParameter
 2331            {
 2332                Name = name,
 2333                In = ParameterLocation.Query,
 2334                Required = false,
 2335                Schema = new OpenApiSchema { Type = JsonSchemaType.String },
 2336            });
 337        }
 1338    }
 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    {
 1348        metadata.Pattern = pattern;
 1349        metadata.PathLikeKind = OpenApiPathLikeKind.Webhook;
 1350        metadata.OperationId = oaPath.OperationId is null
 1351            ? func.Name
 1352            : string.IsNullOrWhiteSpace(oaPath.OperationId) ? metadata.OperationId : oaPath.OperationId;
 1353    }
 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
 2372        metadata.PathLikeKind = OpenApiPathLikeKind.Callback;
 2373        if (string.IsNullOrWhiteSpace(oaCallback.Expression))
 374        {
 1375            throw new InvalidOperationException("OpenApiCallback attribute must specify a non-empty Expression property.
 376        }
 377        // Callbacks must have an expression
 1378        metadata.Expression = CallbackOperationId.BuildCallbackKey(oaCallback.Expression, callbackPattern);
 1379        metadata.Inline = oaCallback.Inline;
 1380        metadata.Pattern = func.Name;
 1381        metadata.OperationId = string.IsNullOrWhiteSpace(oaCallback.OperationId)
 1382           ? CallbackOperationId.FromLastSegment(func.Name, httpVerb, oaCallback.Expression)
 1383           : oaCallback.OperationId;
 1384    }
 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    {
 51393        foreach (var value in values)
 394        {
 18395            if (!string.IsNullOrWhiteSpace(value))
 396            {
 1397                return NormalizeNewlines(value);
 398            }
 399        }
 400
 7401        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>
 1409    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        var iResponse = attribute.Inline || inline ? response!.CreateShallowCopy() : new OpenApiResponseReference(attrib
 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    {
 4547        if (schema is null)
 548        {
 0549            return null;
 550        }
 551
 552        // References (components) can represent arbitrary shapes.
 4553        if (schema is OpenApiSchemaReference)
 554        {
 0555            return typeof(object);
 556        }
 557
 4558        if (schema is not OpenApiSchema s)
 559        {
 0560            return typeof(object);
 561        }
 562
 563        // Ignore nullability bit; we only capture the underlying CLR type.
 4564        var type = (s.Type ?? JsonSchemaType.Object) & ~JsonSchemaType.Null;
 565
 4566        if ((type & JsonSchemaType.Array) != 0)
 567        {
 1568            var itemType = TryInferClrTypeFromSchema(s.Items);
 1569            return itemType is null ? typeof(object[]) : itemType.MakeArrayType();
 570        }
 571
 3572        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.
 3584        if ((type & JsonSchemaType.Object) != 0)
 585        {
 0586            return typeof(object);
 587        }
 588        // For booleans, we can directly map to bool.
 3589        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,
 3594        if ((type & JsonSchemaType.Integer) != 0)
 595        {
 1596            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, 
 2599        if ((type & JsonSchemaType.Number) != 0)
 600        {
 1601            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
 1604        if ((type & JsonSchemaType.String) != 0)
 605        {
 1606            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)
 1618        => 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)
 1626        => 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)
 1634        => format?.ToLowerInvariant() switch
 1635        {
 0636            "binary" => typeof(byte[]),
 1637            "uuid" => typeof(Guid),
 0638            "uri" => typeof(Uri),
 0639            "duration" => typeof(TimeSpan),
 0640            "date-time" => typeof(DateTimeOffset),
 0641            _ => typeof(string)
 1642        };
 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    {
 2651        return responses
 5652            .Select(kvp => new
 5653            {
 5654                StatusCode = kvp.Key,
 5655                Code = TryParseStatusCode(kvp.Key),
 5656                Response = kvp.Value
 5657            })
 2658            .Where(x =>
 5659                x.Code is >= 200 and < 300 &&
 5660                HasContent(x.Response))
 2661            .OrderBy(x => x.Code)
 1662            .Select(x => x.StatusCode)
 2663            .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)
 5672        => 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:
 4682        if (response is OpenApiResponse r)
 683        {
 4684            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    {
 1743        return [.. (string.IsNullOrWhiteSpace(policies) ? new List<string>() : [.. policies.Split(',')])
 4744            .Where(p => !string.IsNullOrWhiteSpace(p))
 4745            .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.CreateShallowCopy();
 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.CreateShallowCopy() : new OpenApiRequestBodyRefer
 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    {
 3998        if (string.IsNullOrWhiteSpace(attribute.ReferenceId))
 999        {
 21000            if (paramInfo.ParameterType.Name is "Object" or null)
 1001            {
 11002                throw new InvalidOperationException("OpenApiRequestBodyRefAttribute must have a ReferenceId specified wh
 1003            }
 1004
 11005            attribute.ReferenceId = paramInfo.ParameterType.Name;
 1006        }
 11007        else if (paramInfo.ParameterType.Name != "Object" && attribute.ReferenceId != paramInfo.ParameterType.Name)
 1008        {
 11009            return FindReferenceIdForParameter(attribute.ReferenceId, paramInfo);
 1010        }
 1011        // Return the reference ID as is
 11012        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
 21025        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
 21031        if (!IsRequestBodySchemaMatchForParameter(schema, paramInfo.ParameterType))
 1032        {
 11033            throw new InvalidOperationException(
 11034                $"Schema for request body component '{referenceId}' does not match parameter type '{paramInfo.ParameterT
 1035        }
 1036        // return the validated reference ID
 11037        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    {
 31048        schema = null!;
 1049
 31050        if (!TryGetRequestBodyItem(referenceId, out var requestBody, out _))
 1051        {
 11052            return false;
 1053        }
 1054
 21055        if (requestBody?.Content is null || requestBody.Content.Count == 0)
 1056        {
 01057            return false;
 1058        }
 1059
 21060        schema = requestBody.Content.First().Value.Schema!;
 21061        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    {
 31072        if (schema is OpenApiSchemaReference schemaRef)
 1073        {
 11074            return schemaRef.Reference.Id == parameterType.Name;
 1075        }
 1076
 21077        if (schema is OpenApiSchema inlineSchema && PrimitiveSchemaMap.TryGetValue(parameterType, out var valueType))
 1078        {
 21079            var expected = valueType();
 21080            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    {
 21212        var requestBody = metadata.RequestBody
 21213        ?? throw new InvalidOperationException(
 21214            "OpenApiRequestBodyExampleRefAttribute must follow OpenApiRequestBodyAttribute or OpenApiRequestBodyRefAttri
 1215
 11216        if (requestBody.Content is null)
 1217        {
 11218            throw new InvalidOperationException(
 11219                "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    {
 11235        var requestBody = new OpenApiRequestBody
 11236        {
 11237            Description = attribute.Description,
 11238            Required = attribute.Required,
 11239            Content = new Dictionary<string, IOpenApiMediaType>(StringComparer.OrdinalIgnoreCase)
 11240        };
 1241
 11242        var encoding = BuildMultipartEncoding(options);
 1243
 61244        foreach (var contentType in contentTypes)
 1245        {
 21246            if (string.IsNullOrWhiteSpace(contentType))
 1247            {
 1248                continue;
 1249            }
 1250
 21251            var mediaType = new OpenApiMediaType
 21252            {
 21253                Schema = schema
 21254            };
 1255
 21256            if (IsMultipartContentType(contentType) && encoding is not null)
 1257            {
 11258                mediaType.Encoding = encoding;
 1259            }
 1260
 21261            requestBody.Content[contentType] = mediaType;
 1262        }
 1263
 11264        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
 31276        if (attribute.ContentType is { Length: > 0 })
 1277        {
 11278            return attribute.ContentType;
 1279        }
 1280
 1281        // if allowed content types are specified, use those
 21282        if (options.AllowedContentTypes.Count > 0)
 1283        {
 11284            return [.. options.AllowedContentTypes];
 1285        }
 1286        // default to multipart/form-data
 11287        return ["multipart/form-data"];
 1288    }
 1289
 1290    private static bool IsMultipartContentType(string contentType)
 41291        => contentType.StartsWith("multipart/", StringComparison.OrdinalIgnoreCase);
 1292
 1293    private static Dictionary<string, OpenApiEncoding>? BuildMultipartEncoding(KrFormOptions options)
 1294    {
 31295        var encoding = new Dictionary<string, OpenApiEncoding>(StringComparer.Ordinal);
 1296
 141297        foreach (var rule in options.Rules)
 1298        {
 41299            if (string.IsNullOrWhiteSpace(rule.Name))
 1300            {
 1301                continue;
 1302            }
 1303
 41304            if (!IsProbablyFileRule(rule))
 1305            {
 1306                continue;
 1307            }
 1308
 21309            if (rule.AllowedContentTypes.Count == 0)
 1310            {
 1311                continue;
 1312            }
 1313
 21314            encoding[rule.Name] = new OpenApiEncoding
 21315            {
 21316                ContentType = string.Join(", ", rule.AllowedContentTypes)
 21317            };
 1318        }
 1319
 31320        return encoding.Count > 0 ? encoding : null;
 1321    }
 1322
 1323    private static bool IsProbablyFileRule(KrFormPartRule rule)
 1324    {
 81325        if (rule.StoreToDisk)
 1326        {
 31327            return true;
 1328        }
 1329
 51330        if (rule.AllowedExtensions.Count > 0)
 1331        {
 11332            return true;
 1333        }
 1334
 151335        foreach (var ct in rule.AllowedContentTypes)
 1336        {
 41337            if (string.IsNullOrWhiteSpace(ct))
 1338            {
 1339                continue;
 1340            }
 1341
 41342            if (!ct.StartsWith("text/", StringComparison.OrdinalIgnoreCase)
 41343                && !ct.Equals("application/json", StringComparison.OrdinalIgnoreCase)
 41344                && !ct.Equals("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase))
 1345            {
 11346                return true;
 1347            }
 1348        }
 1349
 31350        return false;
 11351    }
 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.CreateShallowCopy()
 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    {
 21385        metadata.Responses ??= [];
 21386        if (metadata.Responses.Count > 0)
 1387        {
 01388            return;
 1389        }
 21390        if (metadata.IsOpenApiCallback)
 1391        {
 11392            metadata.Responses.Add("204", new OpenApiResponse { Description = "Accepted" });
 1393        }
 1394        else
 1395        {
 11396            metadata.Responses.Add("200", new OpenApiResponse { Description = "Ok" });
 11397            metadata.Responses.Add("default", new OpenApiResponse { Description = "Unexpected error" });
 1398        }
 11399    }
 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    {
 31416        metadata.DocumentId ??= Host.OpenApiDocumentIds;
 31417        var documentIds = metadata.DocumentId;
 31418        if (metadata.IsOpenApiPath)
 1419        {
 11420            FinalizePathRouteOptions(func, sb, metadata, routeOptions, parsedVerb);
 11421            return;
 1422        }
 1423
 21424        if (metadata.IsOpenApiWebhook)
 1425        {
 11426            RegisterWebhook(func, sb, metadata, parsedVerb, documentIds);
 11427            return;
 1428        }
 1429
 11430        if (metadata.IsOpenApiCallback)
 1431        {
 11432            RegisterCallback(func, sb, metadata, parsedVerb, documentIds);
 1433        }
 11434    }
 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    {
 11451        routeOptions.OpenAPI.Add(parsedVerb, metadata);
 1452
 11453        if (string.IsNullOrWhiteSpace(routeOptions.Pattern))
 1454        {
 11455            routeOptions.Pattern = "/" + func.Name;
 1456        }
 1457
 11458        if (!string.IsNullOrWhiteSpace(metadata.CorsPolicy))
 1459        {
 01460            routeOptions.CorsPolicy = metadata.CorsPolicy;
 1461        }
 1462
 1463        // Set the script block or wrap for form options
 11464        routeOptions.ScriptCode.ScriptBlock = sb;
 11465        routeOptions.HandlerName = func.Name;
 11466        routeOptions.IsOpenApiAnnotatedFunctionRoute = true;
 11467        routeOptions.DefaultResponseContentType ??= new Dictionary<string, ICollection<ContentTypeWithSchema>>(Host.Opti
 11468        _ = Host.AddMapRoute(routeOptions);
 11469    }
 1470
 1471    /// <summary>
 1472    /// Registers a webhook in the OpenAPI document descriptors.
 1473    /// </summary>
 1474    /// <param name="func">The function information.</param>
 1475    /// <param name="sb">The script block.</param>
 1476    /// <param name="metadata">The OpenAPI path metadata.</param>
 1477    /// <param name="parsedVerb">The HTTP verb parsed from the function.</param>
 1478    /// <param name="documentIds">The collection of OpenAPI document IDs.</param>
 1479    private void RegisterWebhook(FunctionInfo func, ScriptBlock sb, OpenAPIPathMetadata metadata, HttpVerb parsedVerb, I
 1480    {
 11481        EnsureParamOnlyScriptBlock(func, sb, kind: "webhook");
 41482        foreach (var docId in documentIds)
 1483        {
 11484            var docdesc = GetDocDescriptorOrThrow(docId, attributeName: "OpenApiWebhook");
 11485            _ = docdesc.WebHook.TryAdd((metadata.Pattern, parsedVerb), metadata);
 1486        }
 11487    }
 1488    /// <summary>
 1489    /// Registers a callback in the OpenAPI document descriptors.
 1490    /// </summary>
 1491    /// <param name="func">The function information.</param>
 1492    /// <param name="sb">The script block.</param>
 1493    /// <param name="metadata">The OpenAPI path metadata.</param>
 1494    /// <param name="parsedVerb">The HTTP verb parsed from the function.</param>
 1495    /// <param name="documentIds">The collection of OpenAPI document IDs.</param>
 1496    private void RegisterCallback(FunctionInfo func, ScriptBlock sb, OpenAPIPathMetadata metadata, HttpVerb parsedVerb, 
 1497    {
 11498        EnsureParamOnlyScriptBlock(func, sb, kind: "callback");
 41499        foreach (var docId in documentIds)
 1500        {
 11501            var docdesc = GetDocDescriptorOrThrow(docId, attributeName: "OpenApiCallback");
 11502            _ = docdesc.Callbacks.TryAdd((metadata.Pattern, parsedVerb), metadata);
 1503        }
 11504    }
 1505
 1506    /// <summary>
 1507    /// Retrieves the OpenApiDocDescriptor for the specified document ID or throws an exception if not found.
 1508    /// </summary>
 1509    /// <param name="docId">The document ID to look up.</param>
 1510    /// <param name="attributeName">The name of the attribute requesting the document.</param>
 1511    /// <returns>The corresponding OpenApiDocDescriptor.</returns>
 1512    private OpenApiDocDescriptor GetDocDescriptorOrThrow(string docId, string attributeName)
 1513    {
 31514        return Host.OpenApiDocumentDescriptor.TryGetValue(docId, out var docdesc)
 31515            ? docdesc
 31516            : throw new InvalidOperationException($"The OpenAPI document ID '{docId}' specified in the {attributeName} a
 1517    }
 1518
 1519    /// <summary>
 1520    /// Ensures that the ScriptBlock contains only a param() block with no executable statements.
 1521    /// </summary>
 1522    /// <param name="func">The function information.</param>
 1523    /// <param name="sb">The ScriptBlock to validate.</param>
 1524    /// <param name="kind">The kind of function (e.g., "webhook" or "callback").</param>
 1525    /// <exception cref="InvalidOperationException">Thrown if the ScriptBlock contains executable statements other than 
 1526    private static void EnsureParamOnlyScriptBlock(FunctionInfo func, ScriptBlock sb, string kind)
 1527    {
 31528        if (!PsScriptBlockValidation.IsParamLast(sb))
 1529        {
 11530            throw new InvalidOperationException($"The ScriptBlock for {kind} function '{func.Name}' must contain only a 
 1531        }
 21532    }
 1533
 1534    /// <summary>
 1535    /// Creates a request body from the given attribute.
 1536    /// </summary>
 1537    /// <param name="attribute">The attribute containing request body information.</param>
 1538    /// <param name="requestBody">The OpenApiRequestBody object to populate.</param>
 1539    /// <param name="schema">The schema to associate with the request body.</param>
 1540    /// <returns>True if the request body was created successfully; otherwise, false.</returns>
 1541    private static bool CreateRequestBodyFromAttribute(KestrunAnnotation attribute, OpenApiRequestBody requestBody, IOpe
 1542    {
 1543        switch (attribute)
 1544        {
 1545            case OpenApiRequestBodyAttribute request:
 11546                requestBody.Description = request.Description;
 11547                requestBody.Required = request.Required;
 1548                // Content
 11549                requestBody.Content ??= new Dictionary<string, IOpenApiMediaType>(StringComparer.Ordinal);
 11550                var mediaType = new OpenApiMediaType();
 1551                // Example
 11552                if (request.Example is not null)
 1553                {
 11554                    mediaType.Example = OpenApiJsonNodeFactory.ToNode(request.Example);
 1555                }
 1556                // Schema
 11557                mediaType.Schema = schema;
 41558                foreach (var contentType in request.ContentType)
 1559                {
 11560                    requestBody.Content[contentType] = mediaType;
 1561                }
 11562                return true;
 1563            default:
 11564                return false;
 1565        }
 1566    }
 1567}

/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    {
 715        if (routes is null || routes.Count == 0)
 16        {
 217            return;
 18        }
 519        Document.Paths = [];
 20
 521        var groups = CreateOpenApiRouteEntries(routes)
 722            .GroupBy(entry => entry.Pattern, StringComparer.Ordinal)
 1023            .Where(g => !string.IsNullOrWhiteSpace(g.Key));
 24
 2025        foreach (var grp in groups)
 26        {
 527            ProcessOpenApiRouteGroup(grp);
 28        }
 529    }
 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    {
 2039        foreach (var kvp in routes)
 40        {
 541            var map = kvp.Value;
 542            if (map is null || map.OpenAPI.Count == 0)
 43            {
 44                continue;
 45            }
 46
 2447            foreach (var metaKvp in map.OpenAPI)
 48            {
 749                var meta = metaKvp.Value;
 750                var pattern = meta.Pattern;
 751                if (string.IsNullOrWhiteSpace(pattern))
 52                {
 053                    pattern = kvp.Key.Pattern;
 54                }
 55
 756                if (string.IsNullOrWhiteSpace(pattern))
 57                {
 58                    continue;
 59                }
 60
 761                yield return new OpenApiRouteEntry(pattern, metaKvp.Key, map, meta);
 62            }
 563        }
 564    }
 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    {
 572        var pattern = grp.Key;
 573        var pathItem = GetOrCreatePathItem(pattern);
 574        OpenAPICommonMetadata? pathMeta = null;
 75
 2476        foreach (var entry in grp)
 77        {
 778            pathMeta = ProcessOpenApiRouteEntry(entry, pathItem, pathMeta);
 79        }
 80
 581        if (pathMeta is not null)
 82        {
 183            ApplyPathLevelMetadata(pathItem, pathMeta, pattern);
 84        }
 585    }
 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    {
 593        Document.Paths ??= [];
 594        if (!Document.Paths.TryGetValue(pattern, out var pathInterface) || pathInterface is null)
 95        {
 596            pathInterface = new OpenApiPathItem();
 597            Document.Paths[pattern] = pathInterface;
 98        }
 599        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    {
 7114        var method = entry.Method;
 7115        var map = entry.Map;
 116
 7117        if (map is null || map.OpenAPI.Count == 0)
 118        {
 0119            return currentPathMeta;
 120        }
 121
 7122        if ((map.PathLevelOpenAPIMetadata is not null) && (currentPathMeta is null))
 123        {
 1124            currentPathMeta = map.PathLevelOpenAPIMetadata;
 125        }
 126
 7127        var meta = entry.Metadata;
 7128        if (meta.Enabled)
 129        {
 6130            if (meta.DocumentId is not null && !meta.DocumentId.Contains(DocumentId))
 131            {
 1132                return currentPathMeta;
 133            }
 5134            var op = BuildOperationFromMetadata(meta);
 5135            pathItem.AddOperation(HttpMethod.Parse(method.ToMethodString()), op);
 136        }
 137
 6138        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>
 7148    private sealed record OpenApiRouteEntry(
 7149        string Pattern,
 7150        HttpVerb Method,
 7151        MapRouteOptions Map,
 14152        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    {
 1162        pathItem.Description = pathMeta.Description;
 1163        pathItem.Summary = pathMeta.Summary;
 164        try
 165        {
 1166            ApplyPathLevelServers(pathItem, pathMeta);
 1167            ApplyPathLevelParameters(pathItem, pathMeta);
 1168        }
 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        }
 1176    }
 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    {
 1185        if (pathMeta.Servers is { Count: > 0 })
 186        {
 1187            dynamic dPath = pathItem;
 2188            if (dPath.Servers == null) { dPath.Servers = new List<OpenApiServer>(); }
 4189            foreach (var s in pathMeta.Servers)
 190            {
 1191                dPath.Servers.Add(s);
 192            }
 193        }
 1194    }
 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    {
 1203        if (pathMeta.Parameters is { Count: > 0 })
 204        {
 1205            dynamic dPath = pathItem;
 2206            if (dPath.Parameters == null) { dPath.Parameters = new List<IOpenApiParameter>(); }
 4207            foreach (var p in pathMeta.Parameters)
 208            {
 1209                dPath.Parameters.Add(p);
 210            }
 211        }
 1212    }
 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{
 18910    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    {
 24319        var properties = prop.GetCustomAttribute<OpenApiProperties>();
 24320        var xmlAttr = prop.GetCustomAttribute<OpenApiXmlAttribute>();
 21
 24322        if (xmlAttr == null)
 23        {
 23524            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    {
 5766        if (Document.Components is not null && Document.Components.Schemas is not null)
 67        {
 5768            if (!ComponentSchemasExists(t.Name))
 69            {
 4770                if (!PrimitiveSchemaMap.ContainsKey(t))
 71                {
 4772                    Document.Components.Schemas[t.Name] = BuildSchemaForType(t, built);
 73                }
 74            }
 75        }
 5776    }
 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    {
 21086        var (propertyType, allowNull) = UnwrapNullableType(p.PropertyType);
 87
 21088        if (propertyType == typeof(KrFilePart))
 89        {
 090            return BuildFilePartSchema(p, allowNull);
 91        }
 92
 21093        ApplyKrPartScope(p, propertyType, out var pushScope);
 94
 95        IOpenApiSchema schema;
 96        try
 97        {
 21098            schema = BuildPropertyTypeSchema(propertyType, p, built);
 21099        }
 100        finally
 101        {
 210102            if (pushScope)
 103            {
 11104                _ = _formPartScopeStack.Pop();
 105            }
 210106        }
 107
 210108        schema = ApplyNullableSchema(schema, allowNull);
 210109        ApplySchemaAttr(MergeXmlAttributes(p), schema);
 210110        PowerShellAttributes.ApplyPowerShellAttributes(p, schema);
 210111        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    {
 210121        var underlying = Nullable.GetUnderlyingType(propertyType);
 210122        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    {
 210151        var currentScope = _formPartScopeStack.Count > 0 ? _formPartScopeStack.Peek() : null;
 210152        FormHelper.ApplyKrPartAttributes(Host, p, currentScope);
 153
 210154        var hasKrPartAttribute = p.IsDefined(typeof(KrPartAttribute), inherit: false);
 210155        var partName = hasKrPartAttribute ? FormHelper.ResolvePartName(p) : null;
 210156        pushScope = hasKrPartAttribute && !string.IsNullOrWhiteSpace(partName) && ShouldPushNestedScope(propertyType);
 210157        if (pushScope)
 158        {
 11159            _formPartScopeStack.Push(partName);
 160        }
 210161    }
 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    {
 210172        if (PrimitiveSchemaMap.TryGetValue(propertyType, out var getSchema))
 173        {
 181174            return getSchema();
 175        }
 176
 29177        if (propertyType.IsArray)
 178        {
 12179            return BuildArraySchema(propertyType, p, built);
 180        }
 181
 182        // Treat enums and complex types the same: register as component and reference
 17183        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    {
 210194        if (!allowNull)
 195        {
 200196            return schema;
 197        }
 198
 10199        if (schema is OpenApiSchema s)
 200        {
 201            // For inline schemas, add null type directly
 9202            s.Type |= JsonSchemaType.Null;
 9203            return s;
 204        }
 205
 1206        if (schema is OpenApiSchemaReference refSchema)
 207        {
 1208            var modifiedRefSchema = refSchema.CreateShallowCopy();
 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    {
 22231        var candidate = propertyType;
 232
 22233        if (candidate.IsArray)
 234        {
 5235            candidate = candidate.GetElementType()!;
 236        }
 237
 22238        if (candidate.IsGenericType && candidate.GetGenericTypeDefinition() == typeof(Nullable<>))
 239        {
 0240            candidate = Nullable.GetUnderlyingType(candidate)!;
 241        }
 242
 22243        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    {
 18255        BuildSchema(pt, built); // ensure component exists
 18256        var refSchema = new OpenApiSchemaReference(pt.Name);
 18257        ApplySchemaAttr(MergeXmlAttributes(p), refSchema);
 18258        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    {
 15290        var item = pt.GetElementType()!;
 291        IOpenApiSchema itemSchema;
 292
 15293        if (item == typeof(KrFilePart))
 294        {
 0295            itemSchema = new OpenApiSchema
 0296            {
 0297                Type = JsonSchemaType.String,
 0298                Format = "binary"
 0299            };
 300        }
 301        else
 302        {
 15303            if (PrimitiveSchemaMap.TryGetValue(item, out var getSchema))
 304            {
 8305                itemSchema = getSchema();
 306            }
 307            else
 308            {
 309                // Treat enums and complex types the same: register as component and reference
 7310                BuildSchema(item, built); // ensure component exists
 7311                itemSchema = new OpenApiSchemaReference(item.Name);
 312            }
 313        }
 15314        var s = new OpenApiSchema
 15315        {
 15316            Type = JsonSchemaType.Array,
 15317            Items = itemSchema
 15318        };
 15319        ApplySchemaAttr(MergeXmlAttributes(p), s);
 15320        PowerShellAttributes.ApplyPowerShellAttributes(p, s);
 15321        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    {
 221        metadata.Callbacks ??= new Dictionary<string, IOpenApiCallback>();
 22
 223        if (TryGetInline(name: attribute.ReferenceId, kind: OpenApiComponentKind.Callbacks, out OpenApiCallback? callbac
 24        {
 025            metadata.Callbacks.Add(attribute.Key, callback!.CreateShallowCopy());
 26        }
 227        else if (TryGetComponent(name: attribute.ReferenceId, kind: OpenApiComponentKind.Callbacks, out callback))
 28        {
 129            if (attribute.Inline)
 30            {
 031                metadata.Callbacks.Add(attribute.Key, callback!.CreateShallowCopy());
 32            }
 33            else
 34            {
 135                var reference = new OpenApiCallbackReference(attribute.ReferenceId);
 136                metadata.Callbacks.Add(attribute.Key, reference);
 37            }
 38        }
 139        else if (attribute.Inline)
 40        {
 141            throw new InvalidOperationException($"Inline callback component with ID '{attribute.ReferenceId}' not found.
 42        }
 43
 144        if (callback is not null)
 45        {
 46            // Compile and store the CallbackPlan for this callback
 147            metadata.MapOptions.CallbackPlan.AddRange(CallbackPlanCompiler.Compile(callback, attribute.ReferenceId));
 48        }
 149    }
 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    {
 157        if (Metadata is null || Metadata.Count == 0)
 58        {
 059            return;
 60        }
 61
 162        var groups = Metadata
 263            .GroupBy(kvp => kvp.Key.Pattern, StringComparer.Ordinal)
 264            .Where(g => !string.IsNullOrWhiteSpace(g.Key));
 65
 466        foreach (var grp in groups)
 67        {
 168            ProcessCallbacksGroup(grp);
 69        }
 170    }
 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    {
 177        var pattern = grp.Key;
 78
 679        foreach (var kvp in grp)
 80        {
 281            if (kvp.Value.DocumentId is not null && !kvp.Value.DocumentId.Contains(DocumentId))
 82            {
 83                continue;
 84            }
 285            var callbackItem = GetOrCreateCallbackItem(pattern, kvp.Value.Inline);
 286            ProcessCallbackOperation(kvp, callbackItem);
 87        }
 188    }
 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    {
 498        callbackItem.PathItems ??= [];
 499        var method = kvp.Key.Method;
 4100        var openapiMetadata = kvp.Value;
 4101        if (openapiMetadata.Expression is null)
 102        {
 1103            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
 3106        var expr = openapiMetadata.Expression;
 3107        var httpMethod = HttpMethod.Parse(method.ToMethodString());
 108        // Only add the path item if it doesn't already exist
 3109        if (!callbackItem.PathItems.TryGetValue(expr, out var iPathItem))
 110        {
 1111            var op = BuildOperationFromMetadata(openapiMetadata);
 1112            var pathItem = new OpenApiPathItem();
 1113            pathItem.AddOperation(httpMethod, op);
 114            // Add the new path item to the callback
 1115            callbackItem.PathItems.Add(expr, pathItem);
 116        }
 117        else
 118        {
 2119            if (iPathItem is OpenApiPathItem pathItem)
 120            {
 1121                if (pathItem.Operations is not null && pathItem.Operations.ContainsKey(httpMethod))
 122                {
 123                    // Operation for this method already exists; skip adding
 0124                    return;
 125                }
 1126                var op = BuildOperationFromMetadata(openapiMetadata);
 1127                pathItem.AddOperation(httpMethod, op);
 128            }
 129            else
 130            {
 1131                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
 2146        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
 2155            Document.Components ??= new OpenApiComponents();
 2156            Document.Components.Callbacks ??= new Dictionary<string, IOpenApiCallback>(StringComparer.Ordinal);
 2157            callbacks = Document.Components.Callbacks;
 158        }
 159        // Retrieve or create the callback item
 2160        if (!callbacks.TryGetValue(pattern, out var pathInterface) || pathInterface is null)
 161        {
 162            // Create a new OpenApiCallback if it doesn't exist
 1163            pathInterface = new OpenApiCallback();
 1164            callbacks[pattern] = pathInterface;
 165        }
 166        // return the callback item
 2167        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    {
 13177        if (meta.Callbacks is not null && meta.Callbacks.Count > 0)
 178        {
 0179            op.Callbacks = new Dictionary<string, IOpenApiCallback>(meta.Callbacks);
 180        }
 13181    }
 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    {
 1722        Document.Components ??= new OpenApiComponents();
 23        // Ensure Examples dictionary exists
 1724        Document.Components.Examples ??= new Dictionary<string, IOpenApiExample>(StringComparer.Ordinal);
 1725        AddComponent(Document.Components.Examples, name,
 1726                        example, ifExists,
 1727                        OpenApiComponentKind.Examples);
 1628    }
 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!.CreateShallowCopy());
 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            var oaExample = attribute.Inline ? example!.CreateShallowCopy() : 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    {
 374        var example = new OpenApiExample
 375        {
 376            Summary = summary
 377        };
 78
 379        if (!string.IsNullOrWhiteSpace(description))
 80        {
 281            example.Description = description;
 82        }
 83        // Extensions
 384        example.Extensions = BuildExtensions(extensions);
 85
 386        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    {
 1103        var example = NewOpenApiExample(
 1104               summary: summary,
 1105               description: description,
 1106               extensions: extensions);
 107
 108        // AllowNull: treat null as null JsonNode
 1109        example.Value = OpenApiJsonNodeFactory.ToNode(value);
 110        // return example
 1111        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    {
 1128        var example = NewOpenApiExample(
 1129                summary: summary,
 1130                description: description,
 1131                extensions: extensions);
 132
 1133        example.ExternalValue = externalValue;
 134
 135        // return example
 1136        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    {
 1155        var example = NewOpenApiExample(
 1156               summary: summary,
 1157               description: description,
 1158               extensions: extensions);
 159
 1160        example.DataValue = OpenApiJsonNodeFactory.ToNode(dataValue);
 1161        if (!string.IsNullOrWhiteSpace(serializedValue))
 162        {
 1163            example.SerializedValue = serializedValue;
 164        }
 165
 166        // return example
 1167        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!.CreateShallowCopy();
 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!.CreateShallowCopy();
 215            }
 216
 1217            if (TryGetComponent(name: mediaRef, kind: OpenApiComponentKind.MediaTypes, out OpenApiMediaType? componentMe
 218            {
 219                // if in main components, clone it
 1220                return componentMediaType!.CreateShallowCopy();
 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!.CreateShallowCopy());
 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            var oaHeader = attribute.Inline ? header!.CreateShallowCopy() : new OpenApiHeaderReference(attribute.Referen
 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    {
 114        ArgumentException.ThrowIfNullOrWhiteSpace(id);
 15        // Look up schema in components
 116        return Document.Components?.Schemas is { } schemas
 117               && schemas.TryGetValue(id, out var p)
 118               && p is IOpenApiSchema op
 119            ? op
 120            : throw new InvalidOperationException($"Schema '{id}' not found.");
 21    }
 22
 23    private OpenApiParameter GetParameter(string id)
 24    {
 125        ArgumentException.ThrowIfNullOrWhiteSpace(id);
 26        // Look up parameter in components
 127        return Document.Components?.Parameters is { } parameters
 128               && parameters.TryGetValue(id, out var p)
 129               && p is OpenApiParameter op
 130            ? op
 131            : throw new InvalidOperationException($"Parameter '{id}' not found.");
 32    }
 33
 34    private OpenApiRequestBody GetRequestBody(string id)
 35    {
 136        ArgumentException.ThrowIfNullOrWhiteSpace(id);
 37        // Look up request body in components
 138        return Document.Components?.RequestBodies is { } requestBodies
 139               && requestBodies.TryGetValue(id, out var p)
 140               && p is OpenApiRequestBody op
 141            ? op
 142            : throw new InvalidOperationException($"RequestBody '{id}' not found.");
 43    }
 44
 45    private OpenApiHeader GetHeader(string id)
 46    {
 147        ArgumentException.ThrowIfNullOrWhiteSpace(id);
 48        // Look up header in components
 149        return Document.Components?.Headers is { } headers
 150               && headers.TryGetValue(id, out var p)
 151               && p is OpenApiHeader op
 152            ? op
 153            : throw new InvalidOperationException($"Header '{id}' not found.");
 54    }
 55
 56    private OpenApiResponse GetResponse(string id)
 57    {
 158        ArgumentException.ThrowIfNullOrWhiteSpace(id);
 59        // Look up response in components
 160        return Document.Components?.Responses is { } responses
 161               && responses.TryGetValue(id, out var p)
 162               && p is OpenApiResponse op
 163            ? op
 164            : throw new InvalidOperationException($"Response '{id}' not found.");
 65    }
 66
 67    private bool ComponentSchemasExists(string id) =>
 6068        Document.Components?.Schemas?.ContainsKey(id) == true;
 69
 70    private bool ComponentRequestBodiesExists(string id) =>
 171        Document.Components?.RequestBodies?.ContainsKey(id) == true;
 72
 73    private bool ComponentResponsesExists(string id) =>
 174        Document.Components?.Responses?.ContainsKey(id) == true;
 75
 76    private bool ComponentParametersExists(string id) =>
 177        Document.Components?.Parameters?.ContainsKey(id) == true;
 78
 79    private bool ComponentExamplesExists(string id) =>
 180        Document.Components?.Examples?.ContainsKey(id) == true;
 81
 82    private bool ComponentHeadersExists(string id) =>
 183        Document.Components?.Headers?.ContainsKey(id) == true;
 84    private bool ComponentCallbacksExists(string id) =>
 185        Document.Components?.Callbacks?.ContainsKey(id) == true;
 86
 87    private bool ComponentLinksExists(string id) =>
 188        Document.Components?.Links?.ContainsKey(id) == true;
 89    private bool ComponentPathItemsExists(string id) =>
 190        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    {
 22100        if (extensions is null || extensions.Count == 0)
 101        {
 14102            return null;
 103        }
 104
 8105        Dictionary<string, IOpenApiExtension>? result = null;
 106
 80107        foreach (DictionaryEntry entry in extensions)
 108        {
 32109            var rawKey = entry.Key?.ToString();
 32110            if (string.IsNullOrWhiteSpace(rawKey))
 111            {
 112                continue;
 113            }
 114
 115            string key;
 27116            if (rawKey.StartsWith("x-", StringComparison.Ordinal))
 117            {
 25118                key = rawKey;
 119            }
 120            else
 121            {
 2122                Host.Logger.WarningSanitized("OpenAPI extension '{rawKey}' is invalid. Extension names must start with '
 2123                continue;
 124            }
 125
 25126            var node = OpenApiJsonNodeFactory.ToNode(entry.Value);
 25127            if (node is null)
 128            {
 7129                Host.Logger.WarningSanitized("OpenAPI extension '{key}' has a null value and will be skipped.", key);
 7130                continue;
 131            }
 132
 18133            result ??= new Dictionary<string, IOpenApiExtension>(StringComparer.Ordinal);
 18134            result[key] = new JsonNodeExtension(node);
 135        }
 136
 8137        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    {
 2565        ArgumentNullException.ThrowIfNull(components);
 2566        ArgumentNullException.ThrowIfNull(name);
 2567        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:
 2085                components[name] = value;
 2086                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)
 29105     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)
 33119        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    {
 62138        ArgumentNullException.ThrowIfNull(name);
 62139        value = null;
 140
 62141        if (components is null)
 142        {
 0143            return false;
 144        }
 145
 62146        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.
 60150        return kind switch
 60151        {
 0152            OpenApiComponentKind.Schemas => TryGetAndCast(components.Schemas, name, out value),
 4153            OpenApiComponentKind.Responses => TryGetAndCast(components.Responses, name, out value),
 12154            OpenApiComponentKind.Parameters => TryGetAndCast(components.Parameters, name, out value),
 14155            OpenApiComponentKind.Examples => TryGetAndCast(components.Examples, name, out value),
 16156            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),
 4160            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)
 60164        };
 165
 166        static bool TryGetAndCast<TSpecific>(
 167            IDictionary<string, TSpecific>? dict,
 168            string componentName,
 169            out T? component)
 170            where TSpecific : class
 171        {
 60172            if (TryGet(dict, componentName, out var specific) && specific is not null)
 173            {
 21174                component = (T)(object)specific;
 21175                return true;
 176            }
 177
 39178            component = null;
 39179            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    {
 62191        var expectedType = kind switch
 62192        {
 0193            OpenApiComponentKind.Schemas => typeof(IOpenApiSchema),
 4194            OpenApiComponentKind.Responses => typeof(OpenApiResponse),
 12195            OpenApiComponentKind.Parameters => typeof(OpenApiParameter),
 15196            OpenApiComponentKind.Examples => typeof(OpenApiExample),
 16197            OpenApiComponentKind.RequestBodies => typeof(OpenApiRequestBody),
 0198            OpenApiComponentKind.Headers => typeof(OpenApiHeader),
 0199            OpenApiComponentKind.SecuritySchemes => typeof(OpenApiSecurityScheme),
 8200            OpenApiComponentKind.Links => typeof(OpenApiLink),
 4201            OpenApiComponentKind.Callbacks => typeof(OpenApiCallback),
 0202            OpenApiComponentKind.PathItems => typeof(OpenApiPathItem),
 2203            OpenApiComponentKind.MediaTypes => typeof(OpenApiMediaType),
 1204            _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null)
 62205        };
 206
 61207        if (typeof(T) != expectedType)
 208        {
 1209            ThrowTypeMismatch<T>(kind);
 210        }
 60211    }
 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    {
 60227        if (dict is not null && dict.TryGetValue(name, out var v))
 228        {
 21229            value = v;
 21230            return true;
 231        }
 232
 39233        value = null;
 39234        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    /// <summary>
 29    /// Tries to add a link to the given links dictionary based on the provided attribute.
 30    /// </summary>
 31    /// <param name="links">The dictionary of links to which the link will be added.</param>
 32    /// <param name="attribute">The attribute containing the link information.</param>
 33    /// <returns>True if the link was successfully added; otherwise, false.</returns>
 34    /// <exception cref="InvalidOperationException">Thrown when the attribute is missing required properties.</exception
 35    private bool TryAddLink(IDictionary<string, IOpenApiLink> links, OpenApiResponseLinkRefAttribute attribute)
 36    {
 437        if (TryGetInline(name: attribute.ReferenceId, kind: OpenApiComponentKind.Links, out OpenApiLink? link))
 38        {
 39            // If InlineComponents, clone the example
 140            return links.TryAdd(attribute.Key, link!.CreateShallowCopy());
 41        }
 342        else if (TryGetComponent(name: attribute.ReferenceId, kind: OpenApiComponentKind.Links, out link))
 43        {
 44            // if in main components, reference it or clone based on Inline flag
 145            var oaLink = attribute.Inline ? link!.CreateShallowCopy() : new OpenApiLinkReference(attribute.ReferenceId);
 146            return links.TryAdd(attribute.Key, oaLink);
 47        }
 248        else if (attribute.Inline)
 49        {
 150            throw new InvalidOperationException($"Inline link component with ID '{attribute.ReferenceId}' not found.");
 51        }
 152        return false;
 53    }
 54
 55    /// <summary>
 56    /// Applies an OpenApiResponseLinkRefAttribute to the specified OpenAPI path metadata.
 57    /// </summary>
 58    /// <param name="metadata">The OpenAPI path metadata to which the link attribute will be applied.</param>
 59    /// <param name="attribute">The OpenApiResponseLinkRefAttribute to apply.</param>
 60    /// <exception cref="InvalidOperationException">Thrown when the attribute is missing required properties.</exception
 61    private void ApplyResponseLinkAttribute(OpenAPIPathMetadata metadata, OpenApiResponseLinkRefAttribute attribute)
 62    {
 263        if (attribute.StatusCode is null)
 64        {
 165            throw new InvalidOperationException("OpenApiLinkAttribute must have a StatusCode specified to associate the 
 66        }
 167        if (attribute.Key is null)
 68        {
 169            throw new InvalidOperationException("OpenApiLinkRefAttribute must have a Key specified to define the link na
 70        }
 071        metadata.Responses ??= [];
 072        var response = metadata.Responses.TryGetValue(attribute.StatusCode, out var value) ? value as OpenApiResponse : 
 073        if (response is not null && CreateResponseFromAttribute(attribute, response))
 74        {
 075            _ = metadata.Responses.TryAdd(attribute.StatusCode, response);
 76        }
 077    }
 78
 79    /// <summary>
 80    /// Applies an OpenApiResponseLinkRefAttribute to the specified OpenAPI response.
 81    /// </summary>
 82    /// <param name="attribute">The OpenApiResponseLinkRefAttribute to apply.</param>
 83    /// <param name="response">The OpenAPI response to which the link attribute will be applied.</param>
 84    /// <returns>True if the link was successfully applied; otherwise, false.</returns>
 85    private bool ApplyLinkRefAttribute(OpenApiResponseLinkRefAttribute attribute, OpenApiResponse response)
 86    {
 087        response.Links ??= new Dictionary<string, IOpenApiLink>();
 88        // Clone or reference the example
 089        _ = TryAddLink(response.Links, attribute);
 90
 091        return true;
 92    }
 93
 94    /// <summary>
 95    /// Creates a new OpenApiLink instance based on the provided parameters.
 96    /// </summary>
 97    /// <param name="operationRef">Operation reference string.</param>
 98    /// <param name="operationId">Operation identifier string.</param>
 99    /// <param name="description">Description of the link.</param>
 100    /// <param name="server">Server object associated with the link.</param>
 101    /// <param name="parameters">Parameters dictionary for the link.</param>
 102    /// <param name="requestBody">Request body object or expression.</param>
 103    /// <param name="extensions">Extensions dictionary for the link.</param>
 104    /// <returns>Newly created OpenApiLink instance.</returns>
 105    /// <exception cref="ArgumentException">Thrown when both operationRef and operationId are provided.</exception>
 106    public OpenApiLink NewOpenApiLink(
 107           string? operationRef,
 108           string? operationId,
 109           string? description,
 110           OpenApiServer? server,
 111           IDictionary? parameters,
 112           object? requestBody,
 113           IDictionary? extensions)
 114    {
 2115        ValidateLinkOperation(operationRef, operationId);
 116
 1117        var link = new OpenApiLink
 1118        {
 1119            Extensions = BuildExtensions(extensions)
 1120        };
 121
 1122        ApplyLinkDescription(link, description);
 1123        ApplyLinkServer(link, server);
 1124        ApplyLinkOperation(link, operationRef, operationId);
 1125        ApplyLinkRequestBody(link, requestBody);
 1126        ApplyLinkParameters(link, parameters);
 127
 1128        return link;
 129    }
 130
 131    /// <summary>
 132    /// Validates that exactly one of <paramref name="operationRef"/> or <paramref name="operationId"/> is provided.
 133    /// </summary>
 134    /// <param name="operationRef">The operation reference string.</param>
 135    /// <param name="operationId">The operation identifier string.</param>
 136    /// <exception cref="ArgumentException">Thrown when both are provided, or when neither is provided.</exception>
 137    private static void ValidateLinkOperation(string? operationRef, string? operationId)
 138    {
 139        // Match the PS safety rule.
 2140        if (!string.IsNullOrWhiteSpace(operationRef) && !string.IsNullOrWhiteSpace(operationId))
 141        {
 1142            throw new ArgumentException("OperationId and OperationRef are mutually exclusive in an OpenAPI Link.");
 143        }
 144
 1145        if (string.IsNullOrWhiteSpace(operationRef) && string.IsNullOrWhiteSpace(operationId))
 146        {
 147            // Should be prevented by parameter sets, but keep it robust.
 0148            throw new ArgumentException("Either OperationRef or OperationId must be provided.");
 149        }
 1150    }
 151
 152    /// <summary>
 153    /// Applies the description to the link when provided.
 154    /// </summary>
 155    /// <param name="link">The link to update.</param>
 156    /// <param name="description">The description value.</param>
 157    private static void ApplyLinkDescription(OpenApiLink link, string? description)
 158    {
 1159        if (!string.IsNullOrWhiteSpace(description))
 160        {
 1161            link.Description = description;
 162        }
 1163    }
 164
 165    /// <summary>
 166    /// Applies the server to the link when provided.
 167    /// </summary>
 168    /// <param name="link">The link to update.</param>
 169    /// <param name="server">The server value.</param>
 170    private static void ApplyLinkServer(OpenApiLink link, OpenApiServer? server)
 171    {
 1172        if (server is not null)
 173        {
 1174            link.Server = server;
 175        }
 1176    }
 177
 178    /// <summary>
 179    /// Applies <see cref="OpenApiLink.OperationRef"/> or <see cref="OpenApiLink.OperationId"/>.
 180    /// </summary>
 181    /// <param name="link">The link to update.</param>
 182    /// <param name="operationRef">The operation reference string.</param>
 183    /// <param name="operationId">The operation identifier string.</param>
 184    private static void ApplyLinkOperation(OpenApiLink link, string? operationRef, string? operationId)
 185    {
 1186        if (!string.IsNullOrWhiteSpace(operationRef))
 187        {
 0188            link.OperationRef = operationRef;
 0189            return;
 190        }
 191
 1192        if (!string.IsNullOrWhiteSpace(operationId))
 193        {
 1194            link.OperationId = operationId;
 195        }
 1196    }
 197
 198    /// <summary>
 199    /// Applies the request body to the link, interpreting string values as runtime expressions.
 200    /// </summary>
 201    /// <param name="link">The link to update.</param>
 202    /// <param name="requestBody">The request body value.</param>
 203    private static void ApplyLinkRequestBody(OpenApiLink link, object? requestBody)
 204    {
 1205        if (requestBody is null)
 206        {
 0207            return;
 208        }
 209
 1210        var wrapper = new RuntimeExpressionAnyWrapper();
 211
 1212        if (requestBody is string s)
 213        {
 1214            if (string.IsNullOrWhiteSpace(s))
 215            {
 0216                return;
 217            }
 218
 1219            wrapper.Expression = RuntimeExpression.Build(s);
 1220            link.RequestBody = wrapper;
 1221            return;
 222        }
 223
 0224        wrapper.Any = OpenApiJsonNodeFactory.ToNode(requestBody);
 0225        link.RequestBody = wrapper;
 0226    }
 227
 228    /// <summary>
 229    /// Applies link parameters, interpreting string values as runtime expressions.
 230    /// </summary>
 231    /// <param name="link">The link to update.</param>
 232    /// <param name="parameters">The parameters dictionary.</param>
 233    private static void ApplyLinkParameters(OpenApiLink link, IDictionary? parameters)
 234    {
 1235        if (parameters is null || parameters.Count == 0)
 236        {
 0237            return;
 238        }
 239
 1240        link.Parameters ??= new Dictionary<string, RuntimeExpressionAnyWrapper>(StringComparer.Ordinal);
 241
 6242        foreach (DictionaryEntry entry in parameters)
 243        {
 2244            if (entry.Key is null)
 245            {
 246                continue;
 247            }
 248
 2249            var key = entry.Key.ToString();
 2250            if (string.IsNullOrWhiteSpace(key))
 251            {
 252                continue;
 253            }
 254
 2255            link.Parameters[key] = ToRuntimeExpressionAnyWrapper(entry.Value);
 256        }
 1257    }
 258
 259    /// <summary>
 260    /// Converts a value into a <see cref="RuntimeExpressionAnyWrapper"/>.
 261    /// </summary>
 262    /// <param name="value">The value to wrap.</param>
 263    /// <returns>A wrapper containing either a runtime expression or an OpenAPI JSON node.</returns>
 264    private static RuntimeExpressionAnyWrapper ToRuntimeExpressionAnyWrapper(object? value)
 265    {
 2266        var wrapper = new RuntimeExpressionAnyWrapper();
 267
 2268        if (value is string expr)
 269        {
 1270            wrapper.Expression = RuntimeExpression.Build(expr);
 271        }
 272        else
 273        {
 1274            wrapper.Any = OpenApiJsonNodeFactory.ToNode(value);
 275        }
 276
 2277        return wrapper;
 278    }
 279}

/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    {
 517        if (attrs == null || attrs.Length == 0)
 18        {
 219            return null;
 20        }
 21
 322        if (attrs.Length == 1)
 23        {
 124            return attrs[0];
 25        }
 26
 227        var m = new OpenApiPropertyAttribute();
 28
 1429        foreach (var a in attrs)
 30        {
 531            MergeStringProperties(m, a);
 532            MergeEnumAndCollections(m, a);
 533            MergeNumericProperties(m, a);
 534            MergeBooleanProperties(m, a);
 535            MergeTypeAndRequired(m, a);
 536            MergeCustomFields(m, a);
 37        }
 38
 239        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    {
 549        if (!string.IsNullOrWhiteSpace(attr.Title))
 50        {
 251            merged.Title = attr.Title;
 52        }
 53
 554        if (!string.IsNullOrWhiteSpace(attr.Description))
 55        {
 156            merged.Description = attr.Description;
 57        }
 58
 559        if (!string.IsNullOrWhiteSpace(attr.Format))
 60        {
 161            merged.Format = attr.Format;
 62        }
 63
 564        if (!string.IsNullOrWhiteSpace(attr.Pattern))
 65        {
 266            merged.Pattern = attr.Pattern;
 67        }
 68
 569        if (!string.IsNullOrWhiteSpace(attr.Maximum))
 70        {
 171            merged.Maximum = attr.Maximum;
 72        }
 73
 574        if (!string.IsNullOrWhiteSpace(attr.Minimum))
 75        {
 276            merged.Minimum = attr.Minimum;
 77        }
 578    }
 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    {
 587        if (attr.Enum is { Length: > 0 })
 88        {
 289            merged.Enum = [.. merged.Enum ?? [], .. attr.Enum];
 90        }
 91
 592        if (attr.Default is not null)
 93        {
 294            merged.Default = attr.Default;
 95        }
 96
 597        if (attr.Example is not null)
 98        {
 299            merged.Example = attr.Example;
 100        }
 5101    }
 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    {
 5110        if (attr.MaxLength >= 0)
 111        {
 1112            merged.MaxLength = attr.MaxLength;
 113        }
 114
 5115        if (attr.MinLength >= 0)
 116        {
 1117            merged.MinLength = attr.MinLength;
 118        }
 119
 5120        if (attr.MaxItems >= 0)
 121        {
 1122            merged.MaxItems = attr.MaxItems;
 123        }
 124
 5125        if (attr.MinItems >= 0)
 126        {
 1127            merged.MinItems = attr.MinItems;
 128        }
 129
 5130        if (attr.MultipleOf is not null)
 131        {
 2132            merged.MultipleOf = attr.MultipleOf;
 133        }
 5134    }
 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    {
 5143        merged.Nullable |= attr.Nullable;
 5144        merged.ReadOnly |= attr.ReadOnly;
 5145        merged.WriteOnly |= attr.WriteOnly;
 5146        merged.Deprecated |= attr.Deprecated;
 5147        merged.UniqueItems |= attr.UniqueItems;
 5148        merged.ExclusiveMaximum |= attr.ExclusiveMaximum;
 5149        merged.ExclusiveMinimum |= attr.ExclusiveMinimum;
 5150    }
 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    {
 5159        if (attr.Type != OaSchemaType.None)
 160        {
 3161            merged.Type = attr.Type;
 162        }
 163
 5164        if (attr.RequiredProperties is { Length: > 0 })
 165        {
 2166            merged.RequiredProperties = [.. (merged.RequiredProperties ?? []).Concat(attr.RequiredProperties).Distinct()
 167        }
 5168    }
 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    {
 5177        if (!string.IsNullOrWhiteSpace(attr.XmlName))
 178        {
 2179            merged.XmlName = attr.XmlName;
 180        }
 181
 5182        if (!string.IsNullOrWhiteSpace(attr.XmlNamespace))
 183        {
 1184            merged.XmlNamespace = attr.XmlNamespace;
 185        }
 186
 5187        if (!string.IsNullOrWhiteSpace(attr.XmlPrefix))
 188        {
 1189            merged.XmlPrefix = attr.XmlPrefix;
 190        }
 191
 5192        merged.XmlAttribute |= attr.XmlAttribute;
 5193        merged.XmlWrapped |= attr.XmlWrapped;
 5194    }
 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.CreateShallowCopy();
 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    {
 6307        if (TryGetParameterItem(variableName, out var parameter))
 308        {
 3309            ValidateParameterHasSchemaOrContentForPowerShell(variableName, parameter);
 2310            ApplyPowerShellAttributeToParameter(variableName, parameter!, powershellAttribute);
 2311            return;
 312        }
 313
 3314        if (TryGetRequestBodyItem(variableName, out var requestBody))
 315        {
 2316            ApplyPowerShellAttributeToRequestBody(variableName, requestBody, powershellAttribute);
 1317            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    {
 3330        if (parameter is null || (parameter.Schema is null && parameter.Content is null))
 331        {
 1332            throw new InvalidOperationException(
 1333                $"Parameter '{variableName}' must have a schema or content defined before adding the powershell property
 334        }
 2335    }
 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    {
 2348        if (parameter.Content is not null)
 349        {
 0350            ApplyPowerShellAttributeToMediaTypeSchemas(
 0351                variableName,
 0352                parameter.Content.Values,
 0353                powershellAttribute,
 0354                subject: "parameter");
 0355            return;
 356        }
 357
 2358        var schema = (OpenApiSchema)parameter.Schema!;
 2359        ApplyPowerShellAttributesToSchema(schema, powershellAttribute);
 2360    }
 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    {
 2374        if (requestBody?.Content is null)
 375        {
 1376            throw new InvalidOperationException(
 1377                $"RequestBody '{variableName}' must have a content defined before adding the powershell property.");
 378        }
 379
 1380        ApplyPowerShellAttributeToMediaTypeSchemas(
 1381            variableName,
 1382            requestBody.Content.Values,
 1383            powershellAttribute,
 1384            subject: "request body");
 1385    }
 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    {
 4400        foreach (var mediaType in mediaTypes)
 401        {
 1402            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
 1409            ApplyPowerShellAttributesToSchema(schema, powershellAttribute);
 410        }
 1411    }
 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    {
 3420        ApplyItemConstraints(schema, powershellAttribute);
 3421        ApplyRangeConstraints(schema, powershellAttribute);
 3422        ApplyLengthConstraints(schema, powershellAttribute);
 3423        ApplyPatternConstraints(schema, powershellAttribute);
 3424        ApplyAllowedValuesConstraints(schema, powershellAttribute);
 3425        ApplyNullabilityConstraints(schema, powershellAttribute);
 3426    }
 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    {
 3435        if (powershellAttribute.MaxItems.HasValue)
 436        {
 1437            schema.MaxItems = powershellAttribute.MaxItems;
 438        }
 3439        if (powershellAttribute.MinItems.HasValue)
 440        {
 1441            schema.MinItems = powershellAttribute.MinItems;
 442        }
 3443    }
 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    {
 3452        if (!string.IsNullOrEmpty(powershellAttribute.MinRange))
 453        {
 2454            schema.Minimum = powershellAttribute.MinRange;
 455        }
 3456        if (!string.IsNullOrEmpty(powershellAttribute.MaxRange))
 457        {
 2458            schema.Maximum = powershellAttribute.MaxRange;
 459        }
 3460    }
 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    {
 3469        if (powershellAttribute.MinLength.HasValue)
 470        {
 2471            schema.MinLength = powershellAttribute.MinLength;
 472        }
 3473        if (powershellAttribute.MaxLength.HasValue)
 474        {
 1475            schema.MaxLength = powershellAttribute.MaxLength;
 476        }
 3477    }
 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    {
 3486        if (!string.IsNullOrEmpty(powershellAttribute.RegexPattern))
 487        {
 1488            schema.Pattern = powershellAttribute.RegexPattern;
 489        }
 3490    }
 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    {
 3499        if (powershellAttribute.AllowedValues is not null && powershellAttribute.AllowedValues.Count > 0)
 500        {
 1501            _ = PowerShellAttributes.ApplyValidateSetAttribute(powershellAttribute.AllowedValues, schema);
 502        }
 3503    }
 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    {
 3512        if (powershellAttribute.ValidateNotNullOrEmptyAttribute is not null)
 513        {
 0514            _ = PowerShellAttributes.ApplyNotNullOrEmpty(schema);
 515        }
 516
 3517        if (powershellAttribute.ValidateNotNullAttribute is not null)
 518        {
 1519            _ = PowerShellAttributes.ApplyNotNull(schema);
 520        }
 521
 3522        if (powershellAttribute.ValidateNotNullOrWhiteSpaceAttribute is not null)
 523        {
 1524            _ = PowerShellAttributes.ApplyNotNullOrWhiteSpace(schema);
 525        }
 3526    }
 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
 4540        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
 4549            Document.Components ??= new OpenApiComponents();
 4550            Document.Components.Parameters ??= new Dictionary<string, IOpenApiParameter>(StringComparer.Ordinal);
 4551            parameters = Document.Components.Parameters;
 552        }
 553        // Retrieve or create the parameter item
 4554        if (!parameters.TryGetValue(parameterName, out var parameterInterface) || parameterInterface is null)
 555        {
 556            // Create a new OpenApiParameter if it doesn't exist
 4557            parameterInterface = new OpenApiParameter();
 4558            parameters[parameterName] = parameterInterface;
 559        }
 560        // return the parameter item
 4561        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    {
 6573        if (TryGetInline(name: parameterName, kind: OpenApiComponentKind.Parameters, out parameter))
 574        {
 0575            isInline = true;
 0576            return true;
 577        }
 6578        else if (TryGetComponent(name: parameterName, kind: OpenApiComponentKind.Parameters, out parameter))
 579        {
 3580            isInline = false;
 3581            return true;
 582        }
 3583        parameter = null;
 3584        isInline = false;
 3585        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) =>
 6595    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    {
 1318        var op = new OpenApiOperation
 1319        {
 1320            OperationId = string.IsNullOrWhiteSpace(meta.OperationId) ? null : meta.OperationId,
 1321            Summary = string.IsNullOrWhiteSpace(meta.Summary) ? null : meta.Summary,
 1322            Description = string.IsNullOrWhiteSpace(meta.Description) ? null : meta.Description,
 1323            Deprecated = meta.Deprecated,
 1324            ExternalDocs = meta.ExternalDocs,
 1325            RequestBody = meta.RequestBody,
 1326            Responses = meta.Responses ?? new OpenApiResponses { ["200"] = new OpenApiResponse { Description = "Success"
 1327        };
 28
 1329        ApplyTags(op, meta);
 1330        ApplyServers(op, meta);
 1331        ApplyParameters(op, meta);
 1332        ApplyCallbacks(op, meta);
 1333        ApplySecurity(op, meta);
 1334        EnsureAutoClientErrorResponses(op, meta);
 1335        ApplyExtensions(op, meta);
 1336        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    {
 1346        operation.Responses ??= [];
 47
 1348        if (ResponseKeyExists(operation.Responses, "4XX") || ResponseKeyExists(operation.Responses, "default"))
 49        {
 250            return;
 51        }
 52
 1153        var statusesToAdd = GetAutoClientErrorStatuses(operation, meta);
 54
 1155        if (statusesToAdd.Count == 0)
 56        {
 557            return;
 58        }
 59
 660        var errorSchemaId = EnsureAutoErrorSchemaComponent();
 661        var errorContentTypes = GetAutoErrorResponseContentTypes();
 62
 663        AddMissingAutoClientErrorResponses(operation, statusesToAdd, errorSchemaId, errorContentTypes);
 664    }
 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    {
 1174        var statusesToAdd = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 1175        var hasParameters = meta.Parameters is { Count: > 0 };
 1176        var hasRequestBody = meta.RequestBody is not null;
 1177        var hasRequestContentTypeValidation = meta.MapOptions.AllowedRequestContentTypes.Count > 0;
 1178        var responses = operation.Responses;
 1179        var hasResponseNegotiation =
 1180            meta.MapOptions.DefaultResponseContentType is { Count: > 0 } ||
 2081            (responses is not null && responses.Values.Any(r => r.Content is { Count: > 0 }));
 82
 1183        if (hasParameters || hasRequestBody)
 84        {
 485            _ = statusesToAdd.Add("400");
 486            _ = statusesToAdd.Add("422");
 87        }
 88
 1189        if (hasRequestBody || hasRequestContentTypeValidation)
 90        {
 491            _ = statusesToAdd.Add("415");
 92        }
 93
 1194        if (hasResponseNegotiation)
 95        {
 496            _ = statusesToAdd.Add("406");
 97        }
 98
 1199        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    {
 6115        operation.Responses ??= [];
 6116        var responses = operation.Responses;
 117
 44118        foreach (var status in statusesToAdd)
 119        {
 16120            if (ResponseKeyExists(responses, status))
 121            {
 122                continue;
 123            }
 124
 15125            responses[status] = CreateAutoClientErrorResponse(status, errorSchemaId, errorContentTypes);
 126        }
 6127    }
 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)
 98135        => 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    {
 6143        Document.Components ??= new OpenApiComponents();
 6144        Document.Components.Schemas ??= new Dictionary<string, IOpenApiSchema>(StringComparer.Ordinal);
 145
 6146        var autoSchemaId = string.IsNullOrWhiteSpace(AutoErrorResponseSchemaId)
 6147            ? DefaultAutoErrorResponseSchemaId
 6148            : AutoErrorResponseSchemaId;
 149
 6150        if (!Document.Components.Schemas.ContainsKey(autoSchemaId))
 151        {
 6152            Document.Components.Schemas[autoSchemaId] = new OpenApiSchema
 6153            {
 6154                Type = JsonSchemaType.Object,
 6155                Required = new HashSet<string>(StringComparer.Ordinal) { "status", "error", "reason", "timestamp" },
 6156                Properties = new Dictionary<string, IOpenApiSchema>(StringComparer.Ordinal)
 6157                {
 6158                    ["status"] = new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int32" },
 6159                    ["error"] = new OpenApiSchema { Type = JsonSchemaType.String },
 6160                    ["reason"] = new OpenApiSchema { Type = JsonSchemaType.String },
 6161                    ["timestamp"] = new OpenApiSchema { Type = JsonSchemaType.String, Format = "date-time" },
 6162                    ["details"] = new OpenApiSchema { Type = JsonSchemaType.String },
 6163                    ["exception"] = new OpenApiSchema { Type = JsonSchemaType.String },
 6164                    ["stackTrace"] = new OpenApiSchema { Type = JsonSchemaType.String },
 6165                    ["path"] = new OpenApiSchema { Type = JsonSchemaType.String },
 6166                    ["method"] = new OpenApiSchema { Type = JsonSchemaType.String },
 6167                }
 6168            };
 169        }
 170
 6171        return autoSchemaId;
 172    }
 173
 174    private IReadOnlyList<string> GetAutoErrorResponseContentTypes()
 175    {
 6176        if (AutoErrorResponseContentTypes is null || AutoErrorResponseContentTypes.Length == 0)
 177        {
 0178            return [DefaultAutoErrorResponseContentType];
 179        }
 180
 6181        var contentTypes = AutoErrorResponseContentTypes
 7182            .Where(ct => !string.IsNullOrWhiteSpace(ct))
 6183            .Distinct(StringComparer.OrdinalIgnoreCase)
 6184            .ToArray();
 185
 6186        return contentTypes.Length == 0
 6187            ? [DefaultAutoErrorResponseContentType]
 6188            : contentTypes;
 189    }
 190
 191    private static OpenApiResponse CreateAutoClientErrorResponse(string statusCode, string errorSchemaId, IReadOnlyList<
 192    {
 15193        var description = statusCode switch
 15194        {
 4195            "400" => "Bad Request",
 4196            "406" => "Not Acceptable",
 3197            "415" => "Unsupported Media Type",
 4198            "422" => "Unprocessable Entity",
 0199            _ => "Client Error"
 15200        };
 201
 15202        var content = new Dictionary<string, IOpenApiMediaType>(StringComparer.Ordinal);
 66203        foreach (var contentType in contentTypes)
 204        {
 18205            content[contentType] = new OpenApiMediaType
 18206            {
 18207                Schema = new OpenApiSchemaReference(errorSchemaId)
 18208            };
 209        }
 210
 15211        return new OpenApiResponse
 15212        {
 15213            Description = description,
 15214            Content = content
 15215        };
 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    {
 13224        if (meta.Extensions is null || meta.Extensions.Count == 0)
 225        {
 13226            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    {
 13238        if (meta.Tags.Count > 0)
 239        {
 3240            op.Tags = new HashSet<OpenApiTagReference>();
 14241            foreach (var t in meta.Tags ?? [])
 242            {
 4243                _ = op.Tags.Add(new OpenApiTagReference(t));
 244            }
 245        }
 13246    }
 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        {
 13257            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            }
 13263        }
 0264        catch (Exception ex)
 265        {
 0266            Host.Logger.Warning(ex, "Failed to set operation-level servers for OpenAPI operation {OperationId}", op.Oper
 0267        }
 13268    }
 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        {
 13279            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            }
 13285        }
 0286        catch { Host.Logger?.Warning("Failed to set operation-level parameters for OpenAPI operation {OperationId}", op.
 13287    }
 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    {
 13296        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        }
 13326        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        }
 13331    }
 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            : example.CreateShallowCopy();
 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    {
 137        var key = requestBodyDescriptor.Key ?? variable.Name;
 138        var requestBody = GetOrCreateRequestBodyItem(key, requestBodyDescriptor.Inline);
 39
 140        ApplyRequestBodyCommonFields(requestBody, requestBodyDescriptor);
 41
 142        TryApplyVariableTypeSchema(requestBody, variable, requestBodyDescriptor);
 143    }
 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    {
 154        requestBody.Description = requestBodyAnnotation.Description;
 55
 156        requestBody.Required = requestBodyAnnotation.Required;
 157    }
 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    {
 170        if (variable.VariableType is null)
 71        {
 072            return;
 73        }
 174        var iSchema = InferPrimitiveSchema(variable.VariableType);
 175        if (iSchema is OpenApiSchema schema)
 76        {
 77            //PowerShellAttributes.ApplyPowerShellAttributes(variable.PropertyInfo, schema);
 78            // Apply any schema attributes from the request body annotation
 179            ApplyConcreteSchemaAttributes(requestBodyAnnotation, schema);
 80            // Try to set default value from the variable initial value if not already set
 181            if (!variable.NoDefault)
 82            {
 183                schema.Default = OpenApiJsonNodeFactory.ToNode(variable.InitialValue);
 84            }
 85        }
 186        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
 193        requestBody.Content ??= new Dictionary<string, IOpenApiMediaType>(StringComparer.Ordinal);
 694        foreach (var ct in requestBodyAnnotation.ContentType)
 95        {
 296            requestBody.Content[ct] = new OpenApiMediaType { Schema = iSchema };
 97        }
 198    }
 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
 1129        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
 1138            Document.Components ??= new OpenApiComponents();
 1139            Document.Components.RequestBodies ??= new Dictionary<string, IOpenApiRequestBody>(StringComparer.Ordinal);
 1140            requestBodies = Document.Components.RequestBodies;
 141        }
 142        // Retrieve or create the request body item
 1143        if (!requestBodies.TryGetValue(requestBodyName, out var requestBodyInterface) || requestBodyInterface is null)
 144        {
 145            // Create a new OpenApiRequestBody if it doesn't exist
 1146            requestBodyInterface = new OpenApiRequestBody();
 1147            requestBodies[requestBodyName] = requestBodyInterface;
 148        }
 149        // return the request body item
 1150        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    {
 8162        if (TryGetInline(name: requestBodyName, kind: OpenApiComponentKind.RequestBodies, out requestBody))
 163        {
 0164            isInline = true;
 0165            return true;
 166        }
 8167        else if (TryGetComponent(name: requestBodyName, kind: OpenApiComponentKind.RequestBodies, out requestBody))
 168        {
 6169            isInline = false;
 6170            return true;
 171        }
 2172        requestBody = null;
 2173        isInline = false;
 2174        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) =>
 4184    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    {
 117        var t = attr.GetType();
 118        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    {
 230        ArgumentNullException.ThrowIfNull(attr);
 231        ArgumentNullException.ThrowIfNull(response);
 32
 233        return attr switch
 234        {
 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),
 141            _ => false
 242        };
 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    {
 2220        if (Document.Components?.Schemas is { } schemas &&
 2221            schemas.TryGetValue(refId, out var schema))
 222        {
 223            // your existing clone semantics
 1224            return (OpenApiSchema)schema.CreateShallowCopy();
 225        }
 226
 1227        throw new InvalidOperationException(
 1228            $"Schema reference '{refId}' cannot be embedded because it was not found in components.");
 229    }
 230
 231    private void ProcessResponseExampleRef(string name, OpenApiResponseExampleRefAttribute attribute)
 232    {
 2233        if (attribute.StatusCode != "default")
 234        {
 1235            throw new InvalidOperationException("Response example references cannot have a status code.");
 236        }
 1237        if (attribute.Key is null)
 238        {
 1239            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    {
 1250        if (attribute.StatusCode != "default")
 251        {
 1252            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    {
 2267        if (attribute.StatusCode != "default")
 268        {
 1269            throw new InvalidOperationException("Response header references cannot have a status code.");
 270        }
 1271        if (attribute.Key is null)
 272        {
 0273            throw new InvalidOperationException("Response header attributes must have a Key specified to define the head
 274        }
 1275        if (!TryGetResponseItem(name, out var response))
 276        {
 1277            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    {
 2286        var response = GetOrCreateResponseItem(variable.Name, responseDescriptor.Inline);
 287
 2288        ApplyResponseCommonFields(response, responseDescriptor);
 289
 2290        TryApplyVariableTypeSchema(response, variable, responseDescriptor);
 2291    }
 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
 2299        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
 2308            Document.Components ??= new OpenApiComponents();
 2309            Document.Components.Responses ??= new Dictionary<string, IOpenApiResponse>(StringComparer.Ordinal);
 2310            responses = Document.Components.Responses;
 311        }
 312        // Retrieve or create the response item
 2313        if (!responses.TryGetValue(responseName, out var responseInterface) || responseInterface is null)
 314        {
 315            // Create a new OpenApiResponse if it doesn't exist
 2316            responseInterface = new OpenApiResponse();
 2317            responses[responseName] = responseInterface;
 318        }
 319        // return the response item
 2320        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
 2333        if (TryGetInline(name: responseName, kind: OpenApiComponentKind.Responses, out response))
 334        {
 0335            isInline = true;
 0336            return true;
 337        }
 338        // Next, check document components
 2339        else if (TryGetComponent(name: responseName, kind: OpenApiComponentKind.Responses, out response))
 340        {
 1341            isInline = false;
 1342            return true;
 343        }
 1344        response = null;
 1345        isInline = false;
 1346        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) =>
 1355    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    {
 7949        built ??= [];
 50
 7951        if (TryBuildPrimitiveSchema(t, out var primitiveSchema))
 52        {
 1553            ApplyTypeAttributes(t, primitiveSchema);
 1554            return primitiveSchema;
 55        }
 56
 6457        var formSchemaParent = TryBuildFormPayloadSchemaParent(t, built);
 58
 6459        if (TryBuildDerivedSchemaFromBaseType(t, built, out var derivedSchema, out var schemaParent))
 60        {
 061            return derivedSchema;
 62        }
 63
 6464        var schema = CreateSchemaForDeclaredProperties(t);
 65
 6466        if (built.Contains(t))
 67        {
 168            return schema;
 69        }
 70
 6371        _ = built.Add(t);
 72
 6373        ApplyTypeAttributes(t, schema);
 74
 6375        if (t.IsEnum)
 76        {
 977            return RegisterEnumSchema(t);
 78        }
 79        // Extensions
 5480        ProcessExtensions(t, schema);
 81        // Properties
 5482        ProcessTypeProperties(t, schema, built);
 83        // Return composed schema if applicable
 5484        var parentToCompose = schemaParent ?? formSchemaParent;
 5485        return ComposeWithParentSchema(parentToCompose, schema);
 86    }
 87
 88    private OpenApiSchema? TryBuildFormPayloadSchemaParent(Type t, HashSet<Type> built)
 89    {
 6490        var bindAttr = t.GetCustomAttributes(inherit: true)
 19191            .FirstOrDefault(a => a.GetType().Name.Equals("KrBindFormAttribute", StringComparison.OrdinalIgnoreCase));
 92
 6493        if (bindAttr is null)
 94        {
 5995            return null;
 96        }
 97
 598        var depthProp = bindAttr.GetType().GetProperty("MaxNestingDepth", BindingFlags.Public | BindingFlags.Instance);
 599        var maxDepth = depthProp?.GetValue(bindAttr) as int? ?? 0;
 5100        var baseType = maxDepth > 0 ? typeof(KrMultipart) : typeof(KrFormData);
 101
 5102        BuildSchema(baseType, built);
 103
 5104        return new OpenApiSchema
 5105        {
 5106            AllOf = [new OpenApiSchemaReference(baseType.Name)]
 5107        };
 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    {
 108117        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        }
 54128    }
 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    {
 79138        if (PrimitiveSchemaMap.TryGetValue(t, out var getSchema))
 139        {
 15140            schema = getSchema();
 15141            return true;
 142        }
 143
 64144        schema = null!;
 64145        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    {
 64163        resolved = null!;
 64164        schemaParent = null;
 165
 64166        if (!HasComposableBaseType(t))
 167        {
 54168            return false;
 169        }
 170
 10171        var baseSchema = BuildBaseTypeSchema(t);
 10172        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).
 10179        if (t.BaseType is not null && t.BaseType != typeof(object)
 10180            && (typeof(KrFormData).IsAssignableFrom(t) || typeof(KrMultipart).IsAssignableFrom(t)))
 181        {
 0182            BuildSchema(t.BaseType, built);
 183        }
 184
 10185        if (TryResolveSimpleOrReferenceBaseSchema(t, baseSchema, out resolved))
 186        {
 0187            return true;
 188        }
 189
 10190        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.
 10196        schemaParent = baseSchema as OpenApiSchema;
 10197        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)
 64206        => 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    {
 10220        resolved = null!;
 221
 10222        if (!IsSimpleSchemaOrReference(baseSchema))
 223        {
 10224            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    {
 10246        return schema is OpenApiSchemaReference
 10247            || (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    {
 10265        resolved = null!;
 266
 10267        if (arraySchema.Type != JsonSchemaType.Array || arraySchema.Items is null)
 268        {
 10269            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    {
 64321        var schema = new OpenApiSchema();
 64322        var declaredPropsCount =
 64323            t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
 275324             .Count(p => p.DeclaringType == t);
 325
 64326        if (declaredPropsCount > 0)
 327        {
 55328            schema.Type = JsonSchemaType.Object;
 55329            schema.Properties = new Dictionary<string, IOpenApiSchema>(StringComparer.Ordinal);
 330        }
 331
 64332        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    {
 54343        if (schemaParent is null)
 344        {
 48345            return schema;
 346        }
 347
 6348        if (schemaParent.AllOf is not null)
 349        {
 6350            schemaParent.AllOf.Add(schema);
 6351            schemaParent.Type = null; // Clear type when using allOf
 6352            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    {
 10375        if (PrimitiveSchemaMap.TryGetValue(t.BaseType!, out var value))
 376        {
 0377            return value();
 378        }
 379
 380        // Fallback to custom base type schema building
 10381        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    {
 10391        var attributes = t.CustomAttributes.ToArray();
 392        // Count declared properties
 10393        var declaredPropsCount =
 10394            t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
 12395             .Count(p => p.DeclaringType == t);
 396
 397        // Check for the special case where the derived type only adds array semantics
 10398        var hasArray =
 10399            attributes.Length > 0 &&
 10400            attributes[0].NamedArguments.Any(na =>
 10401                na.MemberName == "Array" &&
 10402                na.TypedValue.ArgumentType == typeof(bool) &&
 10403                na.TypedValue.Value is bool b && b);
 404        // If so, we can represent this as a simple reference to the base type
 10405        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
 10419        var schema = new OpenApiSchema
 10420        {
 10421            AllOf = [new OpenApiSchemaReference(t.BaseType!.Name)]
 10422        };
 423        // Apply array semantics if specified
 10424        return hasArray
 10425            ? new OpenApiSchema
 10426            {
 10427                Type = JsonSchemaType.Array,
 10428                Items = schema
 10429            }
 10430            : 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    {
 11439        var enumSchema = new OpenApiSchema
 11440        {
 11441            Type = JsonSchemaType.String,
 153442            Enum = [.. enumType.GetEnumNames().Select(n => (JsonNode)n)]
 11443        };
 11444        if (Document.Components?.Schemas is not null)
 445        {
 11446            Document.Components.Schemas[enumType.Name] = enumSchema;
 447        }
 11448        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    {
 78458        ApplySchemaComponentAttributes(t, schema);
 78459        ApplyGeneratedRequiredPropertiesMetadata(t, schema);
 78460        ApplyPatternProperties(t, schema);
 78461    }
 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    {
 78470        var requiredProp = t.GetProperty("RequiredProperties", BindingFlags.Public | BindingFlags.NonPublic | BindingFla
 78471        if (requiredProp?.GetValue(null) is not System.Collections.IEnumerable requiredValues)
 472        {
 78473            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    {
 78503        var schemaAttribute = t.GetCustomAttributes(true)
 78504            .OfType<OpenApiSchemaComponent>()
 78505            .FirstOrDefault();
 506
 78507        if (schemaAttribute is null)
 508        {
 45509            return;
 510        }
 511
 33512        ApplySchemaAttr(schemaAttribute, schema);
 33513        ApplySchemaComponentExamples(schemaAttribute, schema);
 33514    }
 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    {
 33523        if (schemaAttribute.Examples is null)
 524        {
 33525            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    {
 166543        foreach (var pattern in t.GetCustomAttributes(true)
 78544                     .OfType<OpenApiPatternPropertiesAttribute>())
 545        {
 5546            var patternSchema = BuildPatternSchema(pattern);
 5547            if (patternSchema is null || string.IsNullOrWhiteSpace(pattern.KeyPattern))
 548            {
 549                continue;
 550            }
 551
 5552            schema.PatternProperties ??= new Dictionary<string, IOpenApiSchema>(StringComparer.Ordinal);
 5553            schema.PatternProperties[pattern.KeyPattern] = patternSchema;
 554        }
 78555    }
 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    {
 5564        if (pattern.SchemaType is null)
 565        {
 0566            return null;
 567        }
 568
 5569        var schemaType = pattern.SchemaType;
 5570        HashSet<Type>? built = null;
 5571        if (!schemaType.IsArray)
 572        {
 5573            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    {
 54592        var instance = TryCreateTypeInstance(t);
 54593        var isFormModel = t.GetCustomAttributes(inherit: true)
 179594            .Any(a => a.GetType().Name.Equals("KrBindFormAttribute", StringComparison.OrdinalIgnoreCase));
 595
 524596        foreach (var prop in t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
 263597                      .Where(p => p.DeclaringType == t))
 598        {
 208599            if (ShouldSkipRuntimeFormPayloadStorageProperty(t, prop))
 600            {
 601                continue;
 602            }
 603
 203604            var propSchema = BuildPropertySchema(prop, built);
 203605            CapturePropertyDefault(instance, prop, propSchema);
 606
 203607            if (isFormModel && prop.GetCustomAttribute<KrPartAttribute>(inherit: false) is { Required: true })
 608            {
 10609                schema.Required ??= new HashSet<string>(StringComparer.Ordinal);
 10610                _ = schema.Required.Add(prop.Name);
 611            }
 203612            schema.Properties?.Add(prop.Name, propSchema);
 613        }
 54614    }
 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    {
 208629        return declaringType == typeof(KrFormData)
 208630            ? prop.Name is nameof(KrFormData.Fields) or nameof(KrFormData.Files)
 208631            : 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        {
 54641            return Activator.CreateInstance(t);
 642        }
 5643        catch
 644        {
 5645            return null;
 646        }
 54647    }
 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    {
 203654        if (instance is null || propSchema is not OpenApiSchema concrete || concrete.Default is not null)
 655        {
 47656            return;
 657        }
 658
 659        try
 660        {
 156661            var value = prop.GetValue(instance);
 156662            if (!IsIntrinsicDefault(value, prop.PropertyType))
 663            {
 61664                concrete.Default = OpenApiJsonNodeFactory.ToNode(value);
 665            }
 156666        }
 0667        catch
 668        {
 669            // Ignore failures when capturing defaults
 0670        }
 156671    }
 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    {
 169681        if (value is null)
 682        {
 34683            return true;
 684        }
 685
 686        // Unwrap Nullable<T>
 135687        var t = Nullable.GetUnderlyingType(declaredType) ?? declaredType;
 688
 689        // Reference types: null is the only intrinsic default
 135690        if (!t.IsValueType)
 691        {
 57692            return false;
 693        }
 694
 695        // Special-cases for common structs
 78696        if (t == typeof(Guid))
 697        {
 6698            return value.Equals(Guid.Empty);
 699        }
 700
 72701        if (t == typeof(TimeSpan))
 702        {
 1703            return value.Equals(TimeSpan.Zero);
 704        }
 705
 71706        if (t == typeof(DateTime))
 707        {
 6708            return value.Equals(default(DateTime));
 709        }
 710
 65711        if (t == typeof(DateTimeOffset))
 712        {
 1713            return value.Equals(default(DateTimeOffset));
 714        }
 715
 716        // Enums: 0 is intrinsic default
 64717        if (t.IsEnum)
 718        {
 2719            return Convert.ToInt64(value) == 0;
 720        }
 721
 722        // Primitive/value types: compare to default(T)
 62723        var def = Activator.CreateInstance(t);
 62724        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    {
 28735        if (isNullable)
 736        {
 2737            schema.Type |= JsonSchemaType.Null;
 738        }
 28739        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    {
 28750        var nullable = false;
 28751        if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)
 28752            && type.GetGenericArguments().Length == 1)
 753        {
 1754            type = type.GetGenericArguments()[0];
 1755            nullable = true;
 756        }
 757        // Direct type mappings
 28758        if (PrimitiveSchemaMap.TryGetValue(type, out var schemaFactory))
 759        {
 26760            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).CreateShallowCopy() : 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.CreateShallowCopy();
 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    {
 71833        [typeof(string)] = () => new OpenApiSchema { Type = JsonSchemaType.String },
 31834        [typeof(bool)] = () => new OpenApiSchema { Type = JsonSchemaType.Boolean },
 0835        [typeof(long)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int64" },
 6836        [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" },
 4839        [typeof(byte[])] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "binary" },
 1840        [typeof(Uri)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "uri" },
 9841        [typeof(Guid)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "uuid" },
 10842        [typeof(object)] = () => new OpenApiSchema { Type = JsonSchemaType.Object },
 0843        [typeof(void)] = () => new OpenApiSchema { Type = JsonSchemaType.Null },
 4844        [typeof(char)] = () => new OpenApiSchema { Type = JsonSchemaType.String, MaxLength = 1, MinLength = 1 },
 4845        [typeof(sbyte)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int32" },
 4846        [typeof(byte)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int32" },
 4847        [typeof(short)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int32" },
 4848        [typeof(ushort)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int32" },
 27849        [typeof(int)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int32" },
 4850        [typeof(uint)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int32" },
 12851        [typeof(long)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int64" },
 4852        [typeof(ulong)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int64" },
 4853        [typeof(float)] = () => new OpenApiSchema { Type = JsonSchemaType.Number, Format = "float" },
 6854        [typeof(double)] = () => new OpenApiSchema { Type = JsonSchemaType.Number, Format = "double" },
 5855        [typeof(decimal)] = () => new OpenApiSchema { Type = JsonSchemaType.Number, Format = "decimal" },
 5856        [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
 4874        [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
 4878        [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
 4882        [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    {
 276892        if (oaProperties is null)
 893        {
 205894            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).
 71899        if (ioaSchema is OpenApiSchema concreteSchema)
 900        {
 61901            ApplyConcreteSchemaAttributes(oaProperties, concreteSchema);
 61902            return;
 903        }
 904
 10905        if (ioaSchema is OpenApiSchemaReference refSchema)
 906        {
 10907            ApplyReferenceSchemaAttributes(oaProperties, refSchema);
 908        }
 10909    }
 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    {
 66918        ApplyTitleAndDescription(properties, schema);
 66919        ApplySchemaType(properties, schema);
 66920        ApplyFormatAndNumericBounds(properties, schema);
 66921        ApplyLengthAndPattern(properties, schema);
 66922        ApplyCollectionConstraints(properties, schema);
 66923        ApplyFlags(properties, schema);
 66924        ApplyExamplesAndDefaults(properties, schema);
 66925        ApplyXmlMetadata(properties, schema);
 66926        if (schema.Type == null && (schema.AdditionalProperties is not null || schema.AdditionalPropertiesAllowed || sch
 927        {
 0928            schema.Type = JsonSchemaType.Object;
 929        }
 66930    }
 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    {
 66939        if (properties.Title is not null)
 940        {
 0941            schema.Title = properties.Title;
 942        }
 66943        if (properties is not OpenApiParameterComponentAttribute && properties.Description is not null)
 944        {
 46945            schema.Description = properties.Description;
 946        }
 66947    }
 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    {
 66956        if (properties.Type != OaSchemaType.None)
 957        {
 5958            schema.Type = properties.Type switch
 5959            {
 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,
 5965                OaSchemaType.Object => JsonSchemaType.Object,
 0966                OaSchemaType.Null => JsonSchemaType.Null,
 0967                _ => schema.Type
 5968            };
 969        }
 970
 66971        if (properties.Nullable)
 972        {
 0973            schema.Type |= JsonSchemaType.Null;
 974        }
 66975    }
 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    {
 66984        if (!string.IsNullOrWhiteSpace(properties.Format))
 985        {
 0986            schema.Format = properties.Format;
 987        }
 988
 66989        if (properties.MultipleOf.HasValue)
 990        {
 0991            schema.MultipleOf = properties.MultipleOf;
 992        }
 993
 66994        if (!string.IsNullOrWhiteSpace(properties.Maximum))
 995        {
 0996            schema.Maximum = properties.Maximum;
 0997            if (properties.ExclusiveMaximum)
 998            {
 0999                schema.ExclusiveMaximum = properties.Maximum;
 1000            }
 1001        }
 1002
 661003        if (!string.IsNullOrWhiteSpace(properties.Minimum))
 1004        {
 01005            schema.Minimum = properties.Minimum;
 01006            if (properties.ExclusiveMinimum)
 1007            {
 01008                schema.ExclusiveMinimum = properties.Minimum;
 1009            }
 1010        }
 661011    }
 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    {
 661020        if (properties.MaxLength >= 0)
 1021        {
 01022            schema.MaxLength = properties.MaxLength;
 1023        }
 1024
 661025        if (properties.MinLength >= 0)
 1026        {
 01027            schema.MinLength = properties.MinLength;
 1028        }
 1029
 661030        if (!string.IsNullOrWhiteSpace(properties.Pattern))
 1031        {
 01032            schema.Pattern = properties.Pattern;
 1033        }
 661034    }
 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    {
 661043        if (properties.MaxItems >= 0)
 1044        {
 01045            schema.MaxItems = properties.MaxItems;
 1046        }
 1047
 661048        if (properties.MinItems >= 0)
 1049        {
 01050            schema.MinItems = properties.MinItems;
 1051        }
 1052
 661053        if (properties.UniqueItems)
 1054        {
 01055            schema.UniqueItems = true;
 1056        }
 1057
 661058        if (properties.MaxProperties >= 0)
 1059        {
 01060            schema.MaxProperties = properties.MaxProperties;
 1061        }
 1062
 661063        if (properties.MinProperties >= 0)
 1064        {
 01065            schema.MinProperties = properties.MinProperties;
 1066        }
 661067    }
 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    {
 661076        schema.ReadOnly = properties.ReadOnly;
 661077        schema.WriteOnly = properties.WriteOnly;
 661078        if (IsObjectSchemaType(schema.Type))
 1079        {
 331080            schema.AdditionalPropertiesAllowed = properties.AdditionalPropertiesAllowed;
 331081            ApplyAdditionalProperties(properties, schema);
 1082        }
 1083        else
 1084        {
 331085            schema.AdditionalPropertiesAllowed = true; // Non-object schemas must allow additional properties to be vali
 1086        }
 661087        schema.UnevaluatedProperties = properties.UnevaluatedProperties;
 661088        if (properties is not OpenApiParameterComponentAttribute)
 1089        {
 641090            schema.Deprecated = properties.Deprecated;
 1091        }
 661092    }
 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    {
 331101        if (!properties.AdditionalPropertiesAllowed || properties.AdditionalProperties is null)
 1102        {
 281103            return;
 1104        }
 1105
 51106        HashSet<Type>? built = null;
 51107        if (properties.AdditionalProperties.IsArray)
 1108        {
 01109            ApplyArrayAdditionalProperties(properties, schema, built);
 01110            return;
 1111        }
 1112
 51113        schema.AdditionalProperties = BuildSchemaForType(properties.AdditionalProperties, built);
 51114        EnsureAdditionalPropertiesAllowed(schema.AdditionalProperties, schema);
 51115    }
 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    {
 51143        if (additionalSchema is OpenApiSchema apiSchema
 51144            && !IsObjectSchemaType(apiSchema.Type)
 51145            && !apiSchema.AdditionalPropertiesAllowed
 51146            && targetSchema.AdditionalProperties is OpenApiSchema targetAdditional)
 1147        {
 01148            targetAdditional.AdditionalPropertiesAllowed = true;
 1149        }
 51150    }
 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)
 711158        => 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    {
 661167        if (properties.Default is not null)
 1168        {
 01169            schema.Default = OpenApiJsonNodeFactory.ToNode(properties.Default);
 1170        }
 661171        if (properties.Example is not null && properties is not OpenApiParameterComponentAttribute)
 1172        {
 01173            schema.Example = OpenApiJsonNodeFactory.ToNode(properties.Example);
 1174        }
 1175
 661176        if (properties.Enum is { Length: > 0 })
 1177        {
 01178            schema.Enum = [.. properties.Enum.Select(OpenApiJsonNodeFactory.ToNode).OfType<JsonNode>()];
 1179        }
 1180
 661181        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        }
 661189    }
 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
 661199        var hasXmlMetadata = !string.IsNullOrWhiteSpace(properties.XmlName) ||
 661200                             !string.IsNullOrWhiteSpace(properties.XmlNamespace) ||
 661201                             !string.IsNullOrWhiteSpace(properties.XmlPrefix) ||
 661202                             properties.XmlAttribute ||
 661203                             properties.XmlWrapped;
 1204
 661205        if (!hasXmlMetadata)
 1206        {
 581207            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)
 101249        if (!string.IsNullOrWhiteSpace(properties.Description))
 1250        {
 101251            reference.Description = properties.Description;
 1252        }
 1253
 101254        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.
 101261    }
 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    {
 3118        var securityScheme = options switch
 3119        {
 620            ApiKeyAuthenticationOptions apiKeyOptions => GetSecurityScheme(apiKeyOptions),
 821            BasicAuthenticationOptions basicOptions => GetSecurityScheme(basicOptions),
 222            CookieAuthOptions cookieOptions => GetSecurityScheme(cookieOptions),
 323            JwtAuthOptions jwtOptions => GetSecurityScheme(jwtOptions),
 724            OAuth2Options oauth2Options => GetSecurityScheme(oauth2Options),
 125            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}
 3129        };
 3130        AddSecurityComponent(scheme: scheme, globalScheme: options.GlobalScheme, securityScheme: securityScheme);
 3131    }
 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
 173        var discoveryUrl = options.MetadataAddress
 174                           ?? (options.Authority is null
 175                               ? throw new InvalidOperationException(
 176                                   "Either Authority or MetadataAddress must be set to build OIDC OpenAPI scheme.")
 177                               : $"{options.Authority.TrimEnd('/')}/.well-known/openid-configuration");
 78
 179        return new OpenApiSecurityScheme
 180        {
 181            Type = SecuritySchemeType.OpenIdConnect,
 182            OpenIdConnectUrl = new Uri(discoveryUrl, UriKind.Absolute),
 183            // Description comes from AuthenticationSchemeOptions base class
 184            Description = options.Description,
 185            Deprecated = options.Deprecated
 186        };
 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
 797        var flows = new OpenApiOAuthFlows
 798        {
 799            // Client Credentials flow
 7100            AuthorizationCode = new OpenApiOAuthFlow
 7101            {
 7102                AuthorizationUrl = new Uri(options.AuthorizationEndpoint, UriKind.Absolute),
 7103            }
 7104        };
 105        // Scopes
 7106        if (options.ClaimPolicy is not null && options.ClaimPolicy.Policies is not null && options.ClaimPolicy.Policies.
 107        {
 1108            var scopes = new Dictionary<string, string>();
 1109            var policies = options.ClaimPolicy.Policies;
 6110            foreach (var item in policies)
 111            {
 2112                scopes.Add(item.Key, item.Value.Description ?? string.Empty);
 113            }
 1114            flows.AuthorizationCode.Scopes = scopes;
 115        }
 116        // Token endpoint
 7117        if (options.TokenEndpoint is not null)
 118        {
 7119            flows.AuthorizationCode.TokenUrl = new Uri(options.TokenEndpoint, UriKind.Absolute);
 120        }
 121
 7122        return new OpenApiSecurityScheme()
 7123        {
 7124            Type = SecuritySchemeType.OAuth2,
 7125            Flows = flows,
 7126            OAuth2MetadataUrl = GetOptionalAbsoluteUri(
 7127                options.OAuth2MetadataUrl,
 7128                nameof(options.OAuth2MetadataUrl)),
 7129            Description = options.Description,
 7130            Deprecated = options.Deprecated
 7131        };
 132    }
 133
 134    /// <summary>
 135    /// Parses an optional URI value and ensures that it is absolute when provided.
 136    /// </summary>
 137    /// <param name="value">The candidate URI string.</param>
 138    /// <param name="optionName">The option name used for error reporting.</param>
 139    /// <returns>An absolute <see cref="Uri"/> when valid; otherwise <see langword="null"/> for empty values.</returns>
 140    /// <exception cref="ArgumentException">Thrown when the value is non-empty but not a valid absolute URI.</exception>
 141    private static Uri? GetOptionalAbsoluteUri(string? value, string optionName)
 142    {
 7143        return string.IsNullOrWhiteSpace(value)
 7144            ? null
 7145            : !Uri.TryCreate(value, UriKind.Absolute, out var uri)
 7146            ? throw new ArgumentException(
 7147                $"Invalid absolute URI for '{optionName}': '{value}'.",
 7148                optionName)
 7149            : uri;
 150    }
 151
 152    /// <summary>
 153    /// Gets the OpenAPI security scheme for API key authentication.
 154    /// </summary>
 155    /// <param name="options">The API key authentication options.</param>
 156    private static OpenApiSecurityScheme GetSecurityScheme(ApiKeyAuthenticationOptions options)
 157    {
 6158        return new OpenApiSecurityScheme()
 6159        {
 6160            Type = SecuritySchemeType.ApiKey,
 6161            Name = options.ApiKeyName,
 6162            In = options.In,
 6163            Description = options.Description,
 6164            Deprecated = options.Deprecated
 6165        };
 166    }
 167
 168    /// <summary>
 169    /// Gets the OpenAPI security scheme for cookie authentication.
 170    /// </summary>
 171    /// <param name="options">The cookie authentication options.</param>
 172    /// <returns></returns>
 173    private static OpenApiSecurityScheme GetSecurityScheme(CookieAuthOptions options)
 174    {
 2175        return new OpenApiSecurityScheme()
 2176        {
 2177            Type = SecuritySchemeType.ApiKey,
 2178            Name = options.Cookie.Name,
 2179            In = ParameterLocation.Cookie,
 2180            Description = options.Description,
 2181            Deprecated = options.Deprecated
 2182        };
 183    }
 184
 185    /// <summary>
 186    /// Gets the OpenAPI security scheme for JWT authentication.
 187    /// </summary>
 188    /// <param name="options">The JWT authentication options.</param>
 189    /// <returns></returns>
 190    private static OpenApiSecurityScheme GetSecurityScheme(JwtAuthOptions options)
 191    {
 3192        return new OpenApiSecurityScheme()
 3193        {
 3194            Type = SecuritySchemeType.Http,
 3195            Scheme = "bearer",
 3196            BearerFormat = "JWT",
 3197            Description = options.Description,
 3198            Deprecated = options.Deprecated
 3199        };
 200    }
 201
 202    /// <summary>
 203    ///  Gets the OpenAPI security scheme for basic authentication.
 204    /// </summary>
 205    /// <param name="options">The basic authentication options.</param>
 206    private static OpenApiSecurityScheme GetSecurityScheme(BasicAuthenticationOptions options)
 207    {
 8208        return new OpenApiSecurityScheme()
 8209        {
 8210            Type = SecuritySchemeType.Http,
 8211            Scheme = "basic",
 8212            Description = options.Description,
 8213            Deprecated = options.Deprecated
 8214        };
 215    }
 216
 217    /// <summary>
 218    /// Adds a security component to the OpenAPI document.
 219    /// </summary>
 220    /// <param name="scheme">The name of the security component.</param>
 221    /// <param name="globalScheme">Indicates whether the security scheme should be applied globally.</param>
 222    /// <param name="securityScheme">The security scheme to add.</param>
 223    private void AddSecurityComponent(string scheme, bool globalScheme, OpenApiSecurityScheme securityScheme)
 224    {
 31225        _ = Document.AddComponent(scheme, securityScheme);
 226
 227        // Reference it by NAME in the requirement (no .Reference in v2)
 31228        var requirement = new OpenApiSecurityRequirement
 31229        {
 31230            {
 31231                new OpenApiSecuritySchemeReference(scheme,Document), new List<string>()
 31232            }
 31233        };
 31234        SecurityRequirement.Add(scheme, requirement);
 235
 236        // Apply globally if specified
 31237        if (globalScheme)
 238        {
 239            // Apply globally
 1240            Document.Security ??= [];
 1241            Document.Security.Add(requirement);
 242        }
 31243    }
 244}

/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    {
 618        if (Metadata is null || Metadata.Count == 0)
 19        {
 520            return;
 21        }
 122        Document.Webhooks = new Dictionary<string, IOpenApiPathItem>();
 23
 124        var groups = Metadata
 325            .GroupBy(kvp => kvp.Key.Pattern, StringComparer.Ordinal)
 326            .Where(g => !string.IsNullOrWhiteSpace(g.Key));
 27
 428        foreach (var grp in groups)
 29        {
 130            ProcessWebhookGroup(grp);
 31        }
 132    }
 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    {
 140        var pattern = grp.Key;
 141        var webhookPathItem = GetOrCreateWebhookItem(pattern);
 42
 643        foreach (var kvp in grp)
 44        {
 245            if (kvp.Value.DocumentId is not null && !kvp.Value.DocumentId.Contains(DocumentId))
 46            {
 47                continue;
 48            }
 149            ProcessWebhookOperation(kvp, webhookPathItem);
 50        }
 151    }
 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    {
 160        var method = kvp.Key.Method;
 161        var openapiMetadata = kvp.Value;
 62
 163        var op = BuildOperationFromMetadata(openapiMetadata);
 164        webhookPathItem.AddOperation(HttpMethod.Parse(method.ToMethodString()), op);
 165    }
 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    {
 174        Document.Webhooks ??= new Dictionary<string, IOpenApiPathItem>(StringComparer.Ordinal);
 175        if (!Document.Webhooks.TryGetValue(pattern, out var pathInterface) || pathInterface is null)
 76        {
 177            pathInterface = new OpenApiPathItem();
 178            Document.Webhooks[pattern] = pathInterface;
 79        }
 180        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>
 46841    public KestrunHost Host { get; init; }
 42
 43    /// <summary>
 44    /// The ID of the OpenAPI document being generated.
 45    /// </summary>
 19246    public string DocumentId { get; init; }
 47
 48    /// <summary>
 49    /// The OpenAPI document being generated.
 50    /// </summary>
 97751    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>
 20156    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>
 20861    public string[] AutoErrorResponseContentTypes { get; set; } = [DefaultAutoErrorResponseContentType];
 62
 63    /// <summary>
 64    /// Security requirements for the OpenAPI document.
 65    /// </summary>
 22066    public IDictionary<string, OpenApiSecurityRequirement> SecurityRequirement { get; private set; } = new Dictionary<st
 67
 68    /// <summary>
 69    /// Inline components specific to this OpenAPI document.
 70    /// </summary>
 4171    public OpenApiComponents InlineComponents { get; }
 72
 73    /// <summary>
 74    /// OpenAPI metadata for webhooks associated with this document.
 75    /// </summary>
 19476    public Dictionary<(string Pattern, HttpVerb Method), OpenAPIPathMetadata> WebHook { get; set; } = [];
 77
 78    /// <summary>
 79    /// OpenAPI metadata for callbacks associated with this document.
 80    /// </summary>
 19181    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>
 18989    public OpenApiDocDescriptor(KestrunHost host, string docId)
 90    {
 18991        ArgumentNullException.ThrowIfNull(host);
 18992        ArgumentNullException.ThrowIfNull(docId);
 18993        Host = host;
 18994        DocumentId = docId;
 18995        HasBeenGenerated = false;
 18996        InlineComponents = new OpenApiComponents();
 18997    }
 98
 99    /// <summary>
 100    /// Indicates whether the OpenAPI document has been generated at least once.
 101    /// </summary>
 193102    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    {
 5111        Document.Components ??= new OpenApiComponents();
 37112        ProcessComponentTypes(components.SchemaTypes, () => Document.Components.Schemas ??= new Dictionary<string, IOpen
 5113    }
 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    {
 5126        if (types is null || types.Count == 0)
 127        {
 0128            return;
 129        }
 130
 5131        ensureDictionary();
 64132        foreach (var type in types)
 133        {
 27134            buildAction(type);
 135        }
 5136    }
 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
 4144        var components = OpenApiSchemaDiscovery.GetOpenApiTypesAuto();
 145
 146        // Generate components from the discovered types
 4147        GenerateComponents(components);
 148
 4149        AddFormOptions(components);
 150
 151        // Process variable annotations from the host
 4152        ProcessVariableAnnotations(Host.ComponentAnnotations);
 4153    }
 154
 155    private void AddFormOptions(OpenApiComponentSet components)
 156    {
 64157        foreach (var type in components.SchemaTypes)
 158        {
 27159            if (type is null || !type.IsDefined(typeof(KrBindFormAttribute), inherit: false))
 160            {
 161                continue;
 162            }
 163
 5164            var formOptions = BuildFormOptionsSchema(type.FullName, type);
 5165            if (formOptions is null)
 166            {
 167                continue;
 168            }
 169
 5170            var rules = FormHelper.BuildFormPartRulesFromType(type);
 5171            AddFormPartRules(formOptions, rules);
 172
 173            // Register the option in the host.
 5174            _ = Host.AddFormOption(formOptions);
 175
 176            // Register part rules in the host runtime (best-effort: host rule store is keyed by name).
 50177            foreach (var rule in rules)
 178            {
 20179                _ = Host.AddFormPartRule(rule);
 180            }
 181        }
 5182    }
 183
 184    private static void AddFormPartRules(KrFormOptions options, IEnumerable<KrFormPartRule> rules)
 185    {
 5186        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 50187        foreach (var rule in rules)
 188        {
 20189            if (string.IsNullOrWhiteSpace(rule.Name))
 190            {
 191                continue;
 192            }
 193
 20194            var key = string.IsNullOrWhiteSpace(rule.Scope)
 20195                ? rule.Name
 20196                : $"{rule.Scope}::{rule.Name}";
 197
 20198            if (!seen.Add(key))
 199            {
 200                continue;
 201            }
 202
 20203            options.Rules.Add(rule);
 204        }
 5205    }
 206
 207    private KrFormOptions? BuildFormOptionsSchema(string? typeName, Type type)
 208    {
 5209        if (typeName is null)
 210        {
 0211            return null;
 212        }
 213
 15214        foreach (var attr in type.GetCustomAttributes<KrBindFormAttribute>(inherit: false))
 215        {
 5216            var formOptions = FormHelper.ApplyKrPartAttributes(attr);
 5217            formOptions.Name = typeName;
 5218            return formOptions;
 219        }
 0220        return null;
 5221    }
 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    {
 9229        if (annotations is null || annotations.Count == 0)
 230        {
 6231            Host.Logger.Warning("No OpenAPI component annotations were found in the host.");
 6232            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    {
 2347        if (variable.VariableType is null)
 348        {
 0349            return;
 350        }
 2351        var iSchema = InferPrimitiveSchema(variable.VariableType);
 2352        if (iSchema is OpenApiSchema schema)
 353        {
 354            // Apply any schema attributes from the parameter annotation
 2355            ApplyConcreteSchemaAttributes(responseDescriptor, schema);
 356            // Try to set default value from the variable initial value if not already set
 2357            if (!variable.NoDefault)
 358            {
 1359                schema.Default = OpenApiJsonNodeFactory.ToNode(variable.InitialValue);
 360            }
 361        }
 362
 363        // Either Schema OR Content, depending on ContentType
 2364        if (responseDescriptor.ContentType.Length == 0)
 365        {
 0366            throw new InvalidOperationException($"Response component '{variable.Name}' must specify at least one Content
 367        }
 368        // Use Content
 2369        response.Content ??= new Dictionary<string, IOpenApiMediaType>(StringComparer.Ordinal);
 8370        foreach (var contentType in responseDescriptor.ContentType)
 371        {
 2372            response.Content[contentType] = new OpenApiMediaType { Schema = iSchema };
 373        }
 2374    }
 375
 376    private static void ApplyResponseCommonFields(
 377        OpenApiResponse response,
 378        OpenApiResponseComponentAttribute responseDescriptor)
 379    {
 2380        if (responseDescriptor.Summary is not null)
 381        {
 0382            response.Summary = responseDescriptor.Summary;
 383        }
 2384        if (responseDescriptor.Description is not null)
 385        {
 2386            response.Description = responseDescriptor.Description;
 387        }
 2388    }
 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
 3398        BuildWebhooks(WebHook);
 399
 400        // Finally, build paths from registered routes
 3401        BuildPathsFromRegisteredRoutes(Host.RegisteredRoutes);
 402
 3403        HasBeenGenerated = true;
 3404    }
 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    {
 8428        using var sw = new StringWriter();
 8429        var w = new OpenApiJsonWriter(sw);
 8430        Document.SerializeAs(version, w);
 8431        return sw.ToString();
 8432    }
 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    {
 1441        using var sw = new StringWriter();
 1442        var w = new OpenApiYamlWriter(sw);
 1443        Document.SerializeAs(version, w);
 1444        return sw.ToString();
 1445    }
 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)
GetOptionalAbsoluteUri(System.String,System.String)
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)