< 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@ca54e35c77799b76774b3805b6f075cdbc0c5fbe
Line coverage
38%
Covered lines: 823
Uncovered lines: 1339
Coverable lines: 2162
Total lines: 6369
Line coverage: 38%
Branch coverage
29%
Covered branches: 487
Total branches: 1668
Branch coverage: 29.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

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

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: LoadAnnotatedFunctions(...)0%7280%
File 1: ProcessFunction(...)0%2040%
File 1: ProcessFunctionAttributes(...)0%812280%
File 1: ApplyExtensionAttribute(...)100%66100%
File 1: ApplyPathAttribute(...)0%272160%
File 1: ApplyPathLikePath(...)0%4260%
File 1: AddQueryParametersFromTemplate(...)0%7280%
File 1: ApplyPathLikeWebhook(...)0%2040%
File 1: ApplyPathLikeCallback(...)0%2040%
File 1: ChooseFirstNonEmpty(...)0%2040%
File 1: NormalizeNewlines(...)0%620%
File 1: ApplyResponseRefAttribute(...)0%110100%
File 1: ApplyResponseAttribute(...)0%342180%
File 1: SelectDefaultSuccessResponse(...)0%2040%
File 1: TryParseStatusCode(...)0%620%
File 1: HasContent(...)0%2040%
File 1: ApplyPropertyAttribute(...)0%342180%
File 1: ApplyAuthorizationAttribute(...)0%620%
File 1: BuildPolicyList(...)0%620%
File 1: ProcessParameters(...)0%1190340%
File 1: ApplyParameterAttribute(...)0%272160%
File 1: ApplyParameterRefAttribute(...)0%210140%
File 1: ApplyParameterExampleRefAttribute(...)0%110100%
File 1: RemoveExistingParameter(...)0%4260%
File 1: ApplyRequestBodyRefAttribute(...)0%2040%
File 1: ResolveRequestBodyReferenceId(...)0%156120%
File 1: FindReferenceIdForParameter(...)0%2040%
File 1: TryGetFirstRequestBodySchema(...)0%7280%
File 1: IsRequestBodySchemaMatchForParameter(...)0%7280%
File 1: ApplyRequestBodyAttribute(...)0%4260%
File 1: ApplyRequestBodyExampleRefAttribute(...)0%7280%
File 1: ApplyPreferredRequestBody(...)0%2040%
File 1: EnsureDefaultResponses(...)0%4260%
File 1: FinalizeRouteOptions(...)0%7280%
File 1: FinalizePathRouteOptions(...)0%2040%
File 1: RegisterWebhook(...)0%620%
File 1: RegisterCallback(...)0%620%
File 1: GetDocDescriptorOrThrow(...)0%620%
File 1: EnsureParamOnlyScriptBlock(...)0%620%
File 1: CreateRequestBodyFromAttribute(...)0%7280%
File 2: BuildPathsFromRegisteredRoutes(...)66.66%6688.88%
File 2: CreateOpenApiRouteEntries()91.66%121291.66%
File 2: ProcessOpenApiRouteGroup(...)75%4487.5%
File 2: GetOrCreatePathItem(...)66.66%66100%
File 2: ProcessOpenApiRouteEntry(...)57.14%161476.92%
File 2: .ctor(...)100%11100%
File 2: get_Pattern()100%11100%
File 2: get_Method()100%11100%
File 2: get_Map()100%11100%
File 2: get_Metadata()100%11100%
File 2: ApplyPathLevelMetadata(...)0%620%
File 2: ApplyPathLevelServers(...)0%420200%
File 2: ApplyPathLevelParameters(...)0%420200%
File 3: MergeXmlAttributes(...)100%1414100%
File 3: BuildSchema(...)100%88100%
File 3: BuildPropertySchema(...)92.85%1414100%
File 3: BuildComplexTypeSchema(...)100%11100%
File 3: BuildEnumSchema(...)0%620%
File 3: BuildArraySchema(...)100%22100%
File 3: BuildPrimitiveSchema(...)100%210%
File 3: GetOrCreateSchemaItem(...)0%156120%
File 3: TryGetSchemaItem(...)0%2040%
File 3: TryGetSchemaItem(...)100%210%
File 4: ApplyCallbackRefAttribute(...)0%156120%
File 4: BuildCallbacks(...)0%4260%
File 4: ProcessCallbacksGroup(...)0%4260%
File 4: ProcessCallbackOperation(...)0%156120%
File 4: GetOrCreateCallbackItem(...)0%156120%
File 4: ApplyCallbacks(...)50%5466.66%
File 5: AddComponentExample(...)75%44100%
File 5: TryAddExample(...)80%101090%
File 5: NewOpenApiExample(...)0%620%
File 5: NewOpenApiExample(...)100%210%
File 5: NewOpenApiExternalExample(...)100%210%
File 5: NewOpenApiExample(...)0%620%
File 6: NewOpenApiHeader(...)50%22100%
File 6: ResolveHeaderSchema(...)100%44100%
File 6: ThrowIfBothSchemaAndContentProvided(...)100%44100%
File 6: ApplyHeaderSchema(...)100%22100%
File 6: ApplyHeaderExamples(...)100%1010100%
File 6: ResolveHeaderExampleValue(...)75%9872.72%
File 6: ApplyHeaderContent(...)90%101087.5%
File 6: ResolveHeaderMediaTypeValue(...)50%18845.45%
File 6: AddComponentHeader(...)0%2040%
File 6: ApplyResponseHeaderAttribute(...)0%156120%
File 6: TryAddHeader(...)0%7280%
File 6: TryGetHeaderItem(...)0%2040%
File 7: GetSchema(...)0%7280%
File 7: GetParameter(...)0%7280%
File 7: GetRequestBody(...)0%7280%
File 7: GetHeader(...)0%7280%
File 7: GetResponse(...)0%7280%
File 7: ComponentSchemasExists(...)50%44100%
File 7: ComponentRequestBodiesExists(...)0%2040%
File 7: ComponentResponsesExists(...)0%2040%
File 7: ComponentParametersExists(...)0%2040%
File 7: ComponentExamplesExists(...)0%2040%
File 7: ComponentHeadersExists(...)0%2040%
File 7: ComponentCallbacksExists(...)0%2040%
File 7: ComponentLinksExists(...)0%2040%
File 7: ComponentPathItemsExists(...)0%2040%
File 7: BuildExtensions(...)87.5%161688.23%
File 8: CreateExternalDocs(...)100%11100%
File 8: CreateExternalDocs(...)50%2266.66%
File 8: CreateInfoContact(...)100%66100%
File 9: AddInlineExample(...)100%22100%
File 9: AddInlineLink(...)100%22100%
File 9: AddComponent(...)83.33%6692.3%
File 9: TryGetComponent(...)100%11100%
File 9: TryGetInline(...)100%11100%
File 9: TryGetFromComponents(...)42.85%271460%
File 9: TryGetAndCast()100%44100%
File 9: ValidateComponentType(...)57.14%211466.66%
File 9: TryGet(...)100%44100%
File 9: ThrowTypeMismatch(...)100%11100%
File 10: AddComponentLink(...)75%44100%
File 10: TryAddLink(...)87.5%88100%
File 10: ApplyResponseLinkAttribute(...)25%371244.44%
File 10: ApplyLinkRefAttribute(...)0%620%
File 10: NewOpenApiLink(...)100%210%
File 10: ValidateLinkOperation(...)0%7280%
File 10: ApplyLinkDescription(...)0%620%
File 10: ApplyLinkServer(...)0%620%
File 10: ApplyLinkOperation(...)0%2040%
File 10: ApplyLinkRequestBody(...)0%4260%
File 10: ApplyLinkParameters(...)0%156120%
File 10: ToRuntimeExpressionAnyWrapper(...)0%620%
File 11: MergeSchemaAttributes(...)0%7280%
File 11: MergeStringProperties(...)0%156120%
File 11: MergeEnumAndCollections(...)0%110100%
File 11: MergeNumericProperties(...)0%110100%
File 11: MergeBooleanProperties(...)100%210%
File 11: MergeTypeAndRequired(...)0%7280%
File 11: MergeCustomFields(...)0%4260%
File 12: CreateParameterFromAttribute(...)0%2040%
File 12: ApplyParameterAttribute(...)0%156120%
File 12: ApplyExampleRefAttribute(...)0%156120%
File 12: ProcessParameterComponent(...)75%1212100%
File 12: ApplyParameterCommonFields(...)50%22100%
File 12: TryApplyVariableTypeSchema(...)40%402063.15%
File 12: ProcessParameterExampleRef(...)0%2040%
File 12: ValidateParameterHasSchemaOrContent(...)0%4260%
File 12: AddExampleToParameterExamples(...)0%620%
File 12: AddExamplesToContentMediaTypes(...)0%7280%
File 12: AddExamplesToContentMediaTypes(...)0%110100%
File 12: ProcessPowerShellAttribute(...)75%4477.77%
File 12: ValidateParameterHasSchemaOrContentForPowerShell(...)50%11650%
File 12: ApplyPowerShellAttributeToParameter(...)50%3240%
File 12: ApplyPowerShellAttributeToRequestBody(...)0%2040%
File 12: ApplyPowerShellAttributeToMediaTypeSchemas(...)0%2040%
File 12: ApplyPowerShellAttributesToSchema(...)100%11100%
File 12: ApplyItemConstraints(...)50%5460%
File 12: ApplyRangeConstraints(...)100%44100%
File 12: ApplyLengthConstraints(...)50%5460%
File 12: ApplyPatternConstraints(...)50%2266.66%
File 12: ApplyAllowedValuesConstraints(...)50%5466.66%
File 12: ApplyNullabilityConstraints(...)50%9657.14%
File 12: GetOrCreateParameterItem(...)50%131280%
File 12: TryGetParameterItem(...)75%4477.77%
File 12: TryGetParameterItem(...)100%11100%
File 13: BuildOperationFromMetadata(...)50%88100%
File 13: ApplyExtensions(...)50%6450%
File 13: ApplyTags(...)83.33%66100%
File 13: ApplyServers(...)15%1392033.33%
File 13: ApplyParameters(...)13.63%1122242.85%
File 13: ApplySecurity(...)28.57%1371414.28%
File 14: CloneExampleOrThrow(...)0%7280%
File 14: ProcessRequestBodyComponent(...)0%620%
File 14: ApplyRequestBodyCommonFields(...)100%210%
File 14: TryApplyVariableTypeSchema(...)0%210140%
File 14: ProcessRequestBodyExampleRef(...)0%4260%
File 14: GetOrCreateRequestBodyItem(...)0%156120%
File 14: TryGetRequestBodyItem(...)50%5455.55%
File 14: TryGetRequestBodyItem(...)100%11100%
File 15: GetKeyOverride(...)0%620%
File 15: CreateResponseFromAttribute(...)0%156120%
File 15: ApplyResponseAttribute(...)100%210%
File 15: ApplyDescription(...)0%620%
File 15: ResolveResponseSchema(...)0%2040%
File 15: ApplySchemaToContentTypes(...)0%156120%
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(...)0%2040%
File 15: CloneSchemaOrThrow(...)0%4260%
File 15: ProcessResponseExampleRef(...)0%4260%
File 15: ProcessResponseLinkRef(...)0%4260%
File 15: ProcessResponseHeaderRef(...)0%4260%
File 15: ProcessResponseComponent(...)100%210%
File 15: GetOrCreateResponseItem(...)0%156120%
File 15: TryGetResponseItem(...)0%2040%
File 15: TryGetResponseItem(...)100%210%
File 16: GetSchemaIdentity(...)0%7280%
File 16: BuildSchemaForType(...)90%101093.75%
File 16: ProcessExtensions(...)33.33%17633.33%
File 16: TryBuildPrimitiveSchema(...)100%22100%
File 16: TryBuildDerivedSchemaFromBaseType(...)70%111076.92%
File 16: HasComposableBaseType(...)50%22100%
File 16: TryResolveSimpleOrReferenceBaseSchema(...)25%9433.33%
File 16: IsSimpleSchemaOrReference(...)50%44100%
File 16: TryResolveArrayWrapperDerivedSchema(...)25%41820%
File 16: CreateAllOfAdditionalObjectSchema(...)100%210%
File 16: CreateSchemaForDeclaredProperties(...)100%22100%
File 16: ComposeWithParentSchema(...)30%191054.54%
File 16: BuildBaseTypeSchema(...)50%2266.66%
File 16: BuildCustomBaseTypeSchema(...)31.25%201675.86%
File 16: RegisterEnumSchema(...)75%44100%
File 16: ApplyTypeAttributes(...)71.42%311455.55%
File 16: ProcessTypeProperties(...)87.5%88100%
File 16: TryCreateTypeInstance(...)100%11100%
File 16: CapturePropertyDefault(...)100%9877.77%
File 16: IsIntrinsicDefault(...)100%1616100%
File 16: MakeNullable(...)75%44100%
File 16: InferPrimitiveSchema(...)91.66%121291.66%
File 16: InferArraySchema(...)33.33%8660%
File 16: InferPowerShellClassSchema(...)0%4260%
File 16: .cctor()100%1156.6%
File 16: ApplySchemaAttr(...)50%8662.5%
File 16: ApplyConcreteSchemaAttributes(...)100%11100%
File 16: ApplyTitleAndDescription(...)66.66%8660%
File 16: ApplySchemaType(...)14.28%1141420%
File 16: ApplyFormatAndNumericBounds(...)50%461238.46%
File 16: ApplyLengthAndPattern(...)50%9657.14%
File 16: ApplyCollectionConstraints(...)50%191054.54%
File 16: ApplyFlags(...)100%22100%
File 16: ApplyExamplesAndDefaults(...)44.44%711845.45%
File 16: ApplyXmlMetadata(...)100%2222100%
File 16: ApplyReferenceSchemaAttributes(...)0%2040%
File 17: ApplySecurityScheme(...)81.25%191678.57%
File 17: GetSecurityScheme(...)100%22100%
File 17: GetSecurityScheme(...)50%22100%
File 17: GetSecurityScheme(...)0%2040%
File 17: GetSecurityScheme(...)0%156120%
File 17: GetSecurityScheme(...)100%11100%
File 17: GetSecurityScheme(...)100%11100%
File 17: GetSecurityScheme(...)100%11100%
File 17: GetSecurityScheme(...)100%11100%
File 17: AddSecurityComponent(...)100%44100%
File 18: AddTag(...)100%1010100%
File 18: AddTagIfMissing(...)100%22100%
File 18: RemoveTag(...)50%2266.66%
File 18: RemoveTag(...)50%22100%
File 18: GetOrCreateTagItem(...)100%44100%
File 18: TryGetTag(...)100%22100%
File 18: ContainsTag(...)100%22100%
File 19: BuildWebhooks(...)50%23622.22%
File 19: ProcessWebhookGroup(...)0%4260%
File 19: ProcessWebhookOperation(...)100%210%
File 19: GetOrCreateWebhookItem(...)0%4260%
File 20: .cctor()100%11100%
File 20: get_Host()100%11100%
File 20: get_DocumentId()100%11100%
File 20: get_Document()100%11100%
File 20: get_SecurityRequirement()100%11100%
File 20: get_InlineComponents()100%11100%
File 20: get_WebHook()100%11100%
File 20: get_Callbacks()100%11100%
File 20: .ctor(...)100%11100%
File 20: get_HasBeenGenerated()100%11100%
File 20: GenerateComponents(...)50%22100%
File 20: ProcessComponentTypes(...)66.66%6683.33%
File 20: GenerateComponents()100%11100%
File 20: ProcessVariableAnnotations(...)91.66%1212100%
File 20: DispatchComponentAnnotations(...)45.45%1982228.57%
File 20: ProcessVariableExtension(...)0%110100%
File 20: TryApplyVariableTypeSchema(...)0%156120%
File 20: ApplyResponseCommonFields(...)0%2040%
File 20: GenerateDoc()100%11100%
File 20: ReadAndDiagnose(...)100%210%
File 20: ToJson(...)100%11100%
File 20: ToYaml(...)100%210%
File 20: AddOpenApiExtension(...)0%4260%

File(s)

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

