From db427d7289c996f94f9eff0316a5f1677fa3823a Mon Sep 17 00:00:00 2001 From: Adam DiCarlo Date: Tue, 18 Feb 2025 16:35:19 -0800 Subject: [PATCH 1/2] Allow telling whether an Operation specifies `security` When an operation does not include the `security` key, it means "use the value of the top-level `security` key." When an operation *does* specify `security`, its value overrides the top-level `security` value. We need to be able to tell these cases apart in order to support operations overriding the global security setting. Here's how the spec documents this, in its description of the `security` field, at https://swagger.io/specification/v3/#operation-object: > A declaration of which security mechanisms can be used for this operation. [...] This definition overrides any declared top-level security. To remove a top-level security declaration, an empty array can be used. --- src/OpenApi/Operation.elm | 8 ++++++-- src/OpenApi/Types.elm | 7 ++++--- tests/Test/OpenApi/Operation.elm | 33 ++++++++++++++++++++++++++++++-- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/OpenApi/Operation.elm b/src/OpenApi/Operation.elm index 8bbc175..09b341a 100644 --- a/src/OpenApi/Operation.elm +++ b/src/OpenApi/Operation.elm @@ -135,8 +135,12 @@ deprecated (Operation operation_) = operation_.deprecated -{-| -} -security : Operation -> List SecurityRequirement +{-| SecurityRequirements the operation specifies (possibly an empty list), or Nothing if the operation does not specify any. + +If an operation specifies `security`, the given value (even if it's an empty array) overrides the schema's top-level `security`. This is documented in the OpenAPI specification here: . + +-} +security : Operation -> Maybe (List SecurityRequirement) security (Operation operation_) = operation_.security diff --git a/src/OpenApi/Types.elm b/src/OpenApi/Types.elm index a3bcc19..c7471d1 100644 --- a/src/OpenApi/Types.elm +++ b/src/OpenApi/Types.elm @@ -1083,7 +1083,7 @@ type alias OperationInternal = , responses : Dict String (ReferenceOr Response) , callbacks : Dict String (ReferenceOr Callback) , deprecated : Bool - , security : List SecurityRequirement + , security : Maybe (List SecurityRequirement) , servers : List Server } @@ -1116,7 +1116,7 @@ decodeOperation = |> decodeOptionalDict "responses" (decodeRefOr decodeResponse) |> Json.Decode.Pipeline.optional "callbacks" (Json.Decode.dict (decodeRefOr decodeCallback)) Dict.empty |> Json.Decode.Pipeline.optional "deprecated" Json.Decode.bool False - |> Json.Decode.Pipeline.optional "security" (Json.Decode.list decodeSecurityRequirement) [] + |> optionalNothing "security" (Json.Decode.list decodeSecurityRequirement) |> Json.Decode.Pipeline.optional "servers" (Json.Decode.list decodeServer) [] |> Json.Decode.andThen (\operation -> @@ -1140,7 +1140,8 @@ encodeOperation (Operation operation) = , Just ( "responses", Json.Encode.dict identity (encodeRefOr encodeResponse) operation.responses ) , Internal.maybeEncodeDictField ( "callbacks", identity, encodeRefOr encodeCallback ) operation.callbacks , Just ( "deprecated", Json.Encode.bool operation.deprecated ) - , Internal.maybeEncodeListField ( "security", encodeSecurityRequirement ) operation.security + , operation.security + |> Maybe.map (\security -> ( "security", Json.Encode.list encodeSecurityRequirement security )) , Internal.maybeEncodeListField ( "servers", encodeServer ) operation.servers ] |> List.filterMap identity diff --git a/tests/Test/OpenApi/Operation.elm b/tests/Test/OpenApi/Operation.elm index a0bc274..461ce82 100644 --- a/tests/Test/OpenApi/Operation.elm +++ b/tests/Test/OpenApi/Operation.elm @@ -3,6 +3,7 @@ module Test.OpenApi.Operation exposing (suite) import Dict import Expect import Json.Decode +import Json.Encode import OpenApi.Operation import Test exposing (..) @@ -68,8 +69,22 @@ suite = , test "security" <| \() -> decodedOperation - |> Result.map (OpenApi.Operation.security >> List.length) - |> Expect.equal (Ok 1) + |> Result.map (OpenApi.Operation.security >> Maybe.map List.length) + |> Expect.equal (Ok (Just 1)) + , describe "when 'security' is unspecified" <| + [ test "it decodes to Nothing" <| + \() -> + Json.Decode.decodeString OpenApi.Operation.decode securityUnspecifiedExample + |> Result.map OpenApi.Operation.security + |> Expect.equal (Ok Nothing) + , test "it is not encoded" <| + \() -> + Json.Decode.decodeString OpenApi.Operation.decode securityUnspecifiedExample + |> Result.map (OpenApi.Operation.encode >> Json.Encode.encode 0) + |> Result.andThen (Json.Decode.decodeString OpenApi.Operation.decode) + |> Result.map OpenApi.Operation.security + |> Expect.equal (Ok Nothing) + ] , test "servers" <| \() -> decodedOperation @@ -148,6 +163,20 @@ example = }""" +securityUnspecifiedExample : String +securityUnspecifiedExample = + """{ + "summary": "Updates a pet in the store with form data", + "operationId": "updatePetWithForm", + "responses": { + "200": { + "description": "Pet updated.", + "content": {} + } + } +}""" + + failingExample : String failingExample = """{ From f749ac0568d64d12219bf599f802d08231ad1427 Mon Sep 17 00:00:00 2001 From: Adam DiCarlo Date: Tue, 18 Feb 2025 18:20:39 -0800 Subject: [PATCH 2/2] Add test to ensure security: [] isn't decoded to Nothing --- tests/Test/OpenApi/Operation.elm | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Test/OpenApi/Operation.elm b/tests/Test/OpenApi/Operation.elm index 461ce82..d79c5bf 100644 --- a/tests/Test/OpenApi/Operation.elm +++ b/tests/Test/OpenApi/Operation.elm @@ -71,6 +71,11 @@ suite = decodedOperation |> Result.map (OpenApi.Operation.security >> Maybe.map List.length) |> Expect.equal (Ok (Just 1)) + , test "security = []" <| + \() -> + Json.Decode.decodeString OpenApi.Operation.decode securityIsEmptyArrayExample + |> Result.map OpenApi.Operation.security + |> Expect.equal (Ok (Just [])) , describe "when 'security' is unspecified" <| [ test "it decodes to Nothing" <| \() -> @@ -163,6 +168,21 @@ example = }""" +securityIsEmptyArrayExample : String +securityIsEmptyArrayExample = + """{ + "summary": "Updates a pet in the store with form data", + "operationId": "updatePetWithForm", + "security": [], + "responses": { + "200": { + "description": "Pet updated.", + "content": {} + } + } +}""" + + securityUnspecifiedExample : String securityUnspecifiedExample = """{