From 50a079e6013d36be0f70d0468e3ce84cb85820bf Mon Sep 17 00:00:00 2001 From: Wolfgang Schuster Date: Wed, 18 Oct 2023 18:08:53 -0500 Subject: [PATCH 01/11] Mostly working errors --- src/CliMonad.elm | 45 +++- src/OpenApi/Generate.elm | 529 ++++++++++++++++++++++++++++++++++----- 2 files changed, 512 insertions(+), 62 deletions(-) diff --git a/src/CliMonad.elm b/src/CliMonad.elm index a384b79..b09a5f4 100644 --- a/src/CliMonad.elm +++ b/src/CliMonad.elm @@ -1,6 +1,32 @@ -module CliMonad exposing (CliMonad, Warning, andThen, andThen2, combine, combineMap, errorToWarning, fail, fromApiSpec, map, map2, map3, recordType, refToTypeName, run, stepOrFail, succeed, todo, todoWithDefault, typeToAnnotation, typeToAnnotationMaybe, withPath, withWarning) +module CliMonad exposing + ( CliMonad + , Warning + , andThen + , andThen2 + , combine + , combineDict + , combineMap + , errorToWarning + , fail + , fromApiSpec + , map + , map2 + , map3 + , recordType + , refToTypeName + , run + , stepOrFail + , succeed + , todo + , todoWithDefault + , typeToAnnotation + , typeToAnnotationMaybe + , withPath + , withWarning + ) import Common exposing (Field, Object, OneOfData, Type(..), TypeName, VariantName, toValueName) +import Dict import Elm import Elm.Annotation import FastDict exposing (Dict) @@ -404,3 +430,20 @@ refToTypeName ref = _ -> fail <| "Couldn't get the type ref (" ++ String.join "/" ref ++ ") for the response" + + +combineDict : Dict.Dict comparable (CliMonad a) -> CliMonad (Dict.Dict comparable a) +combineDict dict = + dict + |> Dict.toList + |> List.foldr + (\( k, vcm ) rescm -> + map2 + (\v res -> + ( k, v ) :: res + ) + vcm + rescm + ) + (succeed []) + |> map Dict.fromList diff --git a/src/OpenApi/Generate.elm b/src/OpenApi/Generate.elm index 22fa46b..87adad7 100644 --- a/src/OpenApi/Generate.elm +++ b/src/OpenApi/Generate.elm @@ -17,7 +17,9 @@ import Elm.ToString import FastDict import Gen.Basics import Gen.Bytes +import Gen.Bytes.Decode import Gen.Debug +import Gen.Dict import Gen.Http import Gen.Json.Decode import Gen.Json.Decode.Extra @@ -75,7 +77,9 @@ file { namespace, generateTodos } apiSpec = , CliMonad.succeed [ decodeOptionalField.declaration , responseToResult.declaration - , jsonResolver.declaration + , customHttpError + , expectJsonCustom.declaration + , jsonResolverCustom.declaration , whateverResolver.declaration , nullableType ] @@ -349,19 +353,20 @@ requestBodyToDeclarations name reference = toRequestFunctions : String -> String -> OpenApi.Operation.Operation -> CliMonad (List Elm.Declaration) toRequestFunctions method url operation = - operationToSuccessTypeAndExpectAndResolver operation + let + functionName : String + functionName = + (OpenApi.Operation.operationId operation + |> Maybe.withDefault url + ) + |> makeNamespaceValid + |> removeInvalidChars + |> String.Extra.camelize + in + operationToTypesExpectAndResolver functionName operation |> CliMonad.andThen - (\( successType, toExpect, resolver ) -> + (\{ successType, bodyTypeAnnotation, errorTypeDeclaration, errorTypeAnnotation, toExpect, resolver } -> let - functionName : String - functionName = - (OpenApi.Operation.operationId operation - |> Maybe.withDefault url - ) - |> makeNamespaceValid - |> removeInvalidChars - |> String.Extra.camelize - replacedUrl : CliMonad (Elm.Expression -> Elm.Expression) replacedUrl = let @@ -543,6 +548,8 @@ toRequestFunctions method url operation = { operation = operation , requireToMsg = True , successAnnotation = successAnnotation + , errorBodyAnnotation = bodyTypeAnnotation + , errorTypeAnnotation = errorTypeAnnotation , authorizationInfo = auth , bodyParams = bp } @@ -576,7 +583,12 @@ toRequestFunctions method url operation = , Elm.Annotation.function [ paramType ] (Gen.Task.annotation_.task - Gen.Http.annotation_.error + (Elm.Annotation.namedWith [] + "Error" + [ errorTypeAnnotation + , bodyTypeAnnotation + ] + ) successAnnotation ) ) @@ -588,6 +600,8 @@ toRequestFunctions method url operation = { operation = operation , requireToMsg = False , successAnnotation = successAnnotation + , errorBodyAnnotation = bodyTypeAnnotation + , errorTypeAnnotation = errorTypeAnnotation , authorizationInfo = auth , bodyParams = bp } @@ -649,6 +663,7 @@ toRequestFunctions method url operation = |> Elm.withType requestTskType |> Elm.declaration (functionName ++ "Task") |> Elm.withDocumentation doc + , errorTypeDeclaration ] ) documentation @@ -834,18 +849,28 @@ toConfigParamAnnotation : { operation : OpenApi.Operation.Operation , requireToMsg : Bool , successAnnotation : Elm.Annotation.Annotation + , errorBodyAnnotation : Elm.Annotation.Annotation + , errorTypeAnnotation : Elm.Annotation.Annotation , authorizationInfo : AuthorizationInfo , bodyParams : List ( String, Elm.Annotation.Annotation ) } -> CliMonad Elm.Annotation.Annotation -toConfigParamAnnotation { operation, requireToMsg, successAnnotation, authorizationInfo, bodyParams } = +toConfigParamAnnotation options = CliMonad.map (\urlParams -> - (authorizationInfo.params - ++ (if requireToMsg then + (options.authorizationInfo.params + ++ (if options.requireToMsg then [ ( "toMsg" , Elm.Annotation.function - [ Gen.Result.annotation_.result Gen.Http.annotation_.error successAnnotation ] + [ Gen.Result.annotation_.result + (Elm.Annotation.namedWith [] + "Error" + [ options.errorTypeAnnotation + , options.errorBodyAnnotation + ] + ) + options.successAnnotation + ] (Elm.Annotation.var "msg") ) ] @@ -853,12 +878,12 @@ toConfigParamAnnotation { operation, requireToMsg, successAnnotation, authorizat else [] ) - ++ bodyParams + ++ options.bodyParams ++ urlParams ) |> CliMonad.recordType ) - (operationToUrlParams operation) + (operationToUrlParams options.operation) operationToUrlParams : OpenApi.Operation.Operation -> CliMonad (List ( String, Elm.Annotation.Annotation )) @@ -1137,16 +1162,31 @@ toConcreteParam param = ) -operationToSuccessTypeAndExpectAndResolver : OpenApi.Operation.Operation -> CliMonad ( Type, (Elm.Expression -> Elm.Expression) -> Elm.Expression, Elm.Expression ) -operationToSuccessTypeAndExpectAndResolver operation = +operationToTypesExpectAndResolver : + String + -> OpenApi.Operation.Operation + -> + CliMonad + { successType : Type + , bodyTypeAnnotation : Elm.Annotation.Annotation + , errorTypeDeclaration : Elm.Declaration + , errorTypeAnnotation : Elm.Annotation.Annotation + , toExpect : (Elm.Expression -> Elm.Expression) -> Elm.Expression + , resolver : Elm.Expression + } +operationToTypesExpectAndResolver functionName operation = let responses : Dict.Dict String (OpenApi.Reference.ReferenceOr OpenApi.Response.Response) responses = OpenApi.Operation.responses operation - expectJson : Elm.Expression -> ((Elm.Expression -> Elm.Expression) -> Elm.Expression) + expectJson : Elm.Expression -> (Elm.Expression -> Elm.Expression) -> Elm.Expression expectJson decoder toMsg = Gen.Http.expectJson toMsg decoder + + expectJsonBetter : Elm.Expression -> Elm.Expression -> (Elm.Expression -> Elm.Expression) -> Elm.Expression + expectJsonBetter errorDecoders successDecoder toMsg = + expectJsonCustom.call (toMsg (Elm.val "")) errorDecoders successDecoder in CliMonad.succeed responses |> CliMonad.stepOrFail @@ -1157,6 +1197,155 @@ operationToSuccessTypeAndExpectAndResolver operation = getFirstSuccessResponse |> CliMonad.andThen (\( _, responseOrRef ) -> + let + errorResponses : Dict.Dict String (OpenApi.Reference.ReferenceOr OpenApi.Response.Response) + errorResponses = + getErrorResponses responses + + toErrorVariant : String -> String + toErrorVariant statusCode = + String.Extra.toSentenceCase functionName ++ "_" ++ statusCode + + errorDecoders : CliMonad Elm.Expression + errorDecoders = + errorResponses + |> Dict.map + (\statusCode errRespOrRef -> + case OpenApi.Reference.toConcrete errRespOrRef of + Just errResp -> + OpenApi.Response.content errResp + |> contentToContentSchema + |> CliMonad.andThen + (\contentSchema -> + case contentSchema of + JsonContent type_ -> + typeToDecoder type_ + |> CliMonad.map + (toErrorVariant statusCode + |> Elm.val + |> Gen.Json.Decode.call_.map + ) + + StringContent _ -> + CliMonad.succeed (Gen.Debug.todo "decode string err?") + |> CliMonad.map + (toErrorVariant statusCode + |> Elm.val + |> Gen.Json.Decode.call_.map + ) + + BytesContent _ -> + CliMonad.succeed (Gen.Debug.todo "decode bytes err?") + |> CliMonad.map + (toErrorVariant statusCode + |> Elm.val + |> Gen.Json.Decode.call_.map + ) + + EmptyContent -> + CliMonad.succeed (Gen.Debug.todo "decode empty err?") + |> CliMonad.map + (toErrorVariant statusCode + |> Elm.val + |> Gen.Json.Decode.call_.map + ) + ) + + Nothing -> + CliMonad.succeed errRespOrRef + |> CliMonad.stepOrFail "I found an error response, but I couldn't convert it to a concrete decoder" + OpenApi.Reference.toReference + |> CliMonad.andThen + (\ref -> + let + inner : String + inner = + OpenApi.Reference.ref ref + in + CliMonad.refToTypeName (String.split "/" inner) + |> CliMonad.map + (\typeName -> + Elm.val ("decode" ++ typeName) + ) + |> CliMonad.map + (toErrorVariant statusCode + |> Elm.val + |> Gen.Json.Decode.call_.map + ) + ) + ) + |> CliMonad.combineDict + |> CliMonad.map + (\decoders -> + Dict.toList decoders + |> List.map (\( k, v ) -> Elm.tuple (Elm.string k) v) + |> Elm.list + |> Gen.Dict.call_.fromList + ) + + errorTypeDeclaration : CliMonad ( Elm.Declaration, Elm.Annotation.Annotation ) + errorTypeDeclaration = + errorResponses + |> Dict.map + (\_ errRespOrRef -> + case OpenApi.Reference.toConcrete errRespOrRef of + Just errResp -> + OpenApi.Response.content errResp + |> contentToContentSchema + |> CliMonad.andThen + (\contentSchema -> + case contentSchema of + JsonContent type_ -> + CliMonad.typeToAnnotation type_ + + StringContent _ -> + CliMonad.succeed Elm.Annotation.string + + BytesContent _ -> + CliMonad.succeed Gen.Bytes.annotation_.bytes + + EmptyContent -> + CliMonad.succeed Elm.Annotation.unit + ) + + Nothing -> + CliMonad.succeed errRespOrRef + |> CliMonad.stepOrFail "I found an error response, but I couldn't convert it to a concrete annotation" + OpenApi.Reference.toReference + |> CliMonad.andThen + (\ref -> + let + inner : String + inner = + OpenApi.Reference.ref ref + in + CliMonad.refToTypeName (String.split "/" inner) + |> CliMonad.map + (\typeName -> + Elm.Annotation.named [] typeName + ) + ) + ) + |> CliMonad.combineDict + |> CliMonad.map + (\dict -> + let + errorName : String + errorName = + String.Extra.toSentenceCase functionName ++ "_Error" + in + ( dict + |> Dict.toList + |> List.map (\( statusCode, annotation ) -> Elm.variantWith (toErrorVariant statusCode) [ annotation ]) + |> Elm.customType errorName + |> Elm.exposeWith + { exposeConstructor = True + , group = Just "Types" + } + , Elm.Annotation.named [] errorName + ) + ) + in case OpenApi.Reference.toConcrete responseOrRef of Just response -> OpenApi.Response.content response @@ -1165,35 +1354,58 @@ operationToSuccessTypeAndExpectAndResolver operation = (\contentSchema -> case contentSchema of JsonContent type_ -> - CliMonad.map - (\decoder -> - ( type_ - , expectJson decoder - , jsonResolver.call decoder - ) + CliMonad.map3 + (\successDecoder errorDecoders_ ( errorTypeDeclaration_, errorTypeAnnotation ) -> + { successType = type_ + , bodyTypeAnnotation = Elm.Annotation.string + , errorTypeDeclaration = errorTypeDeclaration_ + , errorTypeAnnotation = errorTypeAnnotation + , toExpect = expectJsonBetter errorDecoders_ successDecoder + , resolver = jsonResolverCustom.call errorDecoders_ successDecoder + } ) (typeToDecoder type_) + errorDecoders + errorTypeDeclaration StringContent _ -> - CliMonad.succeed - ( String - , Gen.Http.expectString - , Gen.Http.stringResolver responseToResult.call + CliMonad.map + (\( errorTypeDeclaration_, errorTypeAnnotation ) -> + { successType = String + , bodyTypeAnnotation = Elm.Annotation.string + , errorTypeDeclaration = errorTypeDeclaration_ + , errorTypeAnnotation = errorTypeAnnotation + , toExpect = Gen.Http.expectString + , resolver = Gen.Http.stringResolver responseToResult.call + } ) + errorTypeDeclaration BytesContent _ -> - CliMonad.succeed - ( Bytes - , \toMsg -> Gen.Http.expectBytes toMsg Gen.Basics.values_.identity - , Gen.Http.bytesResolver responseToResult.call + CliMonad.map + (\( errorTypeDeclaration_, errorTypeAnnotation ) -> + { successType = Bytes + , bodyTypeAnnotation = Gen.Bytes.annotation_.bytes + , errorTypeDeclaration = errorTypeDeclaration_ + , errorTypeAnnotation = errorTypeAnnotation + , toExpect = \toMsg -> Gen.Http.expectBytes toMsg Gen.Basics.values_.identity + , resolver = Gen.Http.bytesResolver responseToResult.call + } ) + errorTypeDeclaration EmptyContent -> - CliMonad.succeed - ( Unit - , Gen.Http.expectWhatever - , whateverResolver.call [] + CliMonad.map + (\( errorTypeDeclaration_, errorTypeAnnotation ) -> + { successType = Unit + , bodyTypeAnnotation = Elm.Annotation.unit + , errorTypeDeclaration = errorTypeDeclaration_ + , errorTypeAnnotation = errorTypeAnnotation + , toExpect = Gen.Http.expectWhatever + , resolver = whateverResolver.call [] + } ) + errorTypeDeclaration ) Nothing -> @@ -1208,43 +1420,232 @@ operationToSuccessTypeAndExpectAndResolver operation = OpenApi.Reference.ref ref in CliMonad.refToTypeName (String.split "/" inner) - |> CliMonad.map - (\typeName -> - ( Common.ref inner - , expectJson <| Elm.val ("decode" ++ typeName) - , jsonResolver.call <| Elm.val ("decode" ++ typeName) - ) + |> CliMonad.map3 + (\errorDecoders_ ( errorTypeDeclaration_, errorTypeAnnotation ) typeName -> + { successType = Common.ref inner + , bodyTypeAnnotation = Elm.Annotation.string + , errorTypeDeclaration = errorTypeDeclaration_ + , errorTypeAnnotation = errorTypeAnnotation + , toExpect = expectJsonBetter errorDecoders_ (Elm.val ("decode" ++ typeName)) + , resolver = jsonResolverCustom.call errorDecoders_ <| Elm.val ("decode" ++ typeName) + } ) + errorDecoders + errorTypeDeclaration ) ) -jsonResolver : +customHttpError : Elm.Declaration +customHttpError = + Elm.customType "Error" + [ Elm.variantWith "BadUrl" [ Elm.Annotation.string ] + , Elm.variant "Timeout" + , Elm.variant "NetworkError" + , Elm.variantWith "KnownBadStatus" [ Elm.Annotation.int, Elm.Annotation.var "err" ] + , Elm.variantWith "UnknownBadStatus" [ Gen.Http.annotation_.metadata, Elm.Annotation.var "body" ] + , Elm.variantWith "BadErrorBody" [ Gen.Http.annotation_.metadata, Elm.Annotation.var "body" ] + , Elm.variantWith "BadBody" [ Gen.Http.annotation_.metadata, Elm.Annotation.var "body" ] + ] + |> Elm.exposeWith + { exposeConstructor = True + , group = Just "Types" + } + + +expectJsonCustom : + { declaration : Elm.Declaration + , call : Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression + , callFrom : List String -> Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression + , value : List String -> Elm.Expression + } +expectJsonCustom = + Elm.Declare.fn3 "expectJsonCustom" + ( "toMsg" + , Just + (Elm.Annotation.function + [ Gen.Result.annotation_.result + (Elm.Annotation.namedWith [] "Error" [ Elm.Annotation.var "err", Elm.Annotation.string ]) + (Elm.Annotation.var "success") + ] + (Elm.Annotation.var "msg") + ) + ) + ( "errorDecoders" + , Just + (Gen.Dict.annotation_.dict + Gen.String.annotation_.string + (Gen.Json.Decode.annotation_.decoder (Elm.Annotation.var "err")) + ) + ) + ( "successDecoder" + , Just + (Gen.Json.Decode.annotation_.decoder (Elm.Annotation.var "success")) + ) + (\toMsg errorDecoders successDecoder -> + Gen.Http.expectStringResponse (\result -> Elm.apply toMsg [ result ]) <| + \response -> + -- NOTE: The below results in unexpected codegen like: + -- + -- Http.BadUrl_ string.String -> + -- Result.Err (BadUrl string.String) + -- + -- so we manually write this codegen. + -- + -- Gen.Http.caseOf_.response response + -- { badUrl_ = \url -> Gen.Result.make_.err (Elm.apply (Elm.val "BadUrl") [ url ]) + -- , timeout_ = Gen.Result.make_.err (Elm.val "Timeout") + -- , networkError_ = Gen.Result.make_.err (Elm.val "NetworkError") + -- , badStatus_ = + -- \metadata body -> + -- Gen.Maybe.caseOf_.maybe + -- (Gen.Dict.call_.get (Gen.String.call_.fromInt (Elm.get "statusCode" metadata)) errorDecoders) + -- { nothing = Gen.Result.make_.err (Elm.apply (Elm.val "UnknownBadStatus") [ metadata, body ]) + -- , just = + -- \errorDecoder -> + -- Gen.Result.caseOf_.result + -- (Gen.Json.Decode.call_.decodeString errorDecoder body) + -- { ok = \x -> Gen.Result.make_.err (Elm.apply (Elm.val "KnownBadStatus") [ Elm.get "statusCode" metadata, x ]) + -- , err = \_ -> Gen.Result.make_.err (Elm.apply (Elm.val "BadErrorBody") [ metadata, body ]) + -- } + -- } + -- , goodStatus_ = + -- \metadata body -> + -- Gen.Result.caseOf_.result + -- (Gen.Json.Decode.call_.decodeString successDecoder body) + -- { err = \_ -> Gen.Result.make_.err (Elm.apply (Elm.val "BadBody") [ metadata, body ]) + -- , ok = \a -> Gen.Result.make_.ok a + -- } + -- } + Elm.Case.custom response + (Gen.Http.annotation_.response Elm.Annotation.string) + [ Elm.Case.branch1 "BadUrl_" + ( "url", Elm.Annotation.string ) + (\url -> Gen.Result.make_.err (Elm.apply (Elm.val "BadUrl") [ url ])) + , Elm.Case.branch0 "Timeout_" + (Gen.Result.make_.err (Elm.val "Timeout")) + , Elm.Case.branch0 "NetworkError_" + (Gen.Result.make_.err (Elm.val "NetworkError")) + , Elm.Case.branch2 "BadStatus_" + ( "metadata", Gen.Http.annotation_.metadata ) + ( "body", Elm.Annotation.string ) + (\metadata body -> + Gen.Maybe.caseOf_.maybe + (Gen.Dict.call_.get (Gen.String.call_.fromInt (Elm.get "statusCode" metadata)) errorDecoders) + { nothing = Gen.Result.make_.err (Elm.apply (Elm.val "UnknownBadStatus") [ metadata, body ]) + , just = + \errorDecoder -> + Gen.Result.caseOf_.result + (Gen.Json.Decode.call_.decodeString errorDecoder body) + { ok = \x -> Gen.Result.make_.err (Elm.apply (Elm.val "KnownBadStatus") [ Elm.get "statusCode" metadata, x ]) + , err = \_ -> Gen.Result.make_.err (Elm.apply (Elm.val "BadErrorBody") [ metadata, body ]) + } + } + ) + , Elm.Case.branch2 "GoodStatus_" + ( "metadata", Gen.Http.annotation_.metadata ) + ( "body", Elm.Annotation.string ) + (\metadata body -> + Gen.Result.caseOf_.result + (Gen.Json.Decode.call_.decodeString successDecoder body) + { err = \_ -> Gen.Result.make_.err (Elm.apply (Elm.val "BadBody") [ metadata, body ]) + , ok = \a -> Gen.Result.make_.ok a + } + ) + ] + ) + + +jsonResolverCustom : { declaration : Elm.Declaration - , call : Elm.Expression -> Elm.Expression - , callFrom : List String -> Elm.Expression -> Elm.Expression + , call : Elm.Expression -> Elm.Expression -> Elm.Expression + , callFrom : List String -> Elm.Expression -> Elm.Expression -> Elm.Expression , value : List String -> Elm.Expression } -jsonResolver = - Elm.Declare.fn "jsonResolver" - ( "decoder" - , Just <| Gen.Json.Decode.annotation_.decoder (Elm.Annotation.var "t") +jsonResolverCustom = + Elm.Declare.fn2 "jsonResolverCustom" + ( "errorDecoders" + , Just + (Gen.Dict.annotation_.dict + Gen.String.annotation_.string + (Gen.Json.Decode.annotation_.decoder (Elm.Annotation.var "err")) + ) + ) + ( "successDecoder" + , Just + (Gen.Json.Decode.annotation_.decoder (Elm.Annotation.var "success")) ) <| - \decoder -> + \errorDecoders successDecoder -> Gen.Http.stringResolver (\response -> - response - |> responseToResult.call - |> Gen.Result.andThen - (\body -> - body - |> Gen.Json.Decode.call_.decodeString decoder - |> Gen.Result.mapError (\err -> Gen.Http.make_.badBody (Gen.Json.Decode.errorToString err)) + Elm.Case.custom response + (Gen.Http.annotation_.response Elm.Annotation.string) + [ Elm.Case.branch1 "BadUrl_" + ( "url", Elm.Annotation.string ) + (\url -> Gen.Result.make_.err (Elm.apply (Elm.val "BadUrl") [ url ])) + , Elm.Case.branch0 "Timeout_" + (Gen.Result.make_.err (Elm.val "Timeout")) + , Elm.Case.branch0 "NetworkError_" + (Gen.Result.make_.err (Elm.val "NetworkError")) + , Elm.Case.branch2 "BadStatus_" + ( "metadata", Gen.Http.annotation_.metadata ) + ( "body", Elm.Annotation.string ) + (\metadata body -> + Gen.Maybe.caseOf_.maybe + (Gen.Dict.call_.get (Gen.String.call_.fromInt (Elm.get "statusCode" metadata)) errorDecoders) + { nothing = Gen.Result.make_.err (Elm.apply (Elm.val "UnknownBadStatus") [ metadata, body ]) + , just = + \errorDecoder -> + Gen.Result.caseOf_.result + (Gen.Json.Decode.call_.decodeString errorDecoder body) + { ok = \x -> Gen.Result.make_.err (Elm.apply (Elm.val "KnownBadStatus") [ Elm.get "statusCode" metadata, x ]) + , err = \_ -> Gen.Result.make_.err (Elm.apply (Elm.val "BadErrorBody") [ metadata, body ]) + } + } + ) + , Elm.Case.branch2 "GoodStatus_" + ( "metadata", Gen.Http.annotation_.metadata ) + ( "body", Elm.Annotation.string ) + (\metadata body -> + Gen.Result.caseOf_.result + (Gen.Json.Decode.call_.decodeString successDecoder body) + { err = \_ -> Gen.Result.make_.err (Elm.apply (Elm.val "BadBody") [ metadata, body ]) + , ok = \a -> Gen.Result.make_.ok a + } ) + ] ) + +-- NOTE: Maybe we keep this around and let people choose the type of error handling they want? +-- jsonResolver : +-- { declaration : Elm.Declaration +-- , call : Elm.Expression -> Elm.Expression +-- , callFrom : List String -> Elm.Expression -> Elm.Expression +-- , value : List String -> Elm.Expression +-- } +-- jsonResolver = +-- Elm.Declare.fn "jsonResolver" +-- ( "decoder" +-- , Just <| Gen.Json.Decode.annotation_.decoder (Elm.Annotation.var "t") +-- ) +-- <| +-- \decoder -> +-- Gen.Http.stringResolver +-- (\response -> +-- response +-- |> responseToResult.call +-- |> Gen.Result.andThen +-- (\body -> +-- body +-- |> Gen.Json.Decode.call_.decodeString decoder +-- |> Gen.Result.mapError (\err -> Gen.Http.make_.badBody (Gen.Json.Decode.errorToString err)) +-- ) +-- ) + + whateverResolver : { declaration : Elm.Declaration , call : List Elm.Expression -> Elm.Expression @@ -1280,6 +1681,12 @@ getFirstSuccessResponse responses = |> List.head +getErrorResponses : Dict.Dict String (OpenApi.Reference.ReferenceOr OpenApi.Response.Response) -> Dict.Dict String (OpenApi.Reference.ReferenceOr OpenApi.Response.Response) +getErrorResponses responses = + responses + |> Dict.filter (\status _ -> not <| isSuccessResponseStatus status) + + nullableType : Elm.Declaration nullableType = Elm.customType "Nullable" From c0e36a26e52ee425c892075e54ad48e45bcdd406 Mon Sep 17 00:00:00 2001 From: Wolfgang Schuster Date: Thu, 19 Oct 2023 21:44:21 -0500 Subject: [PATCH 02/11] Fix exposing error types --- src/OpenApi/Generate.elm | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/OpenApi/Generate.elm b/src/OpenApi/Generate.elm index 87adad7..d0c1e53 100644 --- a/src/OpenApi/Generate.elm +++ b/src/OpenApi/Generate.elm @@ -148,15 +148,7 @@ pathDeclarations = ) |> CliMonad.map (List.filterMap identity >> List.concat) ) - |> CliMonad.map - (List.concat - >> List.map - (Elm.exposeWith - { exposeConstructor = False - , group = Just "Request functions" - } - ) - ) + |> CliMonad.map List.concat ) @@ -659,10 +651,18 @@ toRequestFunctions method url operation = |> Elm.withType requestCmdType |> Elm.declaration functionName |> Elm.withDocumentation doc + |> Elm.exposeWith + { exposeConstructor = False + , group = Just "Request functions" + } , requestTsk |> Elm.withType requestTskType |> Elm.declaration (functionName ++ "Task") |> Elm.withDocumentation doc + |> Elm.exposeWith + { exposeConstructor = False + , group = Just "Request functions" + } , errorTypeDeclaration ] ) From c74ef2c674e19cb3c976376770c835f5eb8f4406 Mon Sep 17 00:00:00 2001 From: Wolfgang Schuster Date: Tue, 31 Oct 2023 18:39:00 -0500 Subject: [PATCH 03/11] Thank you @miniBill --- src/CliMonad.elm | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/CliMonad.elm b/src/CliMonad.elm index b09a5f4..69267f5 100644 --- a/src/CliMonad.elm +++ b/src/CliMonad.elm @@ -435,9 +435,8 @@ refToTypeName ref = combineDict : Dict.Dict comparable (CliMonad a) -> CliMonad (Dict.Dict comparable a) combineDict dict = dict - |> Dict.toList - |> List.foldr - (\( k, vcm ) rescm -> + |> Dict.foldr + (\k vcm rescm -> map2 (\v res -> ( k, v ) :: res From 6d1b21742f8af985402fb0dc18842108b19dd467 Mon Sep 17 00:00:00 2001 From: Wolfgang Schuster Date: Tue, 31 Oct 2023 18:40:14 -0500 Subject: [PATCH 04/11] Remove excess parens --- src/OpenApi/Generate.elm | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/OpenApi/Generate.elm b/src/OpenApi/Generate.elm index d0c1e53..b869318 100644 --- a/src/OpenApi/Generate.elm +++ b/src/OpenApi/Generate.elm @@ -348,9 +348,8 @@ toRequestFunctions method url operation = let functionName : String functionName = - (OpenApi.Operation.operationId operation + OpenApi.Operation.operationId operation |> Maybe.withDefault url - ) |> makeNamespaceValid |> removeInvalidChars |> String.Extra.camelize From 366c6e02af8e4f7b5b842ed2cae20c037ce3d053 Mon Sep 17 00:00:00 2001 From: Wolfgang Schuster Date: Tue, 31 Oct 2023 19:09:11 -0500 Subject: [PATCH 05/11] Dedupe annotation --- src/OpenApi/Generate.elm | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/OpenApi/Generate.elm b/src/OpenApi/Generate.elm index b869318..f0b6f84 100644 --- a/src/OpenApi/Generate.elm +++ b/src/OpenApi/Generate.elm @@ -574,12 +574,7 @@ toRequestFunctions method url operation = , Elm.Annotation.function [ paramType ] (Gen.Task.annotation_.task - (Elm.Annotation.namedWith [] - "Error" - [ errorTypeAnnotation - , bodyTypeAnnotation - ] - ) + (customErrorAnnotation errorTypeAnnotation bodyTypeAnnotation) successAnnotation ) ) @@ -671,6 +666,15 @@ toRequestFunctions method url operation = |> CliMonad.withPath url +customErrorAnnotation : Elm.Annotation.Annotation -> Elm.Annotation.Annotation -> Elm.Annotation.Annotation +customErrorAnnotation errorTypeAnnotation bodyTypeAnnotation = + Elm.Annotation.namedWith [] + "Error" + [ errorTypeAnnotation + , bodyTypeAnnotation + ] + + operationToAuthorizationInfo : OpenApi.Operation.Operation -> CliMonad AuthorizationInfo operationToAuthorizationInfo operation = let @@ -862,12 +866,7 @@ toConfigParamAnnotation options = [ ( "toMsg" , Elm.Annotation.function [ Gen.Result.annotation_.result - (Elm.Annotation.namedWith [] - "Error" - [ options.errorTypeAnnotation - , options.errorBodyAnnotation - ] - ) + (customErrorAnnotation options.errorTypeAnnotation options.errorBodyAnnotation) options.successAnnotation ] (Elm.Annotation.var "msg") From 8df8c8628f5ab73059443556f3aa969c2eff7a3f Mon Sep 17 00:00:00 2001 From: Wolfgang Schuster Date: Wed, 1 Nov 2023 10:10:00 -0500 Subject: [PATCH 06/11] More cleanup --- src/OpenApi/Generate.elm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenApi/Generate.elm b/src/OpenApi/Generate.elm index f0b6f84..5c82b0d 100644 --- a/src/OpenApi/Generate.elm +++ b/src/OpenApi/Generate.elm @@ -1225,7 +1225,7 @@ operationToTypesExpectAndResolver functionName operation = ) StringContent _ -> - CliMonad.succeed (Gen.Debug.todo "decode string err?") + CliMonad.succeed Gen.Json.Decode.string |> CliMonad.map (toErrorVariant statusCode |> Elm.val @@ -1241,7 +1241,7 @@ operationToTypesExpectAndResolver functionName operation = ) EmptyContent -> - CliMonad.succeed (Gen.Debug.todo "decode empty err?") + CliMonad.succeed (Gen.Json.Decode.succeed Elm.unit) |> CliMonad.map (toErrorVariant statusCode |> Elm.val From e1f7e75f65f3079118589f740c655862b4e0a33c Mon Sep 17 00:00:00 2001 From: Wolfgang Schuster Date: Wed, 8 Nov 2023 08:17:22 -0700 Subject: [PATCH 07/11] Improved handling of toMsg --- src/OpenApi/Generate.elm | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/OpenApi/Generate.elm b/src/OpenApi/Generate.elm index 5c82b0d..109fc13 100644 --- a/src/OpenApi/Generate.elm +++ b/src/OpenApi/Generate.elm @@ -487,7 +487,7 @@ toRequestFunctions method url operation = expect : Elm.Expression -> Elm.Expression expect config = - toExpect (\toMsgArg -> Elm.apply (Elm.get "toMsg" config) [ toMsgArg ]) + toExpect (Elm.get "toMsg" config) bodyParams : ContentSchema -> CliMonad (List ( String, Elm.Annotation.Annotation )) bodyParams contentSchema = @@ -1169,7 +1169,7 @@ operationToTypesExpectAndResolver : , bodyTypeAnnotation : Elm.Annotation.Annotation , errorTypeDeclaration : Elm.Declaration , errorTypeAnnotation : Elm.Annotation.Annotation - , toExpect : (Elm.Expression -> Elm.Expression) -> Elm.Expression + , toExpect : Elm.Expression -> Elm.Expression , resolver : Elm.Expression } operationToTypesExpectAndResolver functionName operation = @@ -1178,13 +1178,9 @@ operationToTypesExpectAndResolver functionName operation = responses = OpenApi.Operation.responses operation - expectJson : Elm.Expression -> (Elm.Expression -> Elm.Expression) -> Elm.Expression - expectJson decoder toMsg = - Gen.Http.expectJson toMsg decoder - - expectJsonBetter : Elm.Expression -> Elm.Expression -> (Elm.Expression -> Elm.Expression) -> Elm.Expression + expectJsonBetter : Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression expectJsonBetter errorDecoders successDecoder toMsg = - expectJsonCustom.call (toMsg (Elm.val "")) errorDecoders successDecoder + expectJsonCustom.call toMsg errorDecoders successDecoder in CliMonad.succeed responses |> CliMonad.stepOrFail @@ -1373,7 +1369,7 @@ operationToTypesExpectAndResolver functionName operation = , bodyTypeAnnotation = Elm.Annotation.string , errorTypeDeclaration = errorTypeDeclaration_ , errorTypeAnnotation = errorTypeAnnotation - , toExpect = Gen.Http.expectString + , toExpect = Gen.Http.call_.expectString , resolver = Gen.Http.stringResolver responseToResult.call } ) @@ -1386,7 +1382,7 @@ operationToTypesExpectAndResolver functionName operation = , bodyTypeAnnotation = Gen.Bytes.annotation_.bytes , errorTypeDeclaration = errorTypeDeclaration_ , errorTypeAnnotation = errorTypeAnnotation - , toExpect = \toMsg -> Gen.Http.expectBytes toMsg Gen.Basics.values_.identity + , toExpect = \toMsg -> Gen.Http.call_.expectBytes toMsg Gen.Basics.values_.identity , resolver = Gen.Http.bytesResolver responseToResult.call } ) @@ -1399,7 +1395,7 @@ operationToTypesExpectAndResolver functionName operation = , bodyTypeAnnotation = Elm.Annotation.unit , errorTypeDeclaration = errorTypeDeclaration_ , errorTypeAnnotation = errorTypeAnnotation - , toExpect = Gen.Http.expectWhatever + , toExpect = Gen.Http.call_.expectWhatever , resolver = whateverResolver.call [] } ) From ca8b5399ccb3a00422b053871ade73fed74cb111 Mon Sep 17 00:00:00 2001 From: Wolfgang Schuster Date: Mon, 13 Nov 2023 17:28:37 -0600 Subject: [PATCH 08/11] Add publish script to make building not as easily forgotten --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 162b4a0..3aab2f6 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "review:watch": "elm-review --watch --fix", "format": "elm-format src tests --validate", "test": "elm-test", - "test:watch": "elm-test --watch" + "test:watch": "elm-test --watch", + "publish": "npm run build && npm publish" }, "author": "Wolfgang Schuster", "license": "MIT", From 50b3ccb3f3c7b4a5be793aa1fef46ebd834a6e81 Mon Sep 17 00:00:00 2001 From: Wolfgang Schuster Date: Fri, 17 Nov 2023 09:05:10 -0600 Subject: [PATCH 09/11] Fix elm-review error --- src/OpenApi/Generate.elm | 1 - 1 file changed, 1 deletion(-) diff --git a/src/OpenApi/Generate.elm b/src/OpenApi/Generate.elm index 109fc13..8d609e8 100644 --- a/src/OpenApi/Generate.elm +++ b/src/OpenApi/Generate.elm @@ -17,7 +17,6 @@ import Elm.ToString import FastDict import Gen.Basics import Gen.Bytes -import Gen.Bytes.Decode import Gen.Debug import Gen.Dict import Gen.Http From 2a7a9638b2ff222de4df3cbf076f872a78d04faa Mon Sep 17 00:00:00 2001 From: Wolfgang Schuster Date: Fri, 17 Nov 2023 09:14:02 -0600 Subject: [PATCH 10/11] start releasing new features as beta --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3aab2f6..5702425 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "elm-open-api", - "version": "0.1.2", + "version": "0.1.3-beta.1", "description": "A tool for generating Elm SDKs from OpenAPI Specs.", "main": "dist/elm-open-api.js", "bin": "dist/elm-open-api.js", From 8ef21453029417ec116fa8f2a1699ed31d9f2681 Mon Sep 17 00:00:00 2001 From: Wolfgang Schuster Date: Fri, 17 Nov 2023 09:24:16 -0600 Subject: [PATCH 11/11] Add a changelog --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..22ffa87 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,31 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.3-beta.1] - 2023-11-17 + +### Added + +- Custom error handling: + - Now generates a custom error type for each endpoint that has a non-2xx responses. + +## [0.1.2] - 2023-11-13 + +### Fixed + +- I published the wrong build for 0.1.1, this published the correct build. See 0.1.1 for changes. + +## [0.1.1] - 2023-11-01 + +### Fixed + +- [incorrect parsing of JSON files](https://github.com/wolfadex/elm-open-api-cli/issues/53) + + - Resolved by https://github.com/wolfadex/elm-open-api-cli/pull/54/, by @lawik + +## [0.1.0] - 2023-10-06 + +### Initial release