#LineLine coverage
 1using System.Management.Automation;
 2using System.Management.Automation.Internal;
 3using System.Management.Automation.Language;
 4using System.Text.Json.Nodes;
 5using Kestrun.Hosting;
 6using Kestrun.Hosting.Options;
 7using Kestrun.Languages;
 8using Kestrun.Utilities;
 9using Microsoft.OpenApi;
 10
 11namespace Kestrun.OpenApi;
 12
 13public partial class OpenApiDocDescriptor
 14{
 15    /// <summary>
 16    /// Enumerates all in-session PowerShell functions in the given runspace,
 17    /// detects those annotated with [OpenApiPath], and maps them into the provided KestrunHost.
 18    /// </summary>
 19    /// <param name="cmdInfos">List of FunctionInfo objects representing PowerShell functions.</param>
 20    public void LoadAnnotatedFunctions(List<FunctionInfo> cmdInfos)
 21    {
 022        ArgumentNullException.ThrowIfNull(cmdInfos);
 023        var callbacks = cmdInfos
 024                .Where(f => f.ScriptBlock.Attributes?.All(a => a is OpenApiCallbackAttribute) != false);
 25
 026        var others = cmdInfos
 027            .Where(f => f.ScriptBlock.Attributes?.All(a => a is not OpenApiCallbackAttribute) != false);
 28        // (equivalent to NOT having any callback attribute)
 29
 030        foreach (var func in callbacks)
 31        {
 032            ProcessFunction(func);
 33        }
 34
 035        BuildCallbacks(Callbacks);
 036        foreach (var func in others)
 37        {
 038            ProcessFunction(func);
 39        }
 040    }
 41
 42    /// <summary>
 43    /// Processes a single PowerShell function, extracting OpenAPI annotations and configuring the host accordingly.
 44    /// </summary>
 45    /// <param name="func">The function information.</param>
 46    private void ProcessFunction(FunctionInfo func)
 47    {
 48        try
 49        {
 050            var help = func.GetHelp();
 051            var sb = func.ScriptBlock;
 052            if (sb is null)
 53            {
 054                return;
 55            }
 56
 057            var attrs = sb.Attributes;
 058            if (attrs.Count == 0)
 59            {
 060                return;
 61            }
 62            // Create route options and OpenAPI metadata
 063            var routeOptions = new MapRouteOptions();
 064            var openApiMetadata = new OpenAPIPathMetadata(mapOptions: routeOptions);
 65            // Process attributes to populate route options and OpenAPI metadata
 066            var parsedVerb = ProcessFunctionAttributes(func, help!, attrs, routeOptions, openApiMetadata);
 67
 068            ProcessParameters(func, help!, routeOptions, openApiMetadata);
 69
 070            EnsureDefaultResponses(openApiMetadata);
 071            FinalizeRouteOptions(func, sb, openApiMetadata, routeOptions, parsedVerb);
 072        }
 073        catch (Exception ex)
 74        {
 075            Host.Logger.Error("Error loading OpenAPI annotated function '{funcName}': {message}", func.Name, ex.Message)
 076        }
 077    }
 78
 79    /// <summary>
 80    /// Processes the OpenAPI-related attributes on the specified function.
 81    /// </summary>
 82    /// <param name="func">The function information.</param>
 83    /// <param name="help">The comment help information.</param>
 84    /// <param name="attrs">The collection of attributes applied to the function.</param>
 85    /// <param name="routeOptions">The route options to configure.</param>
 86    /// <param name="openApiMetadata">The OpenAPI metadata to populate.</param>
 87    /// <returns>The parsed HTTP verb for the function.</returns>
 88    private HttpVerb ProcessFunctionAttributes(
 89        FunctionInfo func,
 90        CommentHelpInfo help,
 91        IReadOnlyCollection<Attribute> attrs,
 92        MapRouteOptions routeOptions,
 93        OpenAPIPathMetadata openApiMetadata)
 94    {
 095        var parsedVerb = HttpVerb.Get;
 96
 097        foreach (var attr in attrs)
 98        {
 99            try
 100            {
 101                switch (attr)
 102                {
 103                    case OpenApiPathAttribute path:
 0104                        parsedVerb = ApplyPathAttribute(func, help, routeOptions, openApiMetadata, parsedVerb, path);
 0105                        break;
 106                    case OpenApiWebhookAttribute webhook:
 0107                        parsedVerb = ApplyPathAttribute(func, help, routeOptions, openApiMetadata, parsedVerb, webhook);
 0108                        break;
 109                    case OpenApiCallbackAttribute callbackOperation:
 0110                        parsedVerb = ApplyPathAttribute(func, help, routeOptions, openApiMetadata, parsedVerb, callbackO
 0111                        break;
 112                    case OpenApiExtensionAttribute extensionAttr:
 0113                        ApplyExtensionAttribute(openApiMetadata, extensionAttr);
 0114                        break;
 115                    case OpenApiResponseRefAttribute responseRef:
 0116                        ApplyResponseRefAttribute(openApiMetadata, responseRef);
 0117                        break;
 118                    case OpenApiResponseAttribute responseAttr:
 0119                        ApplyResponseAttribute(openApiMetadata, responseAttr, routeOptions);
 0120                        break;
 121                    case OpenApiResponseExampleRefAttribute responseAttr:
 0122                        ApplyResponseAttribute(openApiMetadata, responseAttr, routeOptions);
 0123                        break;
 124                    case OpenApiResponseLinkRefAttribute linkRefAttr:
 0125                        ApplyResponseLinkAttribute(openApiMetadata, linkRefAttr);
 0126                        break;
 127                    case OpenApiPropertyAttribute propertyAttr:
 0128                        ApplyPropertyAttribute(openApiMetadata, propertyAttr);
 0129                        break;
 130                    case OpenApiAuthorizationAttribute authAttr:
 0131                        ApplyAuthorizationAttribute(routeOptions, openApiMetadata, authAttr);
 0132                        break;
 133                    case IOpenApiResponseHeaderAttribute responseHeaderAttr:
 0134                        ApplyResponseHeaderAttribute(openApiMetadata, responseHeaderAttr);
 0135                        break;
 136                    case OpenApiCallbackRefAttribute callbackRefAttr:
 0137                        ApplyCallbackRefAttribute(openApiMetadata, callbackRefAttr);
 0138                        break;
 139                    case KestrunAnnotation ka:
 0140                        throw new InvalidOperationException($"Unhandled Kestrun annotation: {ka.GetType().Name}");
 141                }
 0142            }
 0143            catch (InvalidOperationException ex)
 144            {
 0145                Host.Logger.Error(ex, "Error processing OpenApiPath attribute on function '{funcName}': {message}", func
 0146            }
 0147            catch (Exception ex)
 148            {
 0149                Host.Logger.Error(ex, "Error processing OpenApiPath attribute on function '{funcName}': {message}", func
 0150            }
 151        }
 152
 0153        return parsedVerb;
 154    }
 155
 156    /// <summary>
 157    /// Applies the OpenApiExtension attribute to the function's OpenAPI metadata.
 158    /// </summary>
 159    /// <param name="openApiMetadata">The OpenAPI metadata to which the extension will be applied.</param>
 160    /// <param name="extensionAttr">The OpenApiExtension attribute containing the extension data.</param>
 161    private void ApplyExtensionAttribute(OpenAPIPathMetadata openApiMetadata, OpenApiExtensionAttribute extensionAttr)
 162    {
 3163        if (Host.Logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 164        {
 3165            Host.Logger.Debug("Applying OpenApiExtension '{extensionName}' to function metadata", extensionAttr.Name);
 166        }
 3167        openApiMetadata.Extensions ??= [];
 168
 169        // Parse string into a JsonNode tree.
 3170        var node = JsonNode.Parse(extensionAttr.Json);
 3171        if (node is null)
 172        {
 1173            Host.Logger.Error("Error parsing OpenAPI extension '{extensionName}': JSON is null", extensionAttr.Name);
 1174            return;
 175        }
 2176        openApiMetadata.Extensions[extensionAttr.Name] = new JsonNodeExtension(node);
 2177    }
 178
 179    /// <summary>
 180    /// Applies the OpenApiPath attribute to the function's route options and metadata.
 181    /// </summary>
 182    /// <param name="func">The function information.</param>
 183    /// <param name="help">The comment help information.</param>
 184    /// <param name="routeOptions">The route options to configure.</param>
 185    /// <param name="metadata">The OpenAPI metadata to populate.</param>
 186    /// <param name="parsedVerb">The currently parsed HTTP verb.</param>
 187    /// <param name="oaPath">The OpenApiPath attribute instance.</param>
 188    /// <returns>The updated HTTP verb after processing the attribute.</returns>
 189    private static HttpVerb ApplyPathAttribute(
 190        FunctionInfo func,
 191        CommentHelpInfo help,
 192        MapRouteOptions routeOptions,
 193        OpenAPIPathMetadata metadata,
 194        HttpVerb parsedVerb,
 195        IOpenApiPathAttribute oaPath)
 196    {
 0197        var httpVerb = oaPath.HttpVerb ?? string.Empty;
 0198        if (!string.IsNullOrWhiteSpace(httpVerb))
 199        {
 0200            parsedVerb = HttpVerbExtensions.FromMethodString(httpVerb);
 0201            routeOptions.HttpVerbs.Add(parsedVerb);
 202        }
 203
 0204        var pattern = oaPath.Pattern;
 0205        if (string.IsNullOrWhiteSpace(pattern))
 206        {
 0207            throw new InvalidOperationException("OpenApiPath attribute must specify a non-empty Pattern property.");
 208        }
 0209        var openApiPattern = pattern;
 0210        var routePattern = pattern;
 0211        if (oaPath is OpenApiPathAttribute)
 212        {
 0213            if (!Rfc6570PathTemplateMapper.TryMapToKestrelRoute(pattern, out var mapping, out var error))
 214            {
 0215                throw new InvalidOperationException($"OpenApiPath pattern '{pattern}' is invalid: {error}");
 216            }
 217
 0218            openApiPattern = mapping.OpenApiPattern;
 0219            routePattern = mapping.KestrelPattern;
 0220            AddQueryParametersFromTemplate(metadata, mapping.QueryParameters);
 221        }
 222
 223        // Apply pattern, summary, description, tags
 0224        routeOptions.Pattern = routePattern;
 0225        metadata.Summary = ChooseFirstNonEmpty(oaPath.Summary, help.GetSynopsis());
 0226        metadata.Description = ChooseFirstNonEmpty(oaPath.Description, help.GetDescription());
 0227        metadata.Tags = [.. oaPath.Tags];
 228
 229        // Apply deprecated flag if specified
 0230        metadata.Deprecated |= oaPath.Deprecated;
 231        // Apply document ID if specified
 0232        metadata.DocumentId = oaPath.DocumentId;
 233        switch (oaPath)
 234        {
 235            case OpenApiPathAttribute oaPathConcrete:
 0236                ApplyPathLikePath(func, routeOptions, metadata, oaPathConcrete, openApiPattern);
 0237                break;
 238            case OpenApiWebhookAttribute oaWebhook:
 0239                ApplyPathLikeWebhook(func, metadata, oaWebhook, pattern);
 0240                break;
 241            case OpenApiCallbackAttribute oaCallback:
 0242                ApplyPathLikeCallback(func, metadata, oaCallback, httpVerb, pattern);
 243                break;
 244        }
 245
 0246        return parsedVerb;
 247    }
 248
 249    /// <summary>
 250    /// Applies the OpenApiPath attribute to the function's route options and metadata for a standard path.
 251    /// </summary>
 252    /// <param name="func">The function information.</param>
 253    /// <param name="routeOptions">The route options to configure.</param>
 254    /// <param name="metadata">The OpenAPI metadata to populate.</param>
 255    /// <param name="oaPath">The OpenApiPath attribute instance.</param>
 256    /// <param name="openApiPattern">The OpenAPI path pattern.</param>
 257    private static void ApplyPathLikePath(
 258        FunctionInfo func,
 259        MapRouteOptions routeOptions,
 260        OpenAPIPathMetadata metadata,
 261        OpenApiPathAttribute oaPath,
 262        string openApiPattern)
 263    {
 0264        metadata.Pattern = openApiPattern;
 0265        metadata.PathLikeKind = OpenApiPathLikeKind.Path;
 0266        if (!string.IsNullOrWhiteSpace(oaPath.CorsPolicy))
 267        {
 268            // Apply Cors policy name if specified
 0269            routeOptions.CorsPolicy = oaPath.CorsPolicy;
 270        }
 271
 0272        metadata.OperationId = oaPath.OperationId is null
 0273            ? func.Name
 0274            : string.IsNullOrWhiteSpace(oaPath.OperationId) ? metadata.OperationId : oaPath.OperationId;
 0275    }
 276
 277    /// <summary>
 278    /// Adds query parameters inferred from RFC6570 query expressions.
 279    /// </summary>
 280    /// <param name="metadata">The OpenAPI metadata to update.</param>
 281    /// <param name="queryParameterNames">The query parameter names to add.</param>
 282    private static void AddQueryParametersFromTemplate(
 283        OpenAPIPathMetadata metadata,
 284        IReadOnlyList<string> queryParameterNames)
 285    {
 0286        if (queryParameterNames.Count == 0)
 287        {
 0288            return;
 289        }
 290
 0291        metadata.Parameters ??= [];
 0292        foreach (var name in queryParameterNames.Distinct(StringComparer.OrdinalIgnoreCase))
 293        {
 0294            if (metadata.Parameters.Any(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase)))
 295            {
 296                continue;
 297            }
 298
 0299            metadata.Parameters.Add(new OpenApiParameter
 0300            {
 0301                Name = name,
 0302                In = ParameterLocation.Query,
 0303                Required = false,
 0304                Schema = new OpenApiSchema { Type = JsonSchemaType.String },
 0305            });
 306        }
 0307    }
 308    /// <summary>
 309    /// Applies the OpenApiWebhook attribute to the function's OpenAPI metadata.
 310    /// </summary>
 311    /// <param name="func">The function information.</param>
 312    /// <param name="metadata">The OpenAPI metadata to populate.</param>
 313    /// <param name="oaPath">The OpenApiWebhook attribute instance.</param>
 314    /// <param name="pattern">The route pattern.</param>
 315    private static void ApplyPathLikeWebhook(FunctionInfo func, OpenAPIPathMetadata metadata, OpenApiWebhookAttribute oa
 316    {
 0317        metadata.Pattern = pattern;
 0318        metadata.PathLikeKind = OpenApiPathLikeKind.Webhook;
 0319        metadata.OperationId = oaPath.OperationId is null
 0320            ? func.Name
 0321            : string.IsNullOrWhiteSpace(oaPath.OperationId) ? metadata.OperationId : oaPath.OperationId;
 0322    }
 323
 324    /// <summary>
 325    /// Applies the OpenApiCallback attribute to the function's OpenAPI metadata.
 326    /// </summary>
 327    /// <param name="func">The function information.</param>
 328    /// <param name="metadata">The OpenAPI metadata to populate.</param>
 329    /// <param name="oaCallback">The OpenApiCallback attribute instance.</param>
 330    /// <param name="httpVerb">The HTTP verb associated with the callback.</param>
 331    /// <param name="callbackPattern">The callback route pattern.</param>
 332    /// <exception cref="InvalidOperationException">Thrown when the Expression property of the OpenApiCallback attribute
 333    private static void ApplyPathLikeCallback(
 334        FunctionInfo func,
 335        OpenAPIPathMetadata metadata,
 336        OpenApiCallbackAttribute oaCallback,
 337        string httpVerb,
 338        string callbackPattern)
 339    {
 340        // Callbacks are neither paths nor webhooks
 0341        metadata.PathLikeKind = OpenApiPathLikeKind.Callback;
 0342        if (string.IsNullOrWhiteSpace(oaCallback.Expression))
 343        {
 0344            throw new InvalidOperationException("OpenApiCallback attribute must specify a non-empty Expression property.
 345        }
 346        // Callbacks must have an expression
 0347        metadata.Expression = CallbackOperationId.BuildCallbackKey(oaCallback.Expression, callbackPattern);
 0348        metadata.Inline = oaCallback.Inline;
 0349        metadata.Pattern = func.Name;
 0350        metadata.OperationId = string.IsNullOrWhiteSpace(oaCallback.OperationId)
 0351           ? CallbackOperationId.FromLastSegment(func.Name, httpVerb, oaCallback.Expression)
 0352           : oaCallback.OperationId;
 0353    }
 354
 355    /// <summary>
 356    /// Chooses the first non-empty string from the provided values, normalizing newlines.
 357    /// </summary>
 358    /// <param name="values">An array of string values to evaluate.</param>
 359    /// <returns>The first non-empty string with normalized newlines, or null if all are null or whitespace.</returns>
 360    private static string? ChooseFirstNonEmpty(params string?[] values)
 361    {
 0362        foreach (var value in values)
 363        {
 0364            if (!string.IsNullOrWhiteSpace(value))
 365            {
 0366                return NormalizeNewlines(value);
 367            }
 368        }
 369
 0370        return null;
 371    }
 372
 373    /// <summary>
 374    /// Normalizes newlines in the given string to use '\n' only.
 375    /// </summary>
 376    /// <param name="value">The string to normalize.</param>
 377    /// <returns>The normalized string.</returns>
 0378    private static string? NormalizeNewlines(string? value) => value?.Replace("\r\n", "\n");
 379
 380    /// <summary>
 381    /// Applies the OpenApiResponseRef attribute to the function's OpenAPI metadata.
 382    /// </summary>
 383    ///     <param name="metadata">The OpenAPI metadata to update.</param>
 384    /// <param name="attribute">The OpenApiResponseRef attribute containing response reference details.</param>
 385    private void ApplyResponseRefAttribute(OpenAPIPathMetadata metadata, OpenApiResponseRefAttribute attribute)
 386    {
 0387        metadata.Responses ??= [];
 388
 0389        if (!TryGetResponseItem(attribute.ReferenceId, out var response, out var inline))
 390        {
 0391            throw new InvalidOperationException($"Response component with ID '{attribute.ReferenceId}' not found.");
 392        }
 393
 0394        IOpenApiResponse iResponse = attribute.Inline || inline ? response!.Clone() : new OpenApiResponseReference(attri
 395
 0396        if (attribute.Description is not null)
 397        {
 0398            iResponse.Description = attribute.Description;
 399        }
 400
 0401        if (metadata.Responses.ContainsKey(attribute.StatusCode))
 402        {
 0403            throw new InvalidOperationException($"Response for status code '{attribute.StatusCode}' is already defined f
 404        }
 405
 0406        metadata.Responses.Add(attribute.StatusCode, iResponse);
 0407    }
 408
 409    /// <summary>
 410    /// Applies the OpenApiResponse attribute to the function's OpenAPI metadata.
 411    /// </summary>
 412    /// <param name="metadata">The OpenAPI metadata to update.</param>
 413    /// <param name="attribute">The OpenApiResponse attribute containing response details.</param>
 414    /// <param name="routeOptions">The route options to update.</param>
 415    private void ApplyResponseAttribute(OpenAPIPathMetadata metadata, IOpenApiResponseAttribute attribute, MapRouteOptio
 416    {
 0417        metadata.Responses ??= [];
 0418        var response = metadata.Responses.TryGetValue(attribute.StatusCode, out var value) ? value as OpenApiResponse : 
 0419        if (response is not null && CreateResponseFromAttribute(attribute, response))
 420        {
 0421            _ = metadata.Responses.TryAdd(attribute.StatusCode, response);
 0422            if (routeOptions.DefaultResponseContentType is null)
 423            {
 0424                var defaultStatusCode = SelectDefaultSuccessResponse(metadata.Responses);
 0425                if (defaultStatusCode is not null && metadata.Responses.TryGetValue(defaultStatusCode, out var defaultRe
 0426                    defaultResponse.Content is not null && defaultResponse.Content.Count > 0)
 427                {
 0428                    routeOptions.DefaultResponseContentType =
 0429                        defaultResponse.Content.Keys.First();
 430                }
 431            }
 432        }
 0433    }
 434
 435    /// <summary>
 436    /// Selects the default success response (2xx) from the given OpenApiResponses.
 437    /// </summary>
 438    /// <param name="responses">The collection of OpenApiResponses to select from.</param>
 439    /// <returns>The status code of the default success response, or null if none found.</returns>
 440    private static string? SelectDefaultSuccessResponse(OpenApiResponses responses)
 441    {
 0442        return responses
 0443            .Select(kvp => new
 0444            {
 0445                StatusCode = kvp.Key,
 0446                Code = TryParseStatusCode(kvp.Key),
 0447                Response = kvp.Value
 0448            })
 0449            .Where(x =>
 0450                x.Code is >= 200 and < 300 &&
 0451                HasContent(x.Response))
 0452            .OrderBy(x => x.Code)
 0453            .Select(x => x.StatusCode)
 0454            .FirstOrDefault();
 455    }
 456
 457    /// <summary>
 458    /// Tries to parse the given status code string into an integer.
 459    /// </summary>
 460    /// <param name="statusCode">The status code as a string.</param>
 461    /// <returns>The parsed integer status code, or -1 if parsing fails.</returns>
 462    private static int TryParseStatusCode(string statusCode)
 0463        => int.TryParse(statusCode, out var code) ? code : -1;
 464
 465    /// <summary>
 466    /// Determines if the given response has content defined.
 467    /// </summary>
 468    /// <param name="response">The OpenAPI response to check for content.</param>
 469    /// <returns>True if the response has content; otherwise, false.</returns>
 470    private static bool HasContent(IOpenApiResponse response)
 471    {
 472        // If your concrete type is OpenApiResponse (common), this is the easiest path:
 0473        if (response is OpenApiResponse r)
 474        {
 0475            return r.Content is not null && r.Content.Count > 0;
 476        }
 477
 478        // Otherwise, we can't reliably know. Be conservative:
 0479        return false;
 480    }
 481
 482    /// <summary>
 483    /// Applies the OpenApiProperty attribute to the function's OpenAPI metadata.
 484    /// </summary>
 485    /// <param name="metadata">The OpenAPI metadata to update.</param>
 486    /// <param name="attribute">The OpenApiProperty attribute containing property details.</param>
 487    /// <exception cref="InvalidOperationException"></exception>
 488    private static void ApplyPropertyAttribute(OpenAPIPathMetadata metadata, OpenApiPropertyAttribute attribute)
 489    {
 0490        if (attribute.StatusCode is null)
 491        {
 0492            return;
 493        }
 494
 0495        if (metadata.Responses is null || !metadata.Responses.TryGetValue(attribute.StatusCode, out var res))
 496        {
 0497            throw new InvalidOperationException($"Response for status code '{attribute.StatusCode}' is not defined for t
 498        }
 499
 0500        if (res is OpenApiResponseReference)
 501        {
 0502            throw new InvalidOperationException($"Cannot apply OpenApiPropertyAttribute to response '{attribute.StatusCo
 503        }
 504
 0505        if (res is OpenApiResponse response)
 506        {
 0507            if (response.Content is null || response.Content.Count == 0)
 508            {
 0509                throw new InvalidOperationException($"Cannot apply OpenApiPropertyAttribute to response '{attribute.Stat
 510            }
 511
 0512            foreach (var content in response.Content.Values)
 513            {
 0514                if (content.Schema is null)
 515                {
 0516                    throw new InvalidOperationException($"Cannot apply OpenApiPropertyAttribute to response '{attribute.
 517                }
 518
 0519                ApplySchemaAttr(attribute, content.Schema);
 520            }
 521        }
 0522    }
 523
 524    private void ApplyAuthorizationAttribute(MapRouteOptions routeOptions, OpenAPIPathMetadata metadata, OpenApiAuthoriz
 525    {
 0526        metadata.SecuritySchemes ??= [];
 0527        var policyList = BuildPolicyList(attribute.Policies);
 0528        var securitySchemeList = Host.AddSecurityRequirementObject(attribute.Scheme, policyList, metadata.SecurityScheme
 0529        routeOptions.AddSecurityRequirementObject(schemes: securitySchemeList, policies: policyList);
 0530    }
 531
 532    private static List<string> BuildPolicyList(string? policies)
 533    {
 0534        return [.. (string.IsNullOrWhiteSpace(policies) ? new List<string>() : [.. policies.Split(',')])
 0535            .Where(p => !string.IsNullOrWhiteSpace(p))
 0536            .Select(p => p.Trim())];
 537    }
 538
 539    /// <summary>
 540    /// Processes the parameters of the specified function, applying OpenAPI annotations as needed.
 541    /// </summary>
 542    /// <param name="func">The function information.</param>
 543    /// <param name="help">The comment help information.</param>
 544    /// <param name="routeOptions">The route options to update.</param>
 545    /// <param name="openApiMetadata">The OpenAPI metadata to update.</param>
 546    /// <exception cref="InvalidOperationException">Thrown when an invalid operation occurs during parameter processing.
 547    private void ProcessParameters(
 548        FunctionInfo func,
 549        CommentHelpInfo help,
 550        MapRouteOptions routeOptions,
 551        OpenAPIPathMetadata openApiMetadata)
 552    {
 0553        foreach (var paramInfo in func.Parameters.Values)
 554        {
 555            // First pass for parameter and request body attributes
 0556            foreach (var attribute in paramInfo.Attributes)
 557            {
 558                switch (attribute)
 559                {
 560                    case OpenApiParameterAttribute paramAttr:
 0561                        ApplyParameterAttribute(func, help, routeOptions, openApiMetadata, paramInfo, paramAttr);
 0562                        break;
 563                    case OpenApiParameterRefAttribute paramRefAttr:
 0564                        ApplyParameterRefAttribute(help, routeOptions, openApiMetadata, paramInfo, paramRefAttr);
 0565                        break;
 566                    case OpenApiRequestBodyRefAttribute requestBodyRefAttr:
 0567                        ApplyRequestBodyRefAttribute(help, routeOptions, openApiMetadata, paramInfo, requestBodyRefAttr)
 0568                        break;
 569                    case OpenApiRequestBodyAttribute requestBodyAttr:
 0570                        ApplyRequestBodyAttribute(help, routeOptions, openApiMetadata, paramInfo, requestBodyAttr);
 0571                        break;
 572                    case OpenApiRequestBodyExampleRefAttribute:
 573                    case OpenApiParameterExampleRefAttribute:
 574                        // Do nothing here; handled later
 575                        break;
 576                    case KestrunAnnotation ka:
 0577                        throw new InvalidOperationException($"Unhandled Kestrun annotation: {ka.GetType().Name}");
 578                }
 579            }
 580            // Second pass for example references
 0581            foreach (var attribute in paramInfo.Attributes)
 582            {
 583                switch (attribute)
 584                {
 585                    case OpenApiParameterAttribute:
 586                    case OpenApiParameterRefAttribute:
 587                    case OpenApiRequestBodyRefAttribute:
 588                    case OpenApiRequestBodyAttribute:
 589                        // Already handled
 590                        break;
 591                    case OpenApiRequestBodyExampleRefAttribute requestBodyExampleRefAttr:
 0592                        ApplyRequestBodyExampleRefAttribute(openApiMetadata, requestBodyExampleRefAttr);
 0593                        break;
 594                    case OpenApiParameterExampleRefAttribute parameterExampleRefAttr:
 0595                        ApplyParameterExampleRefAttribute(openApiMetadata, paramInfo, parameterExampleRefAttr);
 0596                        break;
 597                    case KestrunAnnotation ka:
 0598                        throw new InvalidOperationException($"Unhandled Kestrun annotation: {ka.GetType().Name}");
 599                }
 600            }
 601        }
 0602    }
 603
 604    #region Parameter Handlers
 605    /// <summary>
 606    /// Applies the OpenApiParameter attribute to the function's OpenAPI metadata.
 607    /// </summary>
 608    /// <param name="func">The function information.</param>
 609    /// <param name="help">The comment help information.</param>
 610    /// <param name="routeOptions">The route options to update.</param>
 611    /// <param name="metadata">The OpenAPI metadata to update.</param>
 612    /// <param name="paramInfo">The parameter information.</param>
 613    /// <param name="attribute">The OpenApiParameter attribute containing parameter details.</param>
 614    private void ApplyParameterAttribute(
 615        FunctionInfo func,
 616        CommentHelpInfo help,
 617        MapRouteOptions routeOptions,
 618        OpenAPIPathMetadata metadata,
 619        ParameterMetadata paramInfo,
 620        OpenApiParameterAttribute attribute)
 621    {
 0622        metadata.Parameters ??= [];
 0623        var parameter = new OpenApiParameter();
 0624        if (!CreateParameterFromAttribute(attribute, parameter))
 625        {
 0626            Host.Logger.Error("Error processing OpenApiParameter attribute on parameter '{paramName}' of function '{func
 0627            return;
 628        }
 629
 0630        if (!string.IsNullOrEmpty(parameter.Name) && parameter.Name != paramInfo.Name)
 631        {
 0632            throw new InvalidOperationException($"Parameter name {parameter.Name} is different from variable name: '{par
 633        }
 634
 0635        parameter.Name = paramInfo.Name;
 0636        parameter.Schema = InferPrimitiveSchema(paramInfo.ParameterType);
 637
 0638        if (parameter.Schema is OpenApiSchema schema)
 639        {
 0640            var defaultValue = func.GetDefaultParameterValue(paramInfo.Name);
 0641            if (defaultValue is not null)
 642            {
 0643                schema.Default = OpenApiJsonNodeFactory.ToNode(defaultValue);
 644            }
 645        }
 646
 0647        parameter.Description ??= help.GetParameterDescription(paramInfo.Name);
 648
 0649        foreach (var attr in paramInfo.Attributes.OfType<CmdletMetadataAttribute>())
 650        {
 0651            PowerShellAttributes.ApplyPowerShellAttribute(attr, (OpenApiSchema)parameter.Schema);
 652        }
 653
 0654        RemoveExistingParameter(metadata, paramInfo.Name);
 0655        metadata.Parameters.Add(parameter);
 0656        routeOptions.ScriptCode.Parameters.Add(new ParameterForInjectionInfo(paramInfo, parameter));
 0657    }
 658
 659    /// <summary>
 660    /// Applies the OpenApiParameterRef attribute to the function's OpenAPI metadata.
 661    /// </summary>
 662    /// <param name="help">The comment help information.</param>
 663    /// <param name="routeOptions">The route options to update.</param>
 664    /// <param name="metadata">The OpenAPI metadata to update.</param>
 665    /// <param name="paramInfo">The parameter information.</param>
 666    /// <param name="attribute">The OpenApiParameterRef attribute containing parameter reference details.</param>
 667    /// <exception cref="InvalidOperationException">If the parameter name does not match the reference name when inlinin
 668    private void ApplyParameterRefAttribute(
 669        CommentHelpInfo help,
 670        MapRouteOptions routeOptions,
 671        OpenAPIPathMetadata metadata,
 672        ParameterMetadata paramInfo,
 673        OpenApiParameterRefAttribute attribute)
 674    {
 0675        metadata.Parameters ??= [];
 0676        routeOptions.ScriptCode.Parameters ??= [];
 677
 0678        if (!TryGetParameterItem(attribute.ReferenceId, out var componentParameter, out var isInline) ||
 0679             componentParameter is null)
 680        {
 0681            throw new InvalidOperationException($"Parameter component with ID '{attribute.ReferenceId}' not found.");
 682        }
 683        IOpenApiParameter parameter;
 684
 0685        if (attribute.Inline || isInline)
 686        {
 0687            parameter = componentParameter.Clone();
 0688            if (componentParameter.Name != paramInfo.Name)
 689            {
 0690                throw new InvalidOperationException($"Parameter name {componentParameter.Name} is different from variabl
 691            }
 692
 0693            parameter.Description ??= help.GetParameterDescription(paramInfo.Name);
 694        }
 695        else
 696        {
 0697            parameter = new OpenApiParameterReference(attribute.ReferenceId);
 698        }
 699
 0700        routeOptions.ScriptCode.Parameters.Add(new ParameterForInjectionInfo(paramInfo, componentParameter));
 0701        RemoveExistingParameter(metadata, paramInfo.Name);
 0702        metadata.Parameters.Add(parameter);
 0703    }
 704
 705    private void ApplyParameterExampleRefAttribute(
 706       OpenAPIPathMetadata metadata,
 707       ParameterMetadata paramInfo,
 708       OpenApiParameterExampleRefAttribute attribute)
 709    {
 0710        var parameters = metadata.Parameters
 0711        ?? throw new InvalidOperationException(
 0712            "OpenApiParameterExampleRefAttribute must follow OpenApiParameterAttribute or OpenApiParameterRefAttribute."
 713
 0714        var parameter = parameters.FirstOrDefault(p => p.Name == paramInfo.Name)
 0715        ?? throw new InvalidOperationException(
 0716            $"OpenApiParameterExampleRefAttribute requires the parameter '{paramInfo.Name}' to be defined.");
 0717        if (parameter is OpenApiParameterReference)
 718        {
 0719            throw new InvalidOperationException(
 0720                "Cannot apply OpenApiParameterExampleRefAttribute to a parameter reference.");
 721        }
 0722        if (parameter is OpenApiParameter opp)
 723        {
 0724            opp.Examples ??= new Dictionary<string, IOpenApiExample>();
 725            // Clone or reference the example
 0726            _ = TryAddExample(opp.Examples, attribute);
 727        }
 0728    }
 729
 730    /// <summary>
 731    /// Removes any existing parameter with the specified name from the OpenAPI metadata.
 732    /// </summary>
 733    /// <param name="metadata">The OpenAPI metadata.</param>
 734    /// <param name="name">The parameter name to remove.</param>
 735    private static void RemoveExistingParameter(OpenAPIPathMetadata metadata, string name)
 736    {
 0737        if (metadata.Parameters is null)
 738        {
 0739            return;
 740        }
 741
 0742        for (var i = metadata.Parameters.Count - 1; i >= 0; i--)
 743        {
 0744            if (string.Equals(metadata.Parameters[i].Name, name, StringComparison.OrdinalIgnoreCase))
 745            {
 0746                metadata.Parameters.RemoveAt(i);
 747            }
 748        }
 0749    }
 750
 751    #endregion
 752    #region Request Body Handlers
 753    /// <summary>
 754    /// Applies the OpenApiRequestBodyRef attribute to the function's OpenAPI metadata.
 755    /// </summary>
 756    /// <param name="help">The comment help information.</param>
 757    /// <param name="routeOptions">The route options to update.</param>
 758    /// <param name="metadata">The OpenAPI metadata to update.</param>
 759    /// <param name="paramInfo">The parameter information.</param>
 760    /// <param name="attribute">The OpenApiRequestBodyRef attribute containing request body reference details.</param>
 761    private void ApplyRequestBodyRefAttribute(
 762        CommentHelpInfo help,
 763        MapRouteOptions routeOptions,
 764        OpenAPIPathMetadata metadata,
 765        ParameterMetadata paramInfo,
 766        OpenApiRequestBodyRefAttribute attribute)
 767    {
 0768        var referenceId = ResolveRequestBodyReferenceId(attribute, paramInfo);
 0769        var componentRequestBody = GetRequestBody(referenceId);
 770
 0771        metadata.RequestBody = attribute.Inline ? componentRequestBody.Clone() : new OpenApiRequestBodyReference(referen
 0772        metadata.RequestBody.Description = attribute.Description ?? help.GetParameterDescription(paramInfo.Name);
 773
 0774        routeOptions.ScriptCode.Parameters.Add(new ParameterForInjectionInfo(paramInfo, componentRequestBody));
 0775    }
 776
 777    /// <summary>
 778    /// Resolves the reference ID for the OpenApiRequestBodyRef attribute.
 779    /// </summary>
 780    /// <param name="attribute">The OpenApiRequestBodyRef attribute.</param>
 781    /// <param name="paramInfo">The parameter metadata.</param>
 782    /// <returns>The resolved reference ID.</returns>
 783    /// <exception cref="InvalidOperationException">
 784    /// Thrown when the ReferenceId is not specified and the parameter type is 'object',
 785    /// or when the ReferenceId does not match the parameter type name.
 786    /// </exception>
 787    private string ResolveRequestBodyReferenceId(OpenApiRequestBodyRefAttribute attribute, ParameterMetadata paramInfo)
 788    {
 0789        if (string.IsNullOrWhiteSpace(attribute.ReferenceId))
 790        {
 0791            if (paramInfo.ParameterType.Name is "Object" or null)
 792            {
 0793                throw new InvalidOperationException("OpenApiRequestBodyRefAttribute must have a ReferenceId specified wh
 794            }
 795
 0796            attribute.ReferenceId = paramInfo.ParameterType.Name;
 797        }
 0798        else if (paramInfo.ParameterType.Name != "Object" && attribute.ReferenceId != paramInfo.ParameterType.Name)
 799        {
 0800            return FindReferenceIdForParameter(attribute.ReferenceId, paramInfo);
 801        }
 802        // Return the reference ID as is
 0803        return attribute.ReferenceId;
 804    }
 805
 806    /// <summary>
 807    /// Finds and validates the reference ID for a request body parameter.
 808    /// </summary>
 809    /// <param name="referenceId">The reference ID to validate.</param>
 810    /// <param name="paramInfo">The parameter metadata.</param>
 811    /// <returns>The validated reference ID.</returns>
 812    /// <exception cref="InvalidOperationException">Thrown when the reference ID does not match the parameter type name.
 813    private string FindReferenceIdForParameter(string referenceId, ParameterMetadata paramInfo)
 814    {
 815        // Ensure the reference ID exists and has a schema
 0816        if (!TryGetFirstRequestBodySchema(referenceId, out var schema))
 817        {
 0818            throw new InvalidOperationException(
 0819                $"Request body component with ReferenceId '{referenceId}' was not found or does not define a schema.");
 820        }
 821        // Validate that the schema matches the parameter type
 0822        if (!IsRequestBodySchemaMatchForParameter(schema, paramInfo.ParameterType))
 823        {
 0824            throw new InvalidOperationException(
 0825                $"Schema for request body component '{referenceId}' does not match parameter type '{paramInfo.ParameterT
 826        }
 827        // return the validated reference ID
 0828        return referenceId;
 829    }
 830
 831    /// <summary>
 832    /// Attempts to retrieve the first schema defined on a request body component.
 833    /// </summary>
 834    /// <param name="referenceId">The request body component reference ID.</param>
 835    /// <param name="schema">The extracted schema when available.</param>
 836    /// <returns><see langword="true"/> if a non-null schema is found; otherwise <see langword="false"/>.</returns>
 837    private bool TryGetFirstRequestBodySchema(string referenceId, out IOpenApiSchema schema)
 838    {
 0839        schema = null!;
 840
 0841        if (!TryGetRequestBodyItem(referenceId, out var requestBody, out _))
 842        {
 0843            return false;
 844        }
 845
 0846        if (requestBody?.Content is null || requestBody.Content.Count == 0)
 847        {
 0848            return false;
 849        }
 850
 0851        schema = requestBody.Content.First().Value.Schema!;
 0852        return schema is not null;
 853    }
 854
 855    /// <summary>
 856    /// Determines whether a request-body schema matches a given CLR parameter type.
 857    /// </summary>
 858    /// <param name="schema">The schema declared for the request body.</param>
 859    /// <param name="parameterType">The CLR parameter type being validated.</param>
 860    /// <returns><see langword="true"/> if the schema matches the parameter type; otherwise <see langword="false"/>.</re
 861    private static bool IsRequestBodySchemaMatchForParameter(IOpenApiSchema schema, Type parameterType)
 862    {
 0863        if (schema is OpenApiSchemaReference schemaRef)
 864        {
 0865            return schemaRef.Reference.Id == parameterType.Name;
 866        }
 867
 0868        if (schema is OpenApiSchema inlineSchema && PrimitiveSchemaMap.TryGetValue(parameterType, out var valueType))
 869        {
 0870            var expected = valueType();
 0871            return inlineSchema.Format == expected.Format && inlineSchema.Type == expected.Type;
 872        }
 873
 0874        return false;
 875    }
 876
 877    /// <summary>
 878    /// Applies the OpenApiRequestBody attribute to the function's OpenAPI metadata.
 879    /// </summary>
 880    /// <param name="help">The comment help information.</param>
 881    /// <param name="routeOptions">The route options to update.</param>
 882    /// <param name="metadata">The OpenAPI metadata to update.</param>
 883    /// <param name="paramInfo">The parameter information.</param>
 884    /// <param name="attribute">The OpenApiRequestBody attribute containing request body details.</param>
 885    private void ApplyRequestBodyAttribute(
 886        CommentHelpInfo help,
 887        MapRouteOptions routeOptions,
 888        OpenAPIPathMetadata metadata,
 889        ParameterMetadata paramInfo,
 890        OpenApiRequestBodyAttribute attribute)
 891    {
 0892        var requestBodyPreferred = ComponentRequestBodiesExists(paramInfo.ParameterType.Name);
 893
 0894        if (requestBodyPreferred)
 895        {
 0896            ApplyPreferredRequestBody(help, routeOptions, metadata, paramInfo, attribute);
 0897            return;
 898        }
 899
 0900        var requestBody = new OpenApiRequestBody();
 0901        var schema = InferPrimitiveSchema(type: paramInfo.ParameterType, inline: attribute.Inline);
 902
 0903        if (!CreateRequestBodyFromAttribute(attribute, requestBody, schema))
 904        {
 0905            return;
 906        }
 907
 0908        metadata.RequestBody = requestBody;
 0909        metadata.RequestBody.Description ??= help.GetParameterDescription(paramInfo.Name);
 0910        routeOptions.ScriptCode.Parameters.Add(new ParameterForInjectionInfo(paramInfo, requestBody));
 0911    }
 912
 913    /// <summary>
 914    /// Applies the OpenApiRequestBodyExampleRef attribute to the function's OpenAPI metadata.
 915    /// </summary>
 916    /// <param name="metadata">The OpenAPI metadata to update.</param>
 917    /// <param name="attribute">The OpenApiRequestBodyExampleRef attribute containing example reference details.</param>
 918    /// <exception cref="InvalidOperationException">Thrown when the request body or its content is not properly defined.
 919    private void ApplyRequestBodyExampleRefAttribute(
 920       OpenAPIPathMetadata metadata,
 921       OpenApiRequestBodyExampleRefAttribute attribute)
 922    {
 0923        var requestBody = metadata.RequestBody
 0924        ?? throw new InvalidOperationException(
 0925            "OpenApiRequestBodyExampleRefAttribute must follow OpenApiRequestBodyAttribute or OpenApiRequestBodyRefAttri
 926
 0927        if (requestBody.Content is null)
 928        {
 0929            throw new InvalidOperationException(
 0930                "OpenApiRequestBodyExampleRefAttribute requires the request body to have content defined.");
 931        }
 932
 0933        foreach (var oamt in requestBody.Content.Values.OfType<OpenApiMediaType>())
 934        {
 0935            oamt.Examples ??= new Dictionary<string, IOpenApiExample>();
 0936            _ = TryAddExample(oamt.Examples, attribute);
 937        }
 0938    }
 939
 940    /// <summary>
 941    /// Applies the preferred request body from components to the function's OpenAPI metadata.
 942    /// </summary>
 943    /// <param name="help">The comment help information.</param>
 944    /// <param name="routeOptions">The route options to update.</param>
 945    /// <param name="metadata">The OpenAPI metadata to update.</param>
 946    /// <param name="paramInfo">The parameter information.</param>
 947    /// <param name="attribute">The OpenApiRequestBody attribute containing request body details.</param>
 948    private void ApplyPreferredRequestBody(
 949        CommentHelpInfo help,
 950        MapRouteOptions routeOptions,
 951        OpenAPIPathMetadata metadata,
 952        ParameterMetadata paramInfo,
 953        OpenApiRequestBodyAttribute attribute)
 954    {
 0955        var componentRequestBody = GetRequestBody(paramInfo.ParameterType.Name);
 956
 0957        metadata.RequestBody = attribute.Inline
 0958            ? componentRequestBody.Clone()
 0959            : new OpenApiRequestBodyReference(paramInfo.ParameterType.Name);
 960
 0961        metadata.RequestBody.Description ??= help.GetParameterDescription(paramInfo.Name);
 0962        routeOptions.ScriptCode.Parameters.Add(new ParameterForInjectionInfo(paramInfo, componentRequestBody));
 0963    }
 964    #endregion
 965
 966    /// <summary>
 967    /// Ensures that the OpenAPIPathMetadata has default responses defined.
 968    /// </summary>
 969    /// <param name="metadata">The OpenAPI metadata to update.</param>
 970    private static void EnsureDefaultResponses(OpenAPIPathMetadata metadata)
 971    {
 0972        metadata.Responses ??= [];
 0973        if (metadata.Responses.Count > 0)
 974        {
 0975            return;
 976        }
 0977        if (metadata.IsOpenApiCallback)
 978        {
 0979            metadata.Responses.Add("204", new OpenApiResponse { Description = "Accepted" });
 980        }
 981        else
 982        {
 0983            metadata.Responses.Add("200", new OpenApiResponse { Description = "Ok" });
 0984            metadata.Responses.Add("default", new OpenApiResponse { Description = "Unexpected error" });
 985        }
 0986    }
 987
 988    /// <summary>
 989    /// Finalizes the route options by adding OpenAPI metadata and configuring defaults.
 990    /// </summary>
 991    /// <param name="func">The function information.</param>
 992    /// <param name="sb">The script block.</param>
 993    /// <param name="metadata">The OpenAPI metadata.</param>
 994    /// <param name="routeOptions">The route options to update.</param>
 995    /// <param name="parsedVerb">The HTTP verb parsed from the function.</param>
 996    private void FinalizeRouteOptions(
 997        FunctionInfo func,
 998        ScriptBlock sb,
 999        OpenAPIPathMetadata metadata,
 1000        MapRouteOptions routeOptions,
 1001        HttpVerb parsedVerb)
 1002    {
 01003        metadata.DocumentId ??= Host.OpenApiDocumentIds;
 01004        var documentIds = metadata.DocumentId;
 01005        if (metadata.IsOpenApiPath)
 1006        {
 01007            FinalizePathRouteOptions(func, sb, metadata, routeOptions, parsedVerb);
 01008            return;
 1009        }
 1010
 01011        if (metadata.IsOpenApiWebhook)
 1012        {
 01013            RegisterWebhook(func, sb, metadata, parsedVerb, documentIds);
 01014            return;
 1015        }
 1016
 01017        if (metadata.IsOpenApiCallback)
 1018        {
 01019            RegisterCallback(func, sb, metadata, parsedVerb, documentIds);
 1020        }
 01021    }
 1022
 1023    /// <summary>
 1024    /// Finalizes the route options for a standard OpenAPI path.
 1025    /// </summary>
 1026    /// <param name="func">The function information.</param>
 1027    /// <param name="sb">The script block.</param>
 1028    /// <param name="metadata">The OpenAPI metadata.</param>
 1029    /// <param name="routeOptions">The route options to update.</param>
 1030    /// <param name="parsedVerb">The HTTP verb parsed from the function.</param>
 1031    private void FinalizePathRouteOptions(
 1032        FunctionInfo func,
 1033        ScriptBlock sb,
 1034        OpenAPIPathMetadata metadata,
 1035        MapRouteOptions routeOptions,
 1036        HttpVerb parsedVerb)
 1037    {
 01038        routeOptions.OpenAPI.Add(parsedVerb, metadata);
 1039
 01040        if (string.IsNullOrWhiteSpace(routeOptions.Pattern))
 1041        {
 01042            routeOptions.Pattern = "/" + func.Name;
 1043        }
 1044
 01045        if (!string.IsNullOrWhiteSpace(metadata.CorsPolicy))
 1046        {
 01047            routeOptions.CorsPolicy = metadata.CorsPolicy;
 1048        }
 1049
 01050        routeOptions.ScriptCode.ScriptBlock = sb;
 01051        routeOptions.DefaultResponseContentType = "application/json";
 01052        _ = Host.AddMapRoute(routeOptions);
 01053    }
 1054    /// <summary>
 1055    /// Registers a webhook in the OpenAPI document descriptors.
 1056    /// </summary>
 1057    /// <param name="func">The function information.</param>
 1058    /// <param name="sb">The script block.</param>
 1059    /// <param name="metadata">The OpenAPI path metadata.</param>
 1060    /// <param name="parsedVerb">The HTTP verb parsed from the function.</param>
 1061    /// <param name="documentIds">The collection of OpenAPI document IDs.</param>
 1062    private void RegisterWebhook(FunctionInfo func, ScriptBlock sb, OpenAPIPathMetadata metadata, HttpVerb parsedVerb, I
 1063    {
 01064        EnsureParamOnlyScriptBlock(func, sb, kind: "webhook");
 01065        foreach (var docId in documentIds)
 1066        {
 01067            var docdesc = GetDocDescriptorOrThrow(docId, attributeName: "OpenApiWebhook");
 01068            _ = docdesc.WebHook.TryAdd((metadata.Pattern, parsedVerb), metadata);
 1069        }
 01070    }
 1071    /// <summary>
 1072    /// Registers a callback in the OpenAPI document descriptors.
 1073    /// </summary>
 1074    /// <param name="func">The function information.</param>
 1075    /// <param name="sb">The script block.</param>
 1076    /// <param name="metadata">The OpenAPI path metadata.</param>
 1077    /// <param name="parsedVerb">The HTTP verb parsed from the function.</param>
 1078    /// <param name="documentIds">The collection of OpenAPI document IDs.</param>
 1079    private void RegisterCallback(FunctionInfo func, ScriptBlock sb, OpenAPIPathMetadata metadata, HttpVerb parsedVerb, 
 1080    {
 01081        EnsureParamOnlyScriptBlock(func, sb, kind: "callback");
 01082        foreach (var docId in documentIds)
 1083        {
 01084            var docdesc = GetDocDescriptorOrThrow(docId, attributeName: "OpenApiCallback");
 01085            _ = docdesc.Callbacks.TryAdd((metadata.Pattern, parsedVerb), metadata);
 1086        }
 01087    }
 1088
 1089    /// <summary>
 1090    /// Retrieves the OpenApiDocDescriptor for the specified document ID or throws an exception if not found.
 1091    /// </summary>
 1092    /// <param name="docId">The document ID to look up.</param>
 1093    /// <param name="attributeName">The name of the attribute requesting the document.</param>
 1094    /// <returns>The corresponding OpenApiDocDescriptor.</returns>
 1095    private OpenApiDocDescriptor GetDocDescriptorOrThrow(string docId, string attributeName)
 1096    {
 01097        return Host.OpenApiDocumentDescriptor.TryGetValue(docId, out var docdesc)
 01098            ? docdesc
 01099            : throw new InvalidOperationException($"The OpenAPI document ID '{docId}' specified in the {attributeName} a
 1100    }
 1101
 1102    /// <summary>
 1103    /// Ensures that the ScriptBlock contains only a param() block with no executable statements.
 1104    /// </summary>
 1105    /// <param name="func">The function information.</param>
 1106    /// <param name="sb">The ScriptBlock to validate.</param>
 1107    /// <param name="kind">The kind of function (e.g., "webhook" or "callback").</param>
 1108    /// <exception cref="InvalidOperationException">Thrown if the ScriptBlock contains executable statements other than 
 1109    private static void EnsureParamOnlyScriptBlock(FunctionInfo func, ScriptBlock sb, string kind)
 1110    {
 01111        if (!PsScriptBlockValidation.IsParamLast(sb))
 1112        {
 01113            throw new InvalidOperationException($"The ScriptBlock for {kind} function '{func.Name}' must contain only a 
 1114        }
 01115    }
 1116
 1117    /// <summary>
 1118    /// Creates a request body from the given attribute.
 1119    /// </summary>
 1120    /// <param name="attribute">The attribute containing request body information.</param>
 1121    /// <param name="requestBody">The OpenApiRequestBody object to populate.</param>
 1122    /// <param name="schema">The schema to associate with the request body.</param>
 1123    /// <returns>True if the request body was created successfully; otherwise, false.</returns>
 1124    private static bool CreateRequestBodyFromAttribute(KestrunAnnotation attribute, OpenApiRequestBody requestBody, IOpe
 1125    {
 1126        switch (attribute)
 1127        {
 1128            case OpenApiRequestBodyAttribute request:
 01129                requestBody.Description = request.Description;
 01130                requestBody.Required = request.Required;
 1131                // Content
 01132                requestBody.Content ??= new Dictionary<string, IOpenApiMediaType>(StringComparer.Ordinal);
 01133                var mediaType = new OpenApiMediaType();
 1134                // Example
 01135                if (request.Example is not null)
 1136                {
 01137                    mediaType.Example = OpenApiJsonNodeFactory.ToNode(request.Example);
 1138                }
 1139                // Schema
 01140                mediaType.Schema = schema;
 01141                foreach (var contentType in request.ContentType)
 1142                {
 01143                    requestBody.Content[contentType] = mediaType;
 1144                }
 01145                return true;
 1146            default:
 01147                return false;
 1148        }
 1149    }
 1150}

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

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

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

#LineLine coverage
 1using System.Reflection;
 2using System.Text.Json.Nodes;
 3using Microsoft.OpenApi;
 4
 5namespace Kestrun.OpenApi;
 6
 7public partial class OpenApiDocDescriptor
 8{
 9    /// <summary>
 10    /// Merges OpenApiProperties with OpenApiXmlAttribute if present.
 11    /// </summary>
 12    /// <param name="prop">The property to extract attributes from.</param>
 13    /// <returns>Merged OpenApiProperties with XML metadata applied.</returns>
 14    private static OpenApiProperties? MergeXmlAttributes(PropertyInfo prop)
 15    {
 16016        var properties = prop.GetCustomAttribute<OpenApiProperties>();
 16017        var xmlAttr = prop.GetCustomAttribute<OpenApiXmlAttribute>();
 18
 16019        if (xmlAttr == null)
 20        {
 15221            return properties;
 22        }
 23
 24        // If no OpenApiProperties, create a new one to hold XML data
 825        properties ??= new OpenApiPropertyAttribute();
 26
 27        // Merge XML attribute properties into OpenApiProperties
 828        if (!string.IsNullOrWhiteSpace(xmlAttr.Name))
 29        {
 230            properties.XmlName = xmlAttr.Name;
 31        }
 32
 833        if (!string.IsNullOrWhiteSpace(xmlAttr.Namespace))
 34        {
 235            properties.XmlNamespace = xmlAttr.Namespace;
 36        }
 37
 838        if (!string.IsNullOrWhiteSpace(xmlAttr.Prefix))
 39        {
 140            properties.XmlPrefix = xmlAttr.Prefix;
 41        }
 42
 843        if (xmlAttr.Attribute)
 44        {
 245            properties.XmlAttribute = true;
 46        }
 47
 848        if (xmlAttr.Wrapped)
 49        {
 250            properties.XmlWrapped = true;
 51        }
 52
 853        return properties;
 54    }
 55
 56    /// <summary>
 57    /// Builds and adds the schema for a given type to the document components.
 58    /// </summary>
 59    /// <param name="t">The type to build the schema for.</param>
 60    /// <param name="built">The set of already built types to avoid recursion.</param>
 61    private void BuildSchema(Type t, HashSet<Type>? built = null)
 62    {
 1563        if (Document.Components is not null && Document.Components.Schemas is not null)
 64        {
 1565            if (!ComponentSchemasExists(t.Name))
 66            {
 1567                if (!PrimitiveSchemaMap.ContainsKey(t))
 68                {
 1569                    Document.Components.Schemas[t.Name] = BuildSchemaForType(t, built);
 70                }
 71            }
 72        }
 1573    }
 74
 75    /// <summary>
 76    /// Builds the schema for a property, handling nullable types and complex types.
 77    /// </summary>
 78    /// <param name="p">The property info.</param>
 79    /// <param name="built">The set of already built types to avoid recursion.</param>
 80    /// <returns>The constructed OpenAPI schema for the property.</returns>
 81    private IOpenApiSchema BuildPropertySchema(PropertyInfo p, HashSet<Type> built)
 82    {
 14183        var pt = p.PropertyType;
 14184        var allowNull = false;
 14185        var underlying = Nullable.GetUnderlyingType(pt);
 14186        if (underlying != null)
 87        {
 888            allowNull = true;
 889            pt = underlying;
 90        }
 91        IOpenApiSchema schema;
 92#pragma warning disable IDE0045
 93        // Convert to conditional expression
 14194        if (PrimitiveSchemaMap.TryGetValue(pt, out var getSchema))
 95        {
 12696            schema = getSchema();
 97        }
 1598        else if (pt.IsArray)
 99        {
 6100            schema = BuildArraySchema(pt, p, built);
 101        }
 102        else
 103        {
 104            // Treat enums and complex types the same: register as component and reference
 9105            schema = BuildComplexTypeSchema(pt, p, built);
 106        }
 107#pragma warning restore IDE0045
 108        // Convert to conditional expression
 109        // Apply nullable flag if needed
 141110        if (allowNull)
 111        {
 8112            if (schema is OpenApiSchema s)
 113            {
 114                // For inline schemas, add null type directly
 7115                s.Type |= JsonSchemaType.Null;
 116            }
 1117            else if (schema is OpenApiSchemaReference refSchema)
 118            {
 1119                var modifiedRefSchema = refSchema.Clone();
 1120                modifiedRefSchema.Description = null; // clear description to avoid duplication
 121                // For $ref schemas (enums/complex types), wrap in anyOf with null
 1122                schema = new OpenApiSchema
 1123                {
 1124                    AnyOf =
 1125                    [
 1126                        modifiedRefSchema,
 1127                        new OpenApiSchema { Type = JsonSchemaType.Null }
 1128                    ]
 1129                };
 130            }
 131        }
 141132        ApplySchemaAttr(MergeXmlAttributes(p), schema);
 141133        PowerShellAttributes.ApplyPowerShellAttributes(p, schema);
 141134        return schema;
 135    }
 136
 137    /// <summary>
 138    /// Builds the schema for a complex type property.
 139    /// </summary>
 140    /// <param name="pt">The property type.</param>
 141    /// <param name="p">The property info.</param>
 142    /// <param name="built">The set of already built types to avoid recursion.</param>
 143    /// <returns>The constructed OpenAPI schema for the complex type property.</returns>
 144    private OpenApiSchemaReference BuildComplexTypeSchema(Type pt, PropertyInfo p, HashSet<Type> built)
 145    {
 10146        BuildSchema(pt, built); // ensure component exists
 10147        var refSchema = new OpenApiSchemaReference(pt.Name);
 10148        ApplySchemaAttr(MergeXmlAttributes(p), refSchema);
 10149        return refSchema;
 150    }
 151
 152    /// <summary>
 153    /// Builds the schema for an enum property.
 154    /// </summary>
 155    /// <param name="pt">The property type.</param>
 156    /// <param name="p">The property info.</param>
 157    /// <returns>The constructed OpenAPI schema for the enum property.</returns>
 158    private static OpenApiSchema BuildEnumSchema(Type pt, PropertyInfo p)
 159    {
 0160        var s = new OpenApiSchema
 0161        {
 0162            Type = JsonSchemaType.String,
 0163            Enum = [.. pt.GetEnumNames().Select(n => (JsonNode)n)]
 0164        };
 0165        var attrs = p.GetCustomAttributes<OpenApiPropertyAttribute>(inherit: false).ToArray();
 0166        var a = MergeSchemaAttributes(attrs);
 0167        ApplySchemaAttr(MergeXmlAttributes(p) ?? a, s);
 0168        PowerShellAttributes.ApplyPowerShellAttributes(p, s);
 0169        return s;
 170    }
 171
 172    /// <summary>
 173    /// Builds the schema for an array property.
 174    /// </summary>
 175    /// <param name="pt">The property type.</param>
 176    /// <param name="p">The property info.</param>
 177    /// <param name="built">The set of already built types to avoid recursion.</param>
 178    /// <returns>The constructed OpenAPI schema for the array property.</returns>
 179    private OpenApiSchema BuildArraySchema(Type pt, PropertyInfo p, HashSet<Type> built)
 180    {
 9181        var item = pt.GetElementType()!;
 182        IOpenApiSchema itemSchema;
 183
 9184        if (PrimitiveSchemaMap.TryGetValue(item, out var getSchema))
 185        {
 7186            itemSchema = getSchema();
 187        }
 188        else
 189        {
 190            // Treat enums and complex types the same: register as component and reference
 2191            BuildSchema(item, built); // ensure component exists
 2192            itemSchema = new OpenApiSchemaReference(item.Name);
 193        }
 9194        var s = new OpenApiSchema
 9195        {
 9196            Type = JsonSchemaType.Array,
 9197            Items = itemSchema
 9198        };
 9199        ApplySchemaAttr(MergeXmlAttributes(p), s);
 9200        PowerShellAttributes.ApplyPowerShellAttributes(p, s);
 9201        return s;
 202    }
 203
 204    /// <summary>
 205    /// Builds the schema for a primitive type property.
 206    /// </summary>
 207    /// <param name="pt">The property type.</param>
 208    /// <param name="p">The property info.</param>
 209    /// <returns>The constructed OpenAPI schema for the primitive type property.</returns>
 210    private IOpenApiSchema BuildPrimitiveSchema(Type pt, PropertyInfo p)
 211    {
 0212        var prim = InferPrimitiveSchema(pt);
 0213        ApplySchemaAttr(MergeXmlAttributes(p), prim);
 0214        PowerShellAttributes.ApplyPowerShellAttributes(p, prim);
 0215        return prim;
 216    }
 217
 218    /// <summary>
 219    /// Gets or creates an OpenAPI schema item in either inline or document components.
 220    /// </summary>
 221    /// <param name="schemaName">The name of the schema.</param>
 222    /// <param name="inline">Whether to use inline components or document components.</param>
 223    /// <returns>The OpenApiSchema item.</returns>
 224    private OpenApiSchema GetOrCreateSchemaItem(string schemaName, bool inline)
 225    {
 226        IDictionary<string, IOpenApiSchema> schema;
 227        // Determine whether to use inline components or document components
 0228        if (inline)
 229        {
 230            // Use inline components
 0231            InlineComponents.Schemas ??= new Dictionary<string, IOpenApiSchema>(StringComparer.Ordinal);
 0232            schema = InlineComponents.Schemas;
 233        }
 234        else
 235        {
 236            // Use document components
 0237            Document.Components ??= new OpenApiComponents();
 0238            Document.Components.Schemas ??= new Dictionary<string, IOpenApiSchema>(StringComparer.Ordinal);
 0239            schema = Document.Components.Schemas;
 240        }
 241        // Retrieve or create the request body item
 0242        if (!schema.TryGetValue(schemaName, out var openApiSchemaItem) || openApiSchemaItem is null)
 243        {
 244            // Create a new OpenApiSchema if it doesn't exist
 0245            openApiSchemaItem = new OpenApiSchema();
 0246            schema[schemaName] = openApiSchemaItem;
 247        }
 248        // return the request body item
 0249        return (OpenApiSchema)openApiSchemaItem;
 250    }
 251
 252    /// <summary>
 253    /// Tries to get a schema by name from either inline or document components.
 254    /// </summary>
 255    /// <param name="schemaName">The name of the schema to retrieve.</param>
 256    /// <param name="schema">The retrieved schema if found; otherwise, null.</param>
 257    /// <param name="isInline">Indicates whether the schema was found in inline components.</param>
 258    /// <returns>True if the schema was found; otherwise, false.</returns>
 259    private bool TryGetSchemaItem(string schemaName, out OpenApiSchema? schema, out bool isInline)
 260    {
 0261        if (TryGetInline(name: schemaName, kind: OpenApiComponentKind.Schemas, out schema))
 262        {
 0263            isInline = true;
 0264            return true;
 265        }
 0266        else if (TryGetComponent(name: schemaName, kind: OpenApiComponentKind.Schemas, out schema))
 267        {
 0268            isInline = false;
 0269            return true;
 270        }
 0271        schema = null;
 0272        isInline = false;
 0273        return false;
 274    }
 275
 276    /// <summary>
 277    /// Tries to get a schema by name from either inline or document components.
 278    /// </summary>
 279    /// <param name="schemaName">The name of the schema to retrieve.</param>
 280    /// <param name="schema">The retrieved schema if found; otherwise, null.</param>
 281    /// <returns>True if the schema was found; otherwise, false.</returns>
 282    private bool TryGetSchemaItem(string schemaName, out OpenApiSchema? schema) =>
 0283    TryGetSchemaItem(schemaName, out schema, out _);
 284}
 285

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

#LineLine coverage
 1using Kestrun.Hosting.Options;
 2using Microsoft.OpenApi;
 3
 4namespace Kestrun.OpenApi;
 5
 6/// <summary>
 7/// Helper methods for accessing OpenAPI document components.
 8/// </summary>
 9public partial class OpenApiDocDescriptor
 10{
 11    /// <summary>
 12    /// Builds an OpenApiOperation from OpenAPIPathMetadata.
 13    /// </summary>
 14    /// <param name="meta">The OpenAPIPathMetadata to build from.</param>
 15    /// <returns>The constructed OpenApiOperation.</returns>
 16    private OpenApiOperation BuildOperationFromMetadata(OpenAPIPathMetadata meta)
 17    {
 218        var op = new OpenApiOperation
 219        {
 220            OperationId = string.IsNullOrWhiteSpace(meta.OperationId) ? null : meta.OperationId,
 221            Summary = string.IsNullOrWhiteSpace(meta.Summary) ? null : meta.Summary,
 222            Description = string.IsNullOrWhiteSpace(meta.Description) ? null : meta.Description,
 223            Deprecated = meta.Deprecated,
 224            ExternalDocs = meta.ExternalDocs,
 225            RequestBody = meta.RequestBody,
 226            Responses = meta.Responses ?? new OpenApiResponses { ["200"] = new OpenApiResponse { Description = "Success"
 227        };
 28
 229        ApplyTags(op, meta);
 230        ApplyServers(op, meta);
 231        ApplyParameters(op, meta);
 232        ApplyCallbacks(op, meta);
 233        ApplySecurity(op, meta);
 234        ApplyExtensions(op, meta);
 235        return op;
 36    }
 37    /// <summary>
 38    /// Applies extension information from metadata to the OpenApiOperation.
 39    /// </summary>
 40    /// <param name="op">The OpenApiOperation to modify.</param>
 41    /// <param name="meta">The OpenAPIPathMetadata containing extension information.</param>
 42    private static void ApplyExtensions(OpenApiOperation op, OpenAPIPathMetadata meta)
 43    {
 244        if (meta.Extensions is null || meta.Extensions.Count == 0)
 45        {
 246            return;
 47        }
 048        op.Extensions = meta.Extensions;
 049    }
 50
 51    /// <summary>
 52    /// Applies tags from metadata to the OpenApiOperation.
 53    /// </summary>
 54    /// <param name="op">The OpenApiOperation to modify.</param>
 55    /// <param name="meta">The OpenAPIPathMetadata containing tags.</param>
 56    private static void ApplyTags(OpenApiOperation op, OpenAPIPathMetadata meta)
 57    {
 258        if (meta.Tags.Count > 0)
 59        {
 260            op.Tags = new HashSet<OpenApiTagReference>();
 1061            foreach (var t in meta.Tags ?? [])
 62            {
 363                _ = op.Tags.Add(new OpenApiTagReference(t));
 64            }
 65        }
 266    }
 67
 68    /// <summary>
 69    /// Applies server information from metadata to the OpenApiOperation.
 70    /// </summary>
 71    /// <param name="op">The OpenApiOperation to modify.</param>
 72    /// <param name="meta">The OpenAPIPathMetadata containing server information.</param>
 73    private void ApplyServers(OpenApiOperation op, OpenAPIPathMetadata meta)
 74    {
 75        try
 76        {
 277            if (meta.Servers is { Count: > 0 })
 78            {
 079                dynamic d = op;
 080                if (d.Servers == null) { d.Servers = new List<OpenApiServer>(); }
 081                foreach (var s in meta.Servers) { d.Servers.Add(s); }
 82            }
 283        }
 084        catch (Exception ex)
 85        {
 086            Host.Logger.Warning(ex, "Failed to set operation-level servers for OpenAPI operation {OperationId}", op.Oper
 087        }
 288    }
 89
 90    /// <summary>
 91    /// Applies parameter information from metadata to the OpenApiOperation.
 92    /// </summary>
 93    /// <param name="op">The OpenApiOperation to modify.</param>
 94    /// <param name="meta">The OpenAPIPathMetadata containing parameter information.</param>
 95    private void ApplyParameters(OpenApiOperation op, OpenAPIPathMetadata meta)
 96    {
 97        try
 98        {
 299            if (meta.Parameters is { Count: > 0 })
 100            {
 0101                dynamic d = op;
 0102                if (d.Parameters == null) { d.Parameters = new List<IOpenApiParameter>(); }
 0103                foreach (var p in meta.Parameters) { d.Parameters.Add(p); }
 104            }
 2105        }
 0106        catch { Host.Logger?.Warning("Failed to set operation-level parameters for OpenAPI operation {OperationId}", op.
 2107    }
 108
 109    /// <summary>
 110    /// Applies security requirement information from metadata to the OpenApiOperation.
 111    /// </summary>
 112    /// <param name="op">The OpenApiOperation to modify.</param>
 113    /// <param name="meta">The OpenAPIPathMetadata containing security requirement information.</param>
 114    private void ApplySecurity(OpenApiOperation op, OpenAPIPathMetadata meta)
 115    {
 2116        if (meta.SecuritySchemes is not null && meta.SecuritySchemes.Count != 0)
 117        {
 0118            op.Security ??= [];
 119
 0120            var seen = new HashSet<string>(StringComparer.Ordinal);
 121
 0122            foreach (var schemeName in meta.SecuritySchemes
 0123                         .SelectMany(d => d.Keys)
 0124                         .Distinct())
 125            {
 0126                if (!seen.Add(schemeName))
 127                {
 128                    continue;
 129                }
 130                // Gather scopes for this scheme
 0131                var scopesForScheme = meta.SecuritySchemes
 0132                    .SelectMany(dict => dict)
 0133                    .Where(kv => kv.Key == schemeName)
 0134                    .SelectMany(kv => kv.Value)
 0135                    .Distinct()
 0136                    .ToList();
 137                // Build requirement
 0138                var requirement = new OpenApiSecurityRequirement
 0139                {
 0140                    [new OpenApiSecuritySchemeReference(schemeName, Document)] = scopesForScheme
 0141                };
 142
 0143                op.Security.Add(requirement);
 144            }
 145        }
 2146        else if (meta.SecuritySchemes is not null && meta.SecuritySchemes.Count == 0)
 147        {
 148            // Explicitly anonymous for this operation (overrides Document.Security)
 0149            op.Security = [];
 150        }
 2151    }
 152}

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

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

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

#LineLine coverage
 1using Microsoft.OpenApi;
 2
 3namespace Kestrun.OpenApi;
 4
 5/// <summary>
 6/// Generates OpenAPI v2 (Swagger) documents from C# types decorated with OpenApiSchema attributes.
 7/// </summary>
 8public partial class OpenApiDocDescriptor
 9{
 10    /// <summary>
 11    /// Gets the name override from an attribute, if present.
 12    /// </summary>
 13    /// <param name="attr">The attribute to inspect.</param>
 14    /// <returns>The name override, if present; otherwise, null.</returns>
 15    private static string? GetKeyOverride(object attr)
 16    {
 017        var t = attr.GetType();
 018        return t.GetProperty("Key")?.GetValue(attr) as string;
 19    }
 20
 21    /// <summary>
 22    /// Creates or modifies an OpenApiResponse based on the provided attribute.
 23    /// </summary>
 24    /// <param name="attr"> The attribute to apply.</param>
 25    /// <param name="response"> The OpenApiResponse to modify.</param>
 26    /// <param name="iSchema"> An optional schema to apply.</param>
 27    /// <returns>True if the response was modified; otherwise, false.</returns>
 28    private bool CreateResponseFromAttribute(object attr, OpenApiResponse? response, IOpenApiSchema? iSchema = null)
 29    {
 030        ArgumentNullException.ThrowIfNull(attr);
 031        ArgumentNullException.ThrowIfNull(response);
 32
 033        return attr switch
 034        {
 035            OpenApiResponseAttribute resp => ApplyResponseAttribute(resp, response, iSchema),
 036            OpenApiResponseHeaderRefAttribute href => ApplyHeaderRefAttribute(href, response),
 037            OpenApiResponseHeaderAttribute head => ApplyHeaderAttribute(head, response),
 038            OpenApiResponseLinkRefAttribute lref => ApplyLinkRefAttribute(lref, response),
 039            OpenApiExampleRefAttribute exRef => ApplyExampleRefAttribute(exRef, response),
 040            OpenApiResponseExampleRefAttribute exRef => ApplyExampleRefAttribute(exRef, response),
 041            _ => false
 042        };
 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    {
 055        ApplyDescription(resp, response);
 056        schema = ResolveResponseSchema(resp, schema);
 057        ApplySchemaToContentTypes(resp, response, schema);
 058        return true;
 59    }
 60
 61    private static void ApplyDescription(OpenApiResponseAttribute resp, OpenApiResponse response)
 62    {
 063        if (!string.IsNullOrEmpty(resp.Description))
 64        {
 065            response.Description = resp.Description;
 66        }
 067    }
 68
 69    private IOpenApiSchema? ResolveResponseSchema(OpenApiResponseAttribute resp, IOpenApiSchema? propertySchema)
 70    {
 71        // 1) Type-based schema
 072        if (resp.Schema is not null)
 73        {
 074            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    {
 087        if (schema is not null && resp.ContentType is { Length: > 0 })
 88        {
 089            foreach (var ct in resp.ContentType)
 90            {
 091                var media = GetOrAddMediaType(response, ct);
 092                if (media is OpenApiMediaType mediaType)
 93                {
 094                    if (resp.SchemaItem != null)
 95                    {
 096                        mediaType.ItemSchema = schema;
 97                    }
 98                    else
 99                    {
 0100                        mediaType.Schema = schema;
 101                    }
 102                }
 103            }
 104        }
 0105    }
 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    {
 0209        resp.Content ??= new Dictionary<string, IOpenApiMediaType>(StringComparer.Ordinal);
 0210        if (!resp.Content.TryGetValue(contentType, out var media))
 211        {
 0212            media = resp.Content[contentType] = new OpenApiMediaType();
 213        }
 214
 0215        return media;
 216    }
 217
 218    private OpenApiSchema CloneSchemaOrThrow(string refId)
 219    {
 0220        if (Document.Components?.Schemas is { } schemas &&
 0221            schemas.TryGetValue(refId, out var schema))
 222        {
 223            // your existing clone semantics
 0224            return (OpenApiSchema)schema.Clone();
 225        }
 226
 0227        throw new InvalidOperationException(
 0228            $"Schema reference '{refId}' cannot be embedded because it was not found in components.");
 229    }
 230
 231    private void ProcessResponseExampleRef(string name, OpenApiResponseExampleRefAttribute attribute)
 232    {
 0233        if (attribute.StatusCode != "default")
 234        {
 0235            throw new InvalidOperationException("Response example references cannot have a status code.");
 236        }
 0237        if (attribute.Key is null)
 238        {
 0239            throw new InvalidOperationException("Response example attributes must have a Key specified to define the exa
 240        }
 0241        if (!TryGetResponseItem(name, out var response))
 242        {
 0243            throw new InvalidOperationException($"response '{name}' not found when trying to add to response.");
 244        }
 0245        _ = CreateResponseFromAttribute(attribute, response);
 0246    }
 247
 248    private void ProcessResponseLinkRef(string name, OpenApiResponseLinkRefAttribute attribute)
 249    {
 0250        if (attribute.StatusCode != "default")
 251        {
 0252            throw new InvalidOperationException("Response link references cannot have a status code.");
 253        }
 0254        if (attribute.Key is null)
 255        {
 0256            throw new InvalidOperationException("Response link attributes must have a Key specified to define the link n
 257        }
 0258        if (!TryGetResponseItem(name, out var response))
 259        {
 0260            throw new InvalidOperationException($"response '{name}' not found when trying to add to response.");
 261        }
 0262        _ = CreateResponseFromAttribute(attribute, response);
 0263    }
 264
 265    private void ProcessResponseHeaderRef(string name, OpenApiResponseHeaderRefAttribute attribute)
 266    {
 0267        if (attribute.StatusCode != "default")
 268        {
 0269            throw new InvalidOperationException("Response header references cannot have a status code.");
 270        }
 0271        if (attribute.Key is null)
 272        {
 0273            throw new InvalidOperationException("Response header attributes must have a Key specified to define the head
 274        }
 0275        if (!TryGetResponseItem(name, out var response))
 276        {
 0277            throw new InvalidOperationException($"response '{name}' not found when trying to add to response.");
 278        }
 0279        _ = CreateResponseFromAttribute(attribute, response);
 0280    }
 281
 282    private void ProcessResponseComponent(
 283      OpenApiComponentAnnotationScanner.AnnotatedVariable variable,
 284      OpenApiResponseComponentAttribute responseDescriptor)
 285    {
 0286        var response = GetOrCreateResponseItem(variable.Name, responseDescriptor.Inline);
 287
 0288        ApplyResponseCommonFields(response, responseDescriptor);
 289
 0290        TryApplyVariableTypeSchema(response, variable, responseDescriptor);
 0291    }
 292
 293    #region Response Item Helpers
 294
 295    private OpenApiResponse GetOrCreateResponseItem(string responseName, bool inline)
 296    {
 297        IDictionary<string, IOpenApiResponse> responses;
 298        // Determine whether to use inline components or document components
 0299        if (inline)
 300        {
 301            // Use inline components
 0302            InlineComponents.Responses ??= new Dictionary<string, IOpenApiResponse>(StringComparer.Ordinal);
 0303            responses = InlineComponents.Responses;
 304        }
 305        else
 306        {
 307            // Use document components
 0308            Document.Components ??= new OpenApiComponents();
 0309            Document.Components.Responses ??= new Dictionary<string, IOpenApiResponse>(StringComparer.Ordinal);
 0310            responses = Document.Components.Responses;
 311        }
 312        // Retrieve or create the response item
 0313        if (!responses.TryGetValue(responseName, out var responseInterface) || responseInterface is null)
 314        {
 315            // Create a new OpenApiResponse if it doesn't exist
 0316            responseInterface = new OpenApiResponse();
 0317            responses[responseName] = responseInterface;
 318        }
 319        // return the response item
 0320        return (OpenApiResponse)responseInterface;
 321    }
 322
 323    /// <summary>
 324    /// Tries to get a response item by name from either inline or document components.
 325    /// </summary>
 326    /// <param name="responseName"> The name of the response item to retrieve.</param>
 327    /// <param name="response">The retrieved OpenApiResponse if found; otherwise, null.</param>
 328    /// <param name="isInline">Indicates whether the response was found in inline components.</param>
 329    /// <returns>True if the response item was found; otherwise, false.</returns>
 330    private bool TryGetResponseItem(string responseName, out OpenApiResponse? response, out bool isInline)
 331    {
 332        // First, check inline components
 0333        if (TryGetInline(name: responseName, kind: OpenApiComponentKind.Responses, out response))
 334        {
 0335            isInline = true;
 0336            return true;
 337        }
 338        // Next, check document components
 0339        else if (TryGetComponent(name: responseName, kind: OpenApiComponentKind.Responses, out response))
 340        {
 0341            isInline = false;
 0342            return true;
 343        }
 0344        response = null;
 0345        isInline = false;
 0346        return false;
 347    }
 348    /// <summary>
 349    /// Tries to get a response item by name from document components only.
 350    /// </summary>
 351    /// <param name="responseName"> The name of the response item to retrieve.</param>
 352    /// <param name="response"> The retrieved OpenApiResponse if found; otherwise, null.</param>
 353    /// <returns>True if the response item was found; otherwise, false.</returns>
 354    private bool TryGetResponseItem(string responseName, out OpenApiResponse? response) =>
 0355    TryGetResponseItem(responseName, out response, out _);
 356
 357    #endregion
 358}

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

#LineLine coverage
 1using System.Reflection;
 2using System.Text.Json.Nodes;
 3using Microsoft.OpenApi;
 4using OpenApiXmlModel = Microsoft.OpenApi.OpenApiXml;
 5using Kestrun.Runtime;
 6
 7namespace Kestrun.OpenApi;
 8
 9/// <summary>
 10/// Generates OpenAPI v2 (Swagger) documents from C# types decorated with OpenApiSchema attributes.
 11/// </summary>
 12public partial class OpenApiDocDescriptor
 13{
 14    #region Schemas
 15    private static OpenApiProperties? GetSchemaIdentity(Type t)
 16    {
 17        // Prefer OpenApiPropertyAttribute (it supports operation/property-level overrides),
 18        // but fall back to any OpenApiProperties-derived attribute (e.g., OpenApiSchemaComponent).
 019        var propAttrs = (OpenApiPropertyAttribute[])t.GetCustomAttributes(typeof(OpenApiPropertyAttribute), inherit: tru
 020        if (propAttrs.Length > 0)
 21        {
 022            return propAttrs[0];
 23        }
 24
 25        // Note: OpenApiSchemaComponent is Inherited=false, so inherit:true won't climb.
 26        // We walk the base chain manually to allow schemas deriving from OpenApi* primitives
 27        // (or from a base schema component) to inherit the underlying identity.
 028        for (var current = t; current is not null && current != typeof(object); current = current.BaseType)
 29        {
 030            var schemaAttrs = current.GetCustomAttributes(inherit: false).OfType<OpenApiProperties>().ToArray();
 031            if (schemaAttrs.Length > 0)
 32            {
 033                return schemaAttrs[0];
 34            }
 35        }
 36
 037        return null;
 38    }
 39
 40    /// <summary>
 41    /// Builds and returns the schema for a given type.
 42    /// </summary>
 43    /// <param name="t">Type to build schema for</param>
 44    /// <param name="built">Set of types already built to avoid recursion</param>
 45    /// <returns>OpenApiSchema representing the type</returns>
 46    private IOpenApiSchema BuildSchemaForType(Type t, HashSet<Type>? built = null)
 47    {
 3648        built ??= [];
 49
 3650        if (TryBuildPrimitiveSchema(t, out var primitiveSchema))
 51        {
 552            ApplyTypeAttributes(t, primitiveSchema);
 553            return primitiveSchema;
 54        }
 55
 3156        if (TryBuildDerivedSchemaFromBaseType(t, built, out var derivedSchema, out var schemaParent))
 57        {
 058            return derivedSchema;
 59        }
 60
 3161        var schema = CreateSchemaForDeclaredProperties(t);
 62
 3163        if (built.Contains(t))
 64        {
 165            return schema;
 66        }
 67
 3068        _ = built.Add(t);
 69
 3070        ApplyTypeAttributes(t, schema);
 71
 3072        if (t.IsEnum)
 73        {
 874            return RegisterEnumSchema(t);
 75        }
 76        // Extensions
 2277        ProcessExtensions(t, schema);
 78        // Properties
 2279        ProcessTypeProperties(t, schema, built);
 80        // Return composed schema if applicable
 2281        return ComposeWithParentSchema(schemaParent, schema);
 82    }
 83
 84    /// <summary>
 85    /// Processes OpenAPI extensions defined on a type and adds them to the schema.
 86    /// </summary>
 87    /// <param name="t">The type being processed.</param>
 88    /// <param name="schema"></param>
 89    private static void ProcessExtensions(Type t, OpenApiSchema schema)
 90    {
 4491        foreach (var attr in t.GetCustomAttributes<OpenApiExtensionAttribute>(inherit: false))
 92        {
 093            schema.Extensions ??= new Dictionary<string, IOpenApiExtension>(StringComparer.Ordinal);
 94            // Parse string into a JsonNode tree.
 095            var node = JsonNode.Parse(attr.Json);
 096            if (node is null)
 97            {
 98                continue;
 99            }
 0100            schema.Extensions[attr.Name] = new JsonNodeExtension(node);
 101        }
 22102    }
 103
 104    /// <summary>
 105    /// Attempts to create a schema for types mapped as OpenAPI primitives/scalars.
 106    /// </summary>
 107    /// <param name="t">The CLR type to map.</param>
 108    /// <param name="schema">The created primitive schema.</param>
 109    /// <returns><c>true</c> if the type was mapped as a primitive/scalar; otherwise, <c>false</c>.</returns>
 110    private static bool TryBuildPrimitiveSchema(Type t, out OpenApiSchema schema)
 111    {
 36112        if (PrimitiveSchemaMap.TryGetValue(t, out var getSchema))
 113        {
 5114            schema = getSchema();
 5115            return true;
 116        }
 117
 31118        schema = null!;
 31119        return false;
 120    }
 121
 122    /// <summary>
 123    /// Attempts to resolve schema generation for types that derive from another schema component.
 124    /// This covers array-wrappers and inheritance composition via <c>allOf</c>.
 125    /// </summary>
 126    /// <param name="t">The derived type being processed.</param>
 127    /// <param name="built">The recursion guard set passed through schema-building.</param>
 128    /// <param name="resolved">The resolved schema to return if handled.</param>
 129    /// <param name="schemaParent">The parent schema, if composition should be applied later.</param>
 130    /// <returns><c>true</c> if schema generation was fully handled and <paramref name="resolved"/> is set.</returns>
 131    private bool TryBuildDerivedSchemaFromBaseType(
 132        Type t,
 133        HashSet<Type> built,
 134        out IOpenApiSchema resolved,
 135        out OpenApiSchema? schemaParent)
 136    {
 31137        resolved = null!;
 31138        schemaParent = null;
 139
 31140        if (!HasComposableBaseType(t))
 141        {
 22142            return false;
 143        }
 144
 9145        var baseSchema = BuildBaseTypeSchema(t);
 9146        if (baseSchema is null)
 147        {
 0148            return false;
 149        }
 150
 9151        if (TryResolveSimpleOrReferenceBaseSchema(t, baseSchema, out resolved))
 152        {
 0153            return true;
 154        }
 155
 9156        if (baseSchema is OpenApiSchema arraySchema && TryResolveArrayWrapperDerivedSchema(t, built, arraySchema, out re
 157        {
 0158            return true;
 159        }
 160
 161        // Defer composition until after properties are processed.
 9162        schemaParent = baseSchema as OpenApiSchema;
 9163        return false;
 164    }
 165
 166    /// <summary>
 167    /// Determines whether a type has a base type that can participate in schema composition.
 168    /// </summary>
 169    /// <param name="t">The type being processed.</param>
 170    /// <returns><c>true</c> if the type derives from something other than <see cref="object"/>; otherwise <c>false</c>.
 171    private static bool HasComposableBaseType(Type t)
 31172        => t.BaseType is not null && t.BaseType != typeof(object);
 173
 174    /// <summary>
 175    /// Attempts to resolve a derived type immediately when its base schema is a simple schema or a reference.
 176    /// </summary>
 177    /// <param name="derivedType">The derived type being processed.</param>
 178    /// <param name="baseSchema">The schema resolved from the base type.</param>
 179    /// <param name="resolved">The resolved schema to return.</param>
 180    /// <returns><c>true</c> if the schema was resolved immediately; otherwise <c>false</c>.</returns>
 181    private bool TryResolveSimpleOrReferenceBaseSchema(
 182        Type derivedType,
 183        IOpenApiSchema baseSchema,
 184        out IOpenApiSchema resolved)
 185    {
 9186        resolved = null!;
 187
 9188        if (!IsSimpleSchemaOrReference(baseSchema))
 189        {
 9190            return false;
 191        }
 192
 0193        if (baseSchema is OpenApiSchema openApiSchema)
 194        {
 0195            ApplyTypeAttributes(derivedType, openApiSchema);
 0196            resolved = openApiSchema;
 0197            return true;
 198        }
 199
 0200        resolved = baseSchema;
 0201        return true;
 202    }
 203
 204    /// <summary>
 205    /// Determines whether a schema represents a "simple" base schema (not composed via <c>allOf</c>)
 206    /// or a schema reference.
 207    /// </summary>
 208    /// <param name="schema">The schema to check.</param>
 209    /// <returns><c>true</c> if the schema is simple or a reference; otherwise <c>false</c>.</returns>
 210    private static bool IsSimpleSchemaOrReference(IOpenApiSchema schema)
 211    {
 9212        return schema is OpenApiSchemaReference
 9213            || (schema.AllOf is null && schema.Type != JsonSchemaType.Array);
 214    }
 215
 216    /// <summary>
 217    /// Resolves the special case where a derived type represents an array wrapper and the array items
 218    /// may themselves be composed via <c>allOf</c>.
 219    /// </summary>
 220    /// <param name="derivedType">The derived type being processed.</param>
 221    /// <param name="built">The recursion guard set passed through schema-building.</param>
 222    /// <param name="arraySchema">The schema resolved from the base type.</param>
 223    /// <param name="resolved">The resolved schema to return.</param>
 224    /// <returns><c>true</c> if handled; otherwise <c>false</c>.</returns>
 225    private bool TryResolveArrayWrapperDerivedSchema(
 226        Type derivedType,
 227        HashSet<Type> built,
 228        OpenApiSchema arraySchema,
 229        out IOpenApiSchema resolved)
 230    {
 9231        resolved = null!;
 232
 9233        if (arraySchema.Type != JsonSchemaType.Array || arraySchema.Items is null)
 234        {
 9235            return false;
 236        }
 237
 0238        ApplyTypeAttributes(derivedType, arraySchema);
 239
 0240        if (arraySchema.Items is not OpenApiSchema itemSchema)
 241        {
 0242            resolved = arraySchema;
 0243            return true;
 244        }
 245
 246        // Preserve existing behavior (type-level attributes applied twice in this branch).
 0247        ApplyTypeAttributes(derivedType, arraySchema);
 248
 0249        if (itemSchema.AllOf is null)
 250        {
 0251            resolved = arraySchema;
 0252            return true;
 253        }
 254
 0255        var additional = CreateAllOfAdditionalObjectSchema(derivedType, built);
 0256        itemSchema.AllOf.Add(additional);
 0257        resolved = arraySchema;
 0258        return true;
 259    }
 260
 261    /// <summary>
 262    /// Creates the additional object schema appended to an existing <c>allOf</c> list for a derived type.
 263    /// </summary>
 264    /// <param name="t">The derived type whose properties should be added.</param>
 265    /// <param name="built">The recursion guard set passed through schema-building.</param>
 266    /// <returns>An <see cref="OpenApiSchema"/> containing only properties declared on <paramref name="t"/>.</returns>
 267    private OpenApiSchema CreateAllOfAdditionalObjectSchema(Type t, HashSet<Type> built)
 268    {
 0269        var additional = new OpenApiSchema
 0270        {
 0271            Type = JsonSchemaType.Object,
 0272            Properties = new Dictionary<string, IOpenApiSchema>(StringComparer.Ordinal)
 0273        };
 274
 0275        ProcessTypeProperties(t, additional, built);
 0276        return additional;
 277    }
 278
 279    /// <summary>
 280    /// Creates the base schema instance for a type, initializing object properties only when
 281    /// the type declares at least one public instance property.
 282    /// </summary>
 283    /// <param name="t">The CLR type being processed.</param>
 284    /// <returns>An <see cref="OpenApiSchema"/> ready for property population.</returns>
 285    private static OpenApiSchema CreateSchemaForDeclaredProperties(Type t)
 286    {
 31287        var schema = new OpenApiSchema();
 31288        var declaredPropsCount =
 31289            t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
 168290             .Count(p => p.DeclaringType == t);
 291
 31292        if (declaredPropsCount > 0)
 293        {
 23294            schema.Type = JsonSchemaType.Object;
 23295            schema.Properties = new Dictionary<string, IOpenApiSchema>(StringComparer.Ordinal);
 296        }
 297
 31298        return schema;
 299    }
 300
 301    /// <summary>
 302    /// Composes a child schema into a parent schema when inheritance composition is active.
 303    /// </summary>
 304    /// <param name="schemaParent">The parent schema built from the base type (if any).</param>
 305    /// <param name="schema">The derived schema with properties populated.</param>
 306    /// <returns>The composed schema to return from schema generation.</returns>
 307    private static IOpenApiSchema ComposeWithParentSchema(OpenApiSchema? schemaParent, OpenApiSchema schema)
 308    {
 22309        if (schemaParent is null)
 310        {
 21311            return schema;
 312        }
 313
 1314        if (schemaParent.AllOf is not null)
 315        {
 1316            schemaParent.AllOf.Add(schema);
 1317            schemaParent.Type = null; // Clear type when using allOf
 1318            return schemaParent;
 319        }
 320
 0321        if (schemaParent.Type == JsonSchemaType.Array)
 322        {
 0323            if (schemaParent.Items is OpenApiSchema items && items.AllOf is not null)
 324            {
 0325                items.AllOf.Add(schema);
 326            }
 327
 0328            return schemaParent;
 329        }
 330
 0331        return schema;
 332    }
 333
 334    /// <summary>
 335    /// Builds schema for custom base type derivations.
 336    /// </summary>
 337    ///  <param name="t">Type to build schema for</param>
 338    /// <returns>OpenApiSchema representing the base type derivation, or null if not applicable</returns>
 339    private static IOpenApiSchema? BuildBaseTypeSchema(Type t)
 340    {
 9341        if (PrimitiveSchemaMap.TryGetValue(t.BaseType!, out var value))
 342        {
 0343            return value();
 344        }
 345
 346        // Fallback to custom base type schema building
 9347        return BuildCustomBaseTypeSchema(t);
 348    }
 349
 350    /// <summary>
 351    /// Builds schema for types with custom base types.
 352    /// </summary>
 353    /// <param name="t">Type to build schema for</param>
 354    /// <returns>OpenApiSchema representing the custom base type derivation</returns>
 355    private static IOpenApiSchema BuildCustomBaseTypeSchema(Type t)
 356    {
 9357        var attributes = t.CustomAttributes.ToArray();
 358        // Count declared properties
 9359        var declaredPropsCount =
 9360            t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
 11361             .Count(p => p.DeclaringType == t);
 362
 363        // Check for the special case where the derived type only adds array semantics
 9364        var hasArray =
 9365            attributes.Length > 0 &&
 9366            attributes[0].NamedArguments.Any(na =>
 9367                na.MemberName == "Array" &&
 9368                na.TypedValue.ArgumentType == typeof(bool) &&
 9369                na.TypedValue.Value is bool b && b);
 370        // If so, we can represent this as a simple reference to the base type
 9371        if (declaredPropsCount == 0 && attributes.Length == 1)
 372        {
 0373            if (hasArray)
 374            {
 0375                return new OpenApiSchema
 0376                {
 0377                    Type = JsonSchemaType.Array,
 0378                    Items = new OpenApiSchemaReference(t.BaseType!.Name)
 0379                };
 380            }
 381            // If the derived type has AdditionalProperties, we can't use allOf
 0382            return new OpenApiSchemaReference(t.BaseType!.Name);
 383        }
 384        // Otherwise, build an allOf schema referencing the base type
 9385        var schema = new OpenApiSchema
 9386        {
 9387            AllOf = [new OpenApiSchemaReference(t.BaseType!.Name)]
 9388        };
 389        // Apply array semantics if specified
 9390        return hasArray
 9391            ? new OpenApiSchema
 9392            {
 9393                Type = JsonSchemaType.Array,
 9394                Items = schema
 9395            }
 9396            : schema;
 397    }
 398
 399    /// <summary>
 400    /// Registers an enum type schema in the document components.
 401    /// </summary>
 402    /// <returns>The registered enum schema.</returns>
 403    private OpenApiSchema RegisterEnumSchema(Type enumType)
 404    {
 10405        var enumSchema = new OpenApiSchema
 10406        {
 10407            Type = JsonSchemaType.String,
 120408            Enum = [.. enumType.GetEnumNames().Select(n => (JsonNode)n)]
 10409        };
 10410        if (Document.Components?.Schemas is not null)
 411        {
 10412            Document.Components.Schemas[enumType.Name] = enumSchema;
 413        }
 10414        return enumSchema;
 415    }
 416
 417    /// <summary>
 418    /// Applies type-level attributes to a schema.
 419    /// </summary>
 420    private static void ApplyTypeAttributes(Type t, OpenApiSchema schema)
 421    {
 76422        foreach (var attr in t.GetCustomAttributes(true)
 90423          .Where(a => a is OpenApiPropertyAttribute or OpenApiSchemaComponent))
 424        {
 3425            ApplySchemaAttr(attr as OpenApiProperties, schema);
 426
 3427            if (attr is OpenApiSchemaComponent schemaAttribute && schemaAttribute.Examples is not null)
 428            {
 0429                schema.Examples ??= [];
 0430                var node = OpenApiJsonNodeFactory.ToNode(schemaAttribute.Examples);
 0431                if (node is not null)
 432                {
 0433                    schema.Examples.Add(node);
 434                }
 435            }
 436        }
 35437    }
 438
 439    /// <summary>
 440    /// Processes all properties of a type and builds their schemas.
 441    /// </summary>
 442    private void ProcessTypeProperties(Type t, OpenApiSchema schema, HashSet<Type> built)
 443    {
 22444        var instance = TryCreateTypeInstance(t);
 445
 312446        foreach (var prop in t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
 157447                      .Where(p => p.DeclaringType == t))
 448        {
 134449            var propSchema = BuildPropertySchema(prop, built);
 134450            CapturePropertyDefault(instance, prop, propSchema);
 451
 134452            if (prop.GetCustomAttribute<OpenApiAdditionalPropertiesAttribute>() is not null)
 453            {
 1454                schema.AdditionalPropertiesAllowed = true;
 1455                schema.AdditionalProperties = propSchema;
 456            }
 457            else
 458            {
 133459                schema.Properties?.Add(prop.Name, propSchema);
 460            }
 461        }
 22462    }
 463
 464    /// <summary>
 465    /// Attempts to create an instance of a type to capture default values.
 466    /// </summary>
 467    private static object? TryCreateTypeInstance(Type t)
 468    {
 469        try
 470        {
 22471            return Activator.CreateInstance(t);
 472        }
 4473        catch
 474        {
 4475            return null;
 476        }
 22477    }
 478
 479    /// <summary>
 480    /// Captures the default value of a property if not already set.
 481    /// </summary>
 482    private static void CapturePropertyDefault(object? instance, PropertyInfo prop, IOpenApiSchema propSchema)
 483    {
 134484        if (instance is null || propSchema is not OpenApiSchema concrete || concrete.Default is not null)
 485        {
 31486            return;
 487        }
 488
 489        try
 490        {
 103491            var value = prop.GetValue(instance);
 103492            if (!IsIntrinsicDefault(value, prop.PropertyType))
 493            {
 37494                concrete.Default = OpenApiJsonNodeFactory.ToNode(value);
 495            }
 103496        }
 0497        catch
 498        {
 499            // Ignore failures when capturing defaults
 0500        }
 103501    }
 502
 503    /// <summary>
 504    /// Determines if a value is the intrinsic default for its declared type.
 505    /// </summary>
 506    /// <param name="value">The value to check.</param>
 507    /// <param name="declaredType">The declared type of the value.</param>
 508    /// <returns>True if the value is the intrinsic default for its declared type; otherwise, false.</returns>
 509    private static bool IsIntrinsicDefault(object? value, Type declaredType)
 510    {
 116511        if (value is null)
 512        {
 20513            return true;
 514        }
 515
 516        // Unwrap Nullable<T>
 96517        var t = Nullable.GetUnderlyingType(declaredType) ?? declaredType;
 518
 519        // Reference types: null is the only intrinsic default
 96520        if (!t.IsValueType)
 521        {
 33522            return false;
 523        }
 524
 525        // Special-cases for common structs
 63526        if (t == typeof(Guid))
 527        {
 5528            return value.Equals(Guid.Empty);
 529        }
 530
 58531        if (t == typeof(TimeSpan))
 532        {
 1533            return value.Equals(TimeSpan.Zero);
 534        }
 535
 57536        if (t == typeof(DateTime))
 537        {
 5538            return value.Equals(default(DateTime));
 539        }
 540
 52541        if (t == typeof(DateTimeOffset))
 542        {
 1543            return value.Equals(default(DateTimeOffset));
 544        }
 545
 546        // Enums: 0 is intrinsic default
 51547        if (t.IsEnum)
 548        {
 2549            return Convert.ToInt64(value) == 0;
 550        }
 551
 552        // Primitive/value types: compare to default(T)
 49553        var def = Activator.CreateInstance(t);
 49554        return value.Equals(def);
 555    }
 556
 557    /// <summary>
 558    /// Makes an OpenApiSchema nullable if specified.
 559    /// </summary>
 560    /// <param name="schema">The OpenApiSchema to modify.</param>
 561    /// <param name="isNullable">Indicates whether the schema should be nullable.</param>
 562    /// <returns>The modified OpenApiSchema.</returns>
 563    private static OpenApiSchema MakeNullable(OpenApiSchema schema, bool isNullable)
 564    {
 24565        if (isNullable)
 566        {
 2567            schema.Type |= JsonSchemaType.Null;
 568        }
 24569        return schema;
 570    }
 571
 572    /// <summary>
 573    /// Infers a primitive OpenApiSchema from a .NET type.
 574    /// </summary>
 575    /// <param name="type">The .NET type to infer from.</param>
 576    /// <param name="inline">Indicates if the schema should be inlined.</param>
 577    /// <returns>The inferred OpenApiSchema.</returns>
 578    public IOpenApiSchema InferPrimitiveSchema(Type type, bool inline = false)
 579    {
 24580        var nullable = false;
 24581        if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)
 24582            && type.GetGenericArguments().Length == 1)
 583        {
 1584            type = type.GetGenericArguments()[0];
 1585            nullable = true;
 586        }
 587        // Direct type mappings
 24588        if (PrimitiveSchemaMap.TryGetValue(type, out var schemaFactory))
 589        {
 22590            return MakeNullable(schemaFactory(), nullable);
 591        }
 592
 593        // Array type handling
 2594        if (type.Name.EndsWith("[]"))
 595        {
 1596            return InferArraySchema(type, inline);
 597        }
 598
 599        // Special handling for PowerShell OpenAPI classes
 1600        if (PowerShellOpenApiClassExporter.ValidClassNames.Contains(type.Name))
 601        {
 0602            return InferPowerShellClassSchema(type, inline);
 603        }
 604
 605        // Fallback
 1606        return new OpenApiSchema { Type = JsonSchemaType.String };
 607    }
 608
 609    /// <summary>
 610    /// Infers an array OpenApiSchema from a .NET array type.
 611    /// </summary>
 612    /// <param name="type">The .NET array type to infer from.</param>
 613    /// <param name="inline">Indicates if the schema should be inlined.</param>
 614    /// <returns>The inferred OpenApiSchema.</returns>
 615    private OpenApiSchema InferArraySchema(Type type, bool inline)
 616    {
 2617        var typeName = type.Name[..^2];
 2618        if (ComponentSchemasExists(typeName))
 619        {
 0620            var items = inline ? GetSchema(typeName).Clone() : new OpenApiSchemaReference(typeName);
 0621            return new OpenApiSchema { Type = JsonSchemaType.Array, Items = items };
 622        }
 623
 2624        return new OpenApiSchema { Type = JsonSchemaType.Array, Items = InferPrimitiveSchema(type.GetElementType() ?? ty
 625    }
 626
 627    /// <summary>
 628    /// Infers a PowerShell OpenAPI class schema.
 629    /// </summary>
 630    /// <param name="type">The .NET type representing the PowerShell OpenAPI class.</param>
 631    /// <param name="inline">Indicates if the schema should be inlined.</param>
 632    /// <returns>The inferred OpenApiSchema.</returns>
 633    private IOpenApiSchema InferPowerShellClassSchema(Type type, bool inline)
 634    {
 0635        if (TryGetSchemaItem(type.Name, out var schema, out var isInline))
 636        {
 0637            if (inline || isInline)
 638            {
 0639                if (schema is OpenApiSchema concreteSchema)
 640                {
 0641                    return concreteSchema.Clone();
 642                }
 643            }
 644            else
 645            {
 0646                return new OpenApiSchemaReference(type.Name);
 647            }
 648        }
 649
 0650        Host.Logger.Warning("Schema for PowerShell OpenAPI class '{typeName}' not found. Defaulting to string schema.", 
 0651        return new OpenApiSchema { Type = JsonSchemaType.String };
 652    }
 653
 654    /// <summary>
 655    /// Mapping of .NET primitive types to OpenAPI schema definitions.
 656    /// </summary>
 657    /// <remarks>
 658    /// This dictionary maps common .NET primitive types to their corresponding OpenAPI schema representations.
 659    /// Each entry consists of a .NET type as the key and a function that returns an OpenApiSchema as the value.
 660    /// </remarks>
 1661    private static readonly Dictionary<Type, Func<OpenApiSchema>> PrimitiveSchemaMap = new()
 1662    {
 38663        [typeof(string)] = () => new OpenApiSchema { Type = JsonSchemaType.String },
 24664        [typeof(bool)] = () => new OpenApiSchema { Type = JsonSchemaType.Boolean },
 0665        [typeof(long)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int64" },
 5666        [typeof(DateTime)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "date-time" },
 1667        [typeof(DateTimeOffset)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "date-time" },
 0668        [typeof(TimeSpan)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "duration" },
 3669        [typeof(byte[])] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "binary" },
 1670        [typeof(Uri)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "uri" },
 7671        [typeof(Guid)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "uuid" },
 4672        [typeof(object)] = () => new OpenApiSchema { Type = JsonSchemaType.Object },
 0673        [typeof(void)] = () => new OpenApiSchema { Type = JsonSchemaType.Null },
 3674        [typeof(char)] = () => new OpenApiSchema { Type = JsonSchemaType.String, MaxLength = 1, MinLength = 1 },
 3675        [typeof(sbyte)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int32" },
 3676        [typeof(byte)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int32" },
 3677        [typeof(short)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int32" },
 3678        [typeof(ushort)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int32" },
 22679        [typeof(int)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int32" },
 3680        [typeof(uint)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int32" },
 9681        [typeof(long)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int64" },
 3682        [typeof(ulong)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int64" },
 3683        [typeof(float)] = () => new OpenApiSchema { Type = JsonSchemaType.Number, Format = "float" },
 5684        [typeof(double)] = () => new OpenApiSchema { Type = JsonSchemaType.Number, Format = "double" },
 4685        [typeof(decimal)] = () => new OpenApiSchema { Type = JsonSchemaType.Number, Format = "decimal" },
 4686        [typeof(OpenApiString)] = () => new OpenApiSchema { Type = JsonSchemaType.String },
 0687        [typeof(OpenApiUuid)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "uuid" },
 0688        [typeof(OpenApiDate)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "date" },
 0689        [typeof(OpenApiDateTime)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "date-time" },
 0690        [typeof(OpenApiEmail)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "email" },
 0691        [typeof(OpenApiBinary)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "binary" },
 0692        [typeof(OpenApiHostname)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "hostname" },
 0693        [typeof(OpenApiIpv4)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "ipv4" },
 0694        [typeof(OpenApiIpv6)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "ipv6" },
 0695        [typeof(OpenApiUri)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "uri" },
 0696        [typeof(OpenApiUrl)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "url" },
 0697        [typeof(OpenApiByte)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "byte" },
 0698        [typeof(OpenApiPassword)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "password" },
 0699        [typeof(OpenApiRegex)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "regex" },
 0700        [typeof(OpenApiJson)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "json" },
 0701        [typeof(OpenApiXmlModel)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "xml" },
 0702        [typeof(OpenApiYaml)] = () => new OpenApiSchema { Type = JsonSchemaType.String, Format = "yaml" },
 1703
 3704        [typeof(OpenApiInteger)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer },
 0705        [typeof(OpenApiInt32)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int32" },
 0706        [typeof(OpenApiInt64)] = () => new OpenApiSchema { Type = JsonSchemaType.Integer, Format = "int64" },
 1707
 3708        [typeof(OpenApiNumber)] = () => new OpenApiSchema { Type = JsonSchemaType.Number },
 0709        [typeof(OpenApiFloat)] = () => new OpenApiSchema { Type = JsonSchemaType.Number, Format = "float" },
 0710        [typeof(OpenApiDouble)] = () => new OpenApiSchema { Type = JsonSchemaType.Number, Format = "double" },
 1711
 3712        [typeof(OpenApiBoolean)] = () => new OpenApiSchema { Type = JsonSchemaType.Boolean, Format = "boolean" },
 1713    };
 714
 715    /// <summary>
 716    /// Applies schema attributes to an OpenAPI schema.
 717    /// </summary>
 718    /// <param name="oaProperties">The OpenApiProperties containing attributes to apply.</param>
 719    /// <param name="ioaSchema">The OpenAPI schema to apply attributes to.</param>
 720    private static void ApplySchemaAttr(OpenApiProperties? oaProperties, IOpenApiSchema ioaSchema)
 721    {
 163722        if (oaProperties is null)
 723        {
 151724            return;
 725        }
 726
 727        // Most models implement OpenApiSchema (concrete) OR OpenApiSchemaReference.
 728        // We set common metadata when possible (Description/Title apply only to concrete schema).
 12729        if (ioaSchema is OpenApiSchema concreteSchema)
 730        {
 12731            ApplyConcreteSchemaAttributes(oaProperties, concreteSchema);
 12732            return;
 733        }
 734
 0735        if (ioaSchema is OpenApiSchemaReference refSchema)
 736        {
 0737            ApplyReferenceSchemaAttributes(oaProperties, refSchema);
 738        }
 0739    }
 740
 741    /// <summary>
 742    /// Applies concrete schema attributes to an OpenApiSchema.
 743    /// </summary>
 744    /// <param name="properties">The OpenApiProperties containing attributes to apply.</param>
 745    /// <param name="schema">The OpenApiSchema to apply attributes to.</param>
 746    private static void ApplyConcreteSchemaAttributes(OpenApiProperties properties, OpenApiSchema schema)
 747    {
 14748        ApplyTitleAndDescription(properties, schema);
 14749        ApplySchemaType(properties, schema);
 14750        ApplyFormatAndNumericBounds(properties, schema);
 14751        ApplyLengthAndPattern(properties, schema);
 14752        ApplyCollectionConstraints(properties, schema);
 14753        ApplyFlags(properties, schema);
 14754        ApplyExamplesAndDefaults(properties, schema);
 14755        ApplyXmlMetadata(properties, schema);
 14756    }
 757
 758    /// <summary>
 759    /// Applies title and description to an OpenApiSchema.
 760    /// </summary>
 761    /// <param name="properties">The OpenApiProperties containing attributes to apply.</param>
 762    /// <param name="schema">The OpenApiSchema to apply attributes to.</param>
 763    private static void ApplyTitleAndDescription(OpenApiProperties properties, OpenApiSchema schema)
 764    {
 14765        if (properties.Title is not null)
 766        {
 0767            schema.Title = properties.Title;
 768        }
 14769        if (properties is not OpenApiParameterComponentAttribute && properties.Description is not null)
 770        {
 0771            schema.Description = properties.Description;
 772        }
 14773    }
 774
 775    /// <summary>
 776    /// Applies schema type and nullability to an OpenApiSchema.
 777    /// </summary>
 778    /// <param name="properties">The OpenApiProperties containing attributes to apply.</param>
 779    /// <param name="schema">The OpenApiSchema to apply attributes to.</param>
 780    private static void ApplySchemaType(OpenApiProperties properties, OpenApiSchema schema)
 781    {
 14782        if (properties.Type != OaSchemaType.None)
 783        {
 0784            schema.Type = properties.Type switch
 0785            {
 0786                OaSchemaType.String => JsonSchemaType.String,
 0787                OaSchemaType.Number => JsonSchemaType.Number,
 0788                OaSchemaType.Integer => JsonSchemaType.Integer,
 0789                OaSchemaType.Boolean => JsonSchemaType.Boolean,
 0790                OaSchemaType.Array => JsonSchemaType.Array,
 0791                OaSchemaType.Object => JsonSchemaType.Object,
 0792                OaSchemaType.Null => JsonSchemaType.Null,
 0793                _ => schema.Type
 0794            };
 795        }
 796
 14797        if (properties.Nullable)
 798        {
 0799            schema.Type |= JsonSchemaType.Null;
 800        }
 14801    }
 802
 803    /// <summary>
 804    /// Applies format and numeric bounds to an OpenApiSchema.
 805    /// </summary>
 806    /// <param name="properties">The OpenApiProperties containing attributes to apply.</param>
 807    /// <param name="schema"></param>
 808    private static void ApplyFormatAndNumericBounds(OpenApiProperties properties, OpenApiSchema schema)
 809    {
 14810        if (!string.IsNullOrWhiteSpace(properties.Format))
 811        {
 0812            schema.Format = properties.Format;
 813        }
 814
 14815        if (properties.MultipleOf.HasValue)
 816        {
 0817            schema.MultipleOf = properties.MultipleOf;
 818        }
 819
 14820        if (!string.IsNullOrWhiteSpace(properties.Maximum))
 821        {
 0822            schema.Maximum = properties.Maximum;
 0823            if (properties.ExclusiveMaximum)
 824            {
 0825                schema.ExclusiveMaximum = properties.Maximum;
 826            }
 827        }
 828
 14829        if (!string.IsNullOrWhiteSpace(properties.Minimum))
 830        {
 0831            schema.Minimum = properties.Minimum;
 0832            if (properties.ExclusiveMinimum)
 833            {
 0834                schema.ExclusiveMinimum = properties.Minimum;
 835            }
 836        }
 14837    }
 838
 839    /// <summary>
 840    /// Applies length and pattern constraints to an OpenApiSchema.
 841    /// </summary>
 842    /// <param name="properties">The OpenApiProperties containing attributes to apply.</param>
 843    /// <param name="schema"></param>
 844    private static void ApplyLengthAndPattern(OpenApiProperties properties, OpenApiSchema schema)
 845    {
 14846        if (properties.MaxLength >= 0)
 847        {
 0848            schema.MaxLength = properties.MaxLength;
 849        }
 850
 14851        if (properties.MinLength >= 0)
 852        {
 0853            schema.MinLength = properties.MinLength;
 854        }
 855
 14856        if (!string.IsNullOrWhiteSpace(properties.Pattern))
 857        {
 0858            schema.Pattern = properties.Pattern;
 859        }
 14860    }
 861
 862    /// <summary>
 863    /// Applies collection constraints to an OpenApiSchema.
 864    /// </summary>
 865    /// <param name="properties">The OpenApiProperties containing attributes to apply.</param>
 866    /// <param name="schema">The OpenApiSchema to apply attributes to.</param>
 867    private static void ApplyCollectionConstraints(OpenApiProperties properties, OpenApiSchema schema)
 868    {
 14869        if (properties.MaxItems >= 0)
 870        {
 0871            schema.MaxItems = properties.MaxItems;
 872        }
 873
 14874        if (properties.MinItems >= 0)
 875        {
 0876            schema.MinItems = properties.MinItems;
 877        }
 878
 14879        if (properties.UniqueItems)
 880        {
 0881            schema.UniqueItems = true;
 882        }
 883
 14884        if (properties.MaxProperties >= 0)
 885        {
 0886            schema.MaxProperties = properties.MaxProperties;
 887        }
 888
 14889        if (properties.MinProperties >= 0)
 890        {
 0891            schema.MinProperties = properties.MinProperties;
 892        }
 14893    }
 894
 895    private static void ApplyFlags(OpenApiProperties properties, OpenApiSchema schema)
 896    {
 14897        schema.ReadOnly = properties.ReadOnly;
 14898        schema.WriteOnly = properties.WriteOnly;
 14899        schema.AdditionalPropertiesAllowed = properties.AdditionalPropertiesAllowed;
 14900        schema.UnevaluatedProperties = properties.UnevaluatedProperties;
 14901        if (properties is not OpenApiParameterComponentAttribute)
 902        {
 12903            schema.Deprecated = properties.Deprecated;
 904        }
 14905    }
 906
 907    private static void ApplyExamplesAndDefaults(OpenApiProperties properties, OpenApiSchema schema)
 908    {
 14909        if (properties.Default is not null)
 910        {
 0911            schema.Default = OpenApiJsonNodeFactory.ToNode(properties.Default);
 912        }
 14913        if (properties.Example is not null && properties is not OpenApiParameterComponentAttribute)
 914        {
 0915            schema.Example = OpenApiJsonNodeFactory.ToNode(properties.Example);
 916        }
 917
 14918        if (properties.Enum is { Length: > 0 })
 919        {
 0920            schema.Enum = [.. properties.Enum.Select(OpenApiJsonNodeFactory.ToNode).OfType<JsonNode>()];
 921        }
 922
 14923        if (properties.RequiredProperties is { Length: > 0 })
 924        {
 0925            schema.Required ??= new HashSet<string>(StringComparer.Ordinal);
 0926            foreach (var r in properties.RequiredProperties)
 927            {
 0928                _ = schema.Required.Add(r);
 929            }
 930        }
 14931    }
 932
 933    /// <summary>
 934    /// Applies XML metadata to an OpenApiSchema.
 935    /// </summary>
 936    /// <param name="properties">The OpenApiProperties containing XML attributes to apply.</param>
 937    /// <param name="schema">The OpenApiSchema to apply XML metadata to.</param>
 938    private static void ApplyXmlMetadata(OpenApiProperties properties, OpenApiSchema schema)
 939    {
 940        // Check if any XML properties are set
 14941        var hasXmlMetadata = !string.IsNullOrWhiteSpace(properties.XmlName) ||
 14942                             !string.IsNullOrWhiteSpace(properties.XmlNamespace) ||
 14943                             !string.IsNullOrWhiteSpace(properties.XmlPrefix) ||
 14944                             properties.XmlAttribute ||
 14945                             properties.XmlWrapped;
 946
 14947        if (!hasXmlMetadata)
 948        {
 6949            return;
 950        }
 951
 952        // Create XML object if it doesn't exist
 8953        schema.Xml ??= new OpenApiXmlModel();
 954
 955        // Apply standard XML properties (supported by Microsoft.OpenApi 3.1.2)
 8956        if (!string.IsNullOrWhiteSpace(properties.XmlName))
 957        {
 2958            schema.Xml.Name = properties.XmlName;
 959        }
 960
 8961        if (!string.IsNullOrWhiteSpace(properties.XmlNamespace))
 962        {
 2963            schema.Xml.Namespace = new Uri(properties.XmlNamespace);
 964        }
 965
 8966        if (!string.IsNullOrWhiteSpace(properties.XmlPrefix))
 967        {
 1968            schema.Xml.Prefix = properties.XmlPrefix;
 969        }
 970
 971        // Set NodeType based on XmlAttribute and XmlWrapped properties
 972        // OpenAPI 3.2 uses NodeType to specify attribute vs element vs text nodes
 8973        if (properties.XmlAttribute)
 974        {
 2975            schema.Xml.NodeType = OpenApiXmlNodeType.Attribute;
 976        }
 6977        else if (properties.XmlWrapped)
 978        {
 2979            schema.Xml.NodeType = OpenApiXmlNodeType.Element;
 980        }
 6981    }
 982
 983    /// <summary>
 984    /// Applies reference schema attributes to an OpenApiSchemaReference.
 985    /// </summary>
 986    /// <param name="properties">The OpenApiProperties containing attributes to apply.</param>
 987    /// <param name="reference">The OpenApiSchemaReference to apply attributes to.</param>
 988    private static void ApplyReferenceSchemaAttributes(OpenApiProperties properties, OpenApiSchemaReference reference)
 989    {
 990        // Description/Title can live on a reference proxy in v2 (and serialize alongside $ref)
 0991        if (!string.IsNullOrWhiteSpace(properties.Description))
 992        {
 0993            reference.Description = properties.Description;
 994        }
 995
 0996        if (!string.IsNullOrWhiteSpace(properties.Title))
 997        {
 0998            reference.Title = properties.Title;
 999        }
 1000
 1001        // Example/Default/Enum aren’t typically set on the ref node itself;
 1002        // attach such metadata to the component target instead if you need it.
 01003    }
 1004
 1005    #endregion
 1006}

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

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

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

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

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

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

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

#LineLine coverage
 1using Microsoft.OpenApi;
 2using Kestrun.Hosting;
 3using Microsoft.OpenApi.Reader;
 4using System.Text;
 5using Kestrun.Hosting.Options;
 6using Kestrun.Utilities;
 7using System.Collections;
 8using System.Text.Json.Nodes;
 9
 10namespace Kestrun.OpenApi;
 11
 12/// <summary>
 13/// Generates OpenAPI v2 (Swagger) documents from C# types decorated with OpenApiSchema attributes.
 14/// </summary>
 15public partial class OpenApiDocDescriptor
 16{
 17    /// <summary>
 18    /// Default documentation identifier.
 19    /// </summary>
 20    public const string DefaultDocumentationId = "Default";
 21
 22    /// <summary>
 23    /// Default documentation identifiers for OpenAPI authentication schemes.
 24    /// </summary>
 125    public static readonly string[] DefaultDocumentationIds = ["Default"];
 26    /// <summary>
 27    /// The Kestrun host providing registered routes.
 28    /// </summary>
 15529    public KestrunHost Host { get; init; }
 30
 31    /// <summary>
 32    /// The ID of the OpenAPI document being generated.
 33    /// </summary>
 13134    public string DocumentId { get; init; }
 35
 36    /// <summary>
 37    /// The OpenAPI document being generated.
 38    /// </summary>
 57539    public OpenApiDocument Document { get; private set; } = new OpenApiDocument { Components = new OpenApiComponents() }
 40
 41    /// <summary>
 42    /// Security requirements for the OpenAPI document.
 43    /// </summary>
 15444    public IDictionary<string, OpenApiSecurityRequirement> SecurityRequirement { get; private set; } = new Dictionary<st
 45
 46    /// <summary>
 47    /// Inline components specific to this OpenAPI document.
 48    /// </summary>
 2649    public OpenApiComponents InlineComponents { get; }
 50
 51    /// <summary>
 52    /// OpenAPI metadata for webhooks associated with this document.
 53    /// </summary>
 13354    public Dictionary<(string Pattern, HttpVerb Method), OpenAPIPathMetadata> WebHook { get; set; } = [];
 55
 56    /// <summary>
 57    /// OpenAPI metadata for callbacks associated with this document.
 58    /// </summary>
 13159    public Dictionary<(string Pattern, HttpVerb Method), OpenAPIPathMetadata> Callbacks { get; set; } = [];
 60
 61    /// <summary>
 62    /// Initializes a new instance of the OpenApiDocDescriptor.
 63    /// </summary>
 64    /// <param name="host">The Kestrun host.</param>
 65    /// <param name="docId">The ID of the OpenAPI document being generated.</param>
 66    /// <exception cref="ArgumentNullException">Thrown if host or docId is null.</exception>
 13167    public OpenApiDocDescriptor(KestrunHost host, string docId)
 68    {
 13169        ArgumentNullException.ThrowIfNull(host);
 13170        ArgumentNullException.ThrowIfNull(docId);
 13171        Host = host;
 13172        DocumentId = docId;
 13173        HasBeenGenerated = false;
 13174        InlineComponents = new OpenApiComponents();
 13175    }
 76
 77    /// <summary>
 78    /// Indicates whether the OpenAPI document has been generated at least once.
 79    /// </summary>
 13380    public bool HasBeenGenerated { get; private set; }
 81
 82    /// <summary>
 83    /// Generates an OpenAPI document from the provided schema types.
 84    /// </summary>
 85    /// <param name="components">The set of discovered OpenAPI component types.</param>
 86    /// <returns>The generated OpenAPI document.</returns>
 87    internal void GenerateComponents(OpenApiComponentSet components)
 88    {
 389        Document.Components ??= new OpenApiComponents();
 990        ProcessComponentTypes(components.SchemaTypes, () => Document.Components.Schemas ??= new Dictionary<string, IOpen
 391    }
 92
 93    /// <summary>
 94    /// Processes a list of component types and builds them into the OpenAPI document.
 95    /// </summary>
 96    /// <param name="types">The list of component types to process.</param>
 97    /// <param name="ensureDictionary">An action to ensure the corresponding dictionary is initialized.</param>
 98    /// <param name="buildAction">An action to build each component type.</param>
 99    private static void ProcessComponentTypes(
 100        IReadOnlyList<Type>? types,
 101        Action ensureDictionary,
 102        Action<Type> buildAction)
 103    {
 3104        if (types is null || types.Count == 0)
 105        {
 0106            return;
 107        }
 108
 3109        ensureDictionary();
 12110        foreach (var type in types)
 111        {
 3112            buildAction(type);
 113        }
 3114    }
 115
 116    /// <summary>
 117    /// Generates the OpenAPI document by auto-discovering component types.
 118    /// </summary>
 119    public void GenerateComponents()
 120    {
 121        // Auto-discover OpenAPI component types
 3122        var components = OpenApiSchemaDiscovery.GetOpenApiTypesAuto();
 123
 124        // Generate components from the discovered types
 3125        GenerateComponents(components);
 126
 127        // Process variable annotations from the host
 3128        ProcessVariableAnnotations(Host.ComponentAnnotations);
 3129    }
 130
 131    /// <summary>
 132    /// Processes variable annotations to build OpenAPI components.
 133    /// </summary>
 134    /// <param name="annotations">A dictionary of variable names to their annotated variables.</param>
 135    private void ProcessVariableAnnotations(Dictionary<string, OpenApiComponentAnnotationScanner.AnnotatedVariable>? ann
 136    {
 8137        if (annotations is null || annotations.Count == 0)
 138        {
 5139            Host.Logger.Warning("No OpenAPI component annotations were found in the host.");
 5140            return;
 141        }
 12142        foreach (var variable in annotations.Values)
 143        {
 3144            if (variable?.Annotations is null || variable.Annotations.Count == 0)
 145            {
 146                continue;
 147            }
 148
 2149            DispatchComponentAnnotations(variable);
 150        }
 3151    }
 152
 153    /// <summary>
 154    /// Dispatches component annotations for a given variable.
 155    /// </summary>
 156    /// <param name="variable">The annotated variable containing annotations.</param>
 157    private void DispatchComponentAnnotations(OpenApiComponentAnnotationScanner.AnnotatedVariable variable)
 158    {
 12159        foreach (var annotation in variable.Annotations)
 160        {
 161            switch (annotation)
 162            {
 163                case OpenApiParameterComponentAttribute paramComponent:
 2164                    ProcessParameterComponent(variable, paramComponent);
 2165                    break;
 166                case OpenApiRequestBodyComponentAttribute requestBodyComponent:
 0167                    ProcessRequestBodyComponent(variable, requestBodyComponent);
 0168                    break;
 169                case OpenApiParameterExampleRefAttribute parameterExampleRef:
 0170                    ProcessParameterExampleRef(variable.Name, parameterExampleRef);
 0171                    break;
 172                case OpenApiRequestBodyExampleRefAttribute requestBodyExampleRef:
 0173                    ProcessRequestBodyExampleRef(variable.Name, requestBodyExampleRef);
 0174                    break;
 175                case OpenApiExtensionAttribute extensionAttribute:
 0176                    ProcessVariableExtension(variable, extensionAttribute);
 0177                    break;
 178                case InternalPowershellAttribute powershellAttribute:
 179                    // Process PowerShell attribute to modify the schema
 2180                    ProcessPowerShellAttribute(variable.Name, powershellAttribute);
 2181                    break;
 182                case OpenApiResponseComponentAttribute responseComponent:
 0183                    ProcessResponseComponent(variable, responseComponent);
 0184                    break;
 185                case OpenApiResponseHeaderRefAttribute headerRef:
 0186                    ProcessResponseHeaderRef(variable.Name, headerRef);
 0187                    break;
 188                case OpenApiResponseLinkRefAttribute linkRef:
 0189                    ProcessResponseLinkRef(variable.Name, linkRef);
 0190                    break;
 191                case OpenApiResponseExampleRefAttribute exampleRef:
 0192                    ProcessResponseExampleRef(variable.Name, exampleRef);
 193                    break;
 194                default:
 195                    break;
 196            }
 197        }
 2198    }
 199
 200    /// <summary>
 201    /// Processes an OpenAPI extension annotation for a given variable.
 202    /// </summary>
 203    /// <param name="variable"> The annotated variable containing annotations.</param>
 204    /// <param name="extensionAttribute"> The OpenAPI extension attribute to process.</param>
 205    private void ProcessVariableExtension(OpenApiComponentAnnotationScanner.AnnotatedVariable variable, OpenApiExtension
 206    {
 0207        var extensions = new Dictionary<string, IOpenApiExtension>(StringComparer.Ordinal);
 208
 0209        if (Host.Logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 210        {
 0211            Host.Logger.Debug("Applying OpenApiExtension '{extensionName}' to function metadata", extensionAttribute.Nam
 212        }
 213        // Parse string into a JsonNode tree.
 0214        var node = JsonNode.Parse(extensionAttribute.Json);
 0215        if (node is null)
 216        {
 0217            Host.Logger.Error("Error parsing OpenAPI extension '{extensionName}': JSON is null", extensionAttribute.Name
 0218            return;
 219        }
 0220        extensions[extensionAttribute.Name] = new JsonNodeExtension(node);
 0221        if (variable.Annotations.Any(a => a is OpenApiParameterComponentAttribute))
 222        {
 0223            var param = GetOrCreateParameterItem(variable.Name, false);
 0224            param.Extensions = extensions;
 225        }
 0226        else if (variable.Annotations.Any(a => a is OpenApiRequestBodyComponentAttribute))
 227        {
 0228            var requestBody = GetOrCreateRequestBodyItem(variable.Name, false);
 0229            requestBody.Extensions = extensions;
 230        }
 0231        else if (variable.Annotations.Any(a => a is OpenApiResponseComponentAttribute))
 232        {
 0233            var response = GetOrCreateResponseItem(variable.Name, false);
 0234            response.Extensions = extensions;
 235        }
 236        else
 237        {
 0238            Host.Logger.Error("OpenApiExtension '{extensionName}' could not be applied: no matching component found for 
 0239            return;
 240        }
 241    }
 242
 243    /// <summary>
 244    /// Tries to apply the variable type schema to the given OpenAPI response.
 245    /// </summary>
 246    /// <param name="response"> The OpenAPI response to apply the schema to.</param>
 247    /// <param name="variable"> The annotated variable containing annotations.</param>
 248    /// <param name="responseDescriptor"> The response component attribute describing the response.</param>
 249    /// <exception cref="InvalidOperationException"> Thrown if the response component does not specify any ContentType.<
 250    private void TryApplyVariableTypeSchema(
 251        OpenApiResponse response,
 252        OpenApiComponentAnnotationScanner.AnnotatedVariable variable,
 253        OpenApiResponseComponentAttribute responseDescriptor)
 254    {
 0255        if (variable.VariableType is null)
 256        {
 0257            return;
 258        }
 0259        var iSchema = InferPrimitiveSchema(variable.VariableType);
 0260        if (iSchema is OpenApiSchema schema)
 261        {
 262            // Apply any schema attributes from the parameter annotation
 0263            ApplyConcreteSchemaAttributes(responseDescriptor, schema);
 264            // Try to set default value from the variable initial value if not already set
 0265            if (!variable.NoDefault)
 266            {
 0267                schema.Default = OpenApiJsonNodeFactory.ToNode(variable.InitialValue);
 268            }
 269        }
 270
 271        // Either Schema OR Content, depending on ContentType
 0272        if (responseDescriptor.ContentType.Length == 0)
 273        {
 0274            throw new InvalidOperationException($"Response component '{variable.Name}' must specify at least one Content
 275        }
 276        // Use Content
 0277        response.Content ??= new Dictionary<string, IOpenApiMediaType>(StringComparer.Ordinal);
 0278        foreach (var contentType in responseDescriptor.ContentType)
 279        {
 0280            response.Content[contentType] = new OpenApiMediaType { Schema = iSchema };
 281        }
 0282    }
 283
 284    private static void ApplyResponseCommonFields(
 285        OpenApiResponse response,
 286        OpenApiResponseComponentAttribute responseDescriptor)
 287    {
 0288        if (responseDescriptor.Summary is not null)
 289        {
 0290            response.Summary = responseDescriptor.Summary;
 291        }
 0292        if (responseDescriptor.Description is not null)
 293        {
 0294            response.Description = responseDescriptor.Description;
 295        }
 0296    }
 297
 298    /// <summary>
 299    /// Generates the OpenAPI document by processing components and building paths and webhooks.
 300    /// </summary>
 301    /// <remarks>BuildCallbacks is already handled elsewhere.</remarks>
 302    /// <remarks>This method sets HasBeenGenerated to true after generation.</remarks>
 303    public void GenerateDoc()
 304    {
 305        // Then, generate webhooks
 2306        BuildWebhooks(WebHook);
 307
 308        // Finally, build paths from registered routes
 2309        BuildPathsFromRegisteredRoutes(Host.RegisteredRoutes);
 310
 2311        HasBeenGenerated = true;
 2312    }
 313
 314    /// <summary>
 315    /// Reads and diagnoses the OpenAPI document by serializing and re-parsing it.
 316    /// </summary>
 317    /// <param name="version">The OpenAPI specification version to read as.</param>
 318    /// <returns>A tuple containing the OpenAPI document and any diagnostics.</returns>
 319    public ReadResult ReadAndDiagnose(OpenApiSpecVersion version)
 320    {
 0321        using var sw = new StringWriter();
 0322        var w = new OpenApiJsonWriter(sw);
 0323        Document.SerializeAs(version, w);
 0324        using var ms = new MemoryStream(Encoding.UTF8.GetBytes(sw.ToString()));
 325        // format must be "json" or "yaml"
 0326        return OpenApiDocument.Load(ms);
 0327    }
 328
 329    /// <summary>
 330    /// Serializes the OpenAPI document to a JSON string.
 331    /// </summary>
 332    /// <param name="version">The OpenAPI specification version to serialize as.</param>
 333    /// <returns>The serialized JSON string.</returns>
 334    public string ToJson(OpenApiSpecVersion version)
 335    {
 6336        using var sw = new StringWriter();
 6337        var w = new OpenApiJsonWriter(sw);
 6338        Document.SerializeAs(version, w);
 6339        return sw.ToString();
 6340    }
 341
 342    /// <summary>
 343    /// Serializes the OpenAPI document to a YAML string.
 344    /// </summary>
 345    /// <param name="version">The OpenAPI specification version to serialize as.</param>
 346    /// <returns>The serialized YAML string.</returns>
 347    public string ToYaml(OpenApiSpecVersion version)
 348    {
 0349        using var sw = new StringWriter();
 0350        var w = new OpenApiYamlWriter(sw);
 0351        Document.SerializeAs(version, w);
 0352        return sw.ToString();
 0353    }
 354
 355    /// <summary>
 356    /// Creates an OpenAPI extension in the document from the provided extensions dictionary.
 357    /// </summary>
 358    /// <param name="extensions">A dictionary containing the extensions.</param>
 359    /// <exception cref="ArgumentException">Thrown when the specified extension name is not found in the provided extens
 360    public void AddOpenApiExtension(IDictionary? extensions)
 361    {
 0362        var built = BuildExtensions(extensions);
 363
 0364        if (built is null)
 365        {
 0366            return;
 367        }
 368
 0369        Document.Extensions ??= new Dictionary<string, IOpenApiExtension>(StringComparer.Ordinal);
 370
 0371        foreach (var kvp in built)
 372        {
 0373            Document.Extensions[kvp.Key] = kvp.Value;
 374        }
 0375    }
 376}

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)
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)
ApplyResponseAttribute(Kestrun.Hosting.Options.OpenAPIPathMetadata,IOpenApiResponseAttribute,Kestrun.Hosting.Options.MapRouteOptions)
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)
ApplyRequestBodyExampleRefAttribute(Kestrun.Hosting.Options.OpenAPIPathMetadata,OpenApiRequestBodyExampleRefAttribute)
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)
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>)
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.OpenApiSchema&,System.Boolean&)
TryGetSchemaItem(System.String,Microsoft.OpenApi.OpenApiSchema&)
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)
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>)
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)
ProcessTypeProperties(System.Type,Microsoft.OpenApi.OpenApiSchema,System.Collections.Generic.HashSet`1<System.Type>)
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)
ApplyExamplesAndDefaults(OpenApiProperties,Microsoft.OpenApi.OpenApiSchema)
ApplyXmlMetadata(OpenApiProperties,Microsoft.OpenApi.OpenApiSchema)
ApplyReferenceSchemaAttributes(OpenApiProperties,Microsoft.OpenApi.OpenApiSchemaReference)
ApplySecurityScheme(System.String,Kestrun.Authentication.IOpenApiAuthenticationOptions)
GetSecurityScheme(Kestrun.Authentication.ClientCertificateAuthenticationOptions)
GetSecurityScheme(Kestrun.Authentication.WindowsAuthOptions)
GetSecurityScheme(Kestrun.Authentication.OidcOptions)
GetSecurityScheme(Kestrun.Authentication.OAuth2Options)
GetSecurityScheme(Kestrun.Authentication.ApiKeyAuthenticationOptions)
GetSecurityScheme(Kestrun.Authentication.CookieAuthOptions)
GetSecurityScheme(Kestrun.Authentication.JwtAuthOptions)
GetSecurityScheme(Kestrun.Authentication.BasicAuthenticationOptions)
AddSecurityComponent(System.String,System.Boolean,Microsoft.OpenApi.OpenApiSecurityScheme)
AddTag(System.String,System.String,System.String,System.String,System.String,Microsoft.OpenApi.OpenApiExternalDocs,System.Collections.IDictionary)
AddTagIfMissing(Microsoft.OpenApi.OpenApiTag)
RemoveTag(System.String)
RemoveTag(Microsoft.OpenApi.OpenApiTag)
GetOrCreateTagItem(System.String)
TryGetTag(System.String,Microsoft.OpenApi.OpenApiTag&)
ContainsTag(System.String)
BuildWebhooks(System.Collections.Generic.Dictionary`2<System.ValueTuple`2<System.String,Kestrun.Utilities.HttpVerb>,Kestrun.Hosting.Options.OpenAPIPathMetadata>)
ProcessWebhookGroup(System.Linq.IGrouping`2<System.String,System.Collections.Generic.KeyValuePair`2<System.ValueTuple`2<System.String,Kestrun.Utilities.HttpVerb>,Kestrun.Hosting.Options.OpenAPIPathMetadata>>)
ProcessWebhookOperation(System.Collections.Generic.KeyValuePair`2<System.ValueTuple`2<System.String,Kestrun.Utilities.HttpVerb>,Kestrun.Hosting.Options.OpenAPIPathMetadata>,Microsoft.OpenApi.OpenApiPathItem)
GetOrCreateWebhookItem(System.String)
.cctor()
get_Host()
get_DocumentId()
get_Document()
get_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()
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)