diff --git a/autodocodec-api-usage/src/Autodocodec/Usage.hs b/autodocodec-api-usage/src/Autodocodec/Usage.hs index ac6cd60..47863fc 100644 --- a/autodocodec-api-usage/src/Autodocodec/Usage.hs +++ b/autodocodec-api-usage/src/Autodocodec/Usage.hs @@ -86,6 +86,7 @@ data Example = Example exampleOptionalOrNull :: !(Maybe Text), exampleOptionalWithDefault :: !Text, exampleOptionalWithNullDefault :: ![Text], + exampleSingleOrList :: ![Text], exampleFruit :: !Fruit } deriving (Show, Eq, Generic) @@ -109,6 +110,7 @@ instance HasCodec Example where <*> optionalFieldOrNull "optional-or-null" "an optional-or-null text" .= exampleOptionalOrNull <*> optionalFieldWithDefault "optional-with-default" "foobar" "an optional text with a default" .= exampleOptionalWithDefault <*> optionalFieldWithOmittedDefault "optional-with-null-default" [] "an optional list of texts with a default empty list where the empty list would be omitted" .= exampleOptionalWithNullDefault + <*> optionalFieldWithOmittedDefaultWith "single-or-list" (singleOrListCodec codec) [] "an optional list that can also be specified as a single element" .= exampleSingleOrList <*> requiredField "fruit" "fruit!!" .= exampleFruit instance ToJSON Example where @@ -129,6 +131,11 @@ instance ToJSON Example where ], [ "optional-with-null-default" JSON..= exampleOptionalWithNullDefault | not (null exampleOptionalWithNullDefault) + ], + [ case exampleSingleOrList of + [e] -> "single-or-list" JSON..= e + l -> "single-or-list" JSON..= l + | not (null exampleSingleOrList) ] ] @@ -142,6 +149,9 @@ instance FromJSON Example where <*> o JSON..:? "optional-or-null" <*> o JSON..:? "optional-with-default" JSON..!= "foobar" <*> o JSON..:? "optional-with-null-default" JSON..!= [] + <*> ( ((: []) <$> o JSON..: "single-or-list") + <|> (o JSON..:? "single-or-list" JSON..!= []) + ) <*> o JSON..: "fruit" -- | A simple Recursive type diff --git a/autodocodec-api-usage/test_resources/json-schema/example.json b/autodocodec-api-usage/test_resources/json-schema/example.json index 3425e91..e6d0296 100644 --- a/autodocodec-api-usage/test_resources/json-schema/example.json +++ b/autodocodec-api-usage/test_resources/json-schema/example.json @@ -19,6 +19,20 @@ } ] }, + "single-or-list": { + "$comment": "an optional list that can also be specified as a single element", + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, "text": { "$comment": "a text", "type": "string" diff --git a/autodocodec-api-usage/test_resources/openapi-schema/example.json b/autodocodec-api-usage/test_resources/openapi-schema/example.json index 5369f53..b4e4734 100644 --- a/autodocodec-api-usage/test_resources/openapi-schema/example.json +++ b/autodocodec-api-usage/test_resources/openapi-schema/example.json @@ -23,6 +23,21 @@ "additionalProperties": true, "description": "a maybe text" }, + "single-or-list": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "additionalProperties": true, + "description": "an optional list that can also be specified as a single element" + }, "text": { "type": "string", "description": "a text" diff --git a/autodocodec-api-usage/test_resources/show-codec/example.txt b/autodocodec-api-usage/test_resources/show-codec/example.txt index fc9766e..f7f9ff3 100644 --- a/autodocodec-api-usage/test_resources/show-codec/example.txt +++ b/autodocodec-api-usage/test_resources/show-codec/example.txt @@ -1 +1 @@ -ObjectOfCodec (Just "Example") (ApCodec (ApCodec (ApCodec (ApCodec (ApCodec (ApCodec (ApCodec (BimapCodec _ _ (RequiredKeyCodec "text" (Just "a text") (StringCodec Nothing))) (BimapCodec _ _ (RequiredKeyCodec "bool" (Just "a bool") (BoolCodec Nothing)))) (BimapCodec _ _ (RequiredKeyCodec "maybe" (Just "a maybe text") (BimapCodec _ _ (EitherCodec NullCodec (StringCodec Nothing)))))) (BimapCodec _ _ (OptionalKeyCodec "optional" (Just "an optional text") (StringCodec Nothing)))) (BimapCodec _ _ (OptionalKeyCodec "optional-or-null" (Just "an optional-or-null text") (BimapCodec _ _ (EitherCodec NullCodec (StringCodec Nothing)))))) (BimapCodec _ _ (OptionalKeyWithDefaultCodec "optional-with-default" (StringCodec Nothing) _ (Just "an optional text with a default")))) (BimapCodec _ _ (OptionalKeyWithOmittedDefaultCodec "optional-with-null-default" (BimapCodec _ _ (ArrayOfCodec Nothing (StringCodec Nothing))) _ (Just "an optional list of texts with a default empty list where the empty list would be omitted")))) (BimapCodec _ _ (RequiredKeyCodec "fruit" (Just "fruit!!") (BimapCodec _ _ (EitherCodec (BimapCodec _ _ (EqCodec "Apple" (StringCodec Nothing))) (BimapCodec _ _ (EitherCodec (BimapCodec _ _ (EqCodec "Orange" (StringCodec Nothing))) (BimapCodec _ _ (EitherCodec (BimapCodec _ _ (EqCodec "Banana" (StringCodec Nothing))) (BimapCodec _ _ (EqCodec "Melon" (StringCodec Nothing)))))))))))) \ No newline at end of file +ObjectOfCodec (Just "Example") (ApCodec (ApCodec (ApCodec (ApCodec (ApCodec (ApCodec (ApCodec (ApCodec (BimapCodec _ _ (RequiredKeyCodec "text" (Just "a text") (StringCodec Nothing))) (BimapCodec _ _ (RequiredKeyCodec "bool" (Just "a bool") (BoolCodec Nothing)))) (BimapCodec _ _ (RequiredKeyCodec "maybe" (Just "a maybe text") (BimapCodec _ _ (EitherCodec NullCodec (StringCodec Nothing)))))) (BimapCodec _ _ (OptionalKeyCodec "optional" (Just "an optional text") (StringCodec Nothing)))) (BimapCodec _ _ (OptionalKeyCodec "optional-or-null" (Just "an optional-or-null text") (BimapCodec _ _ (EitherCodec NullCodec (StringCodec Nothing)))))) (BimapCodec _ _ (OptionalKeyWithDefaultCodec "optional-with-default" (StringCodec Nothing) _ (Just "an optional text with a default")))) (BimapCodec _ _ (OptionalKeyWithOmittedDefaultCodec "optional-with-null-default" (BimapCodec _ _ (ArrayOfCodec Nothing (StringCodec Nothing))) _ (Just "an optional list of texts with a default empty list where the empty list would be omitted")))) (BimapCodec _ _ (OptionalKeyWithOmittedDefaultCodec "single-or-list" (BimapCodec _ _ (EitherCodec (StringCodec Nothing) (BimapCodec _ _ (ArrayOfCodec Nothing (StringCodec Nothing))))) _ (Just "an optional list that can also be specified as a single element")))) (BimapCodec _ _ (RequiredKeyCodec "fruit" (Just "fruit!!") (BimapCodec _ _ (EitherCodec (BimapCodec _ _ (EqCodec "Apple" (StringCodec Nothing))) (BimapCodec _ _ (EitherCodec (BimapCodec _ _ (EqCodec "Orange" (StringCodec Nothing))) (BimapCodec _ _ (EitherCodec (BimapCodec _ _ (EqCodec "Banana" (StringCodec Nothing))) (BimapCodec _ _ (EqCodec "Melon" (StringCodec Nothing)))))))))))) \ No newline at end of file diff --git a/autodocodec-api-usage/test_resources/swagger-schema/example.json b/autodocodec-api-usage/test_resources/swagger-schema/example.json index 5232be0..e400f39 100644 --- a/autodocodec-api-usage/test_resources/swagger-schema/example.json +++ b/autodocodec-api-usage/test_resources/swagger-schema/example.json @@ -19,6 +19,10 @@ "additionalProperties": true, "description": "a maybe text" }, + "single-or-list": { + "additionalProperties": true, + "description": "an optional list that can also be specified as a single element" + }, "text": { "type": "string", "description": "a text" diff --git a/autodocodec-api-usage/test_resources/yaml-schema/example.txt b/autodocodec-api-usage/test_resources/yaml-schema/example.txt index 1c1d4f9..a156b9b 100644 --- a/autodocodec-api-usage/test_resources/yaml-schema/example.txt +++ b/autodocodec-api-usage/test_resources/yaml-schema/example.txt @@ -26,6 +26,12 @@ # default: [] # an optional list of texts with a default empty list where the empty list would be omitted -  +single-or-list: # optional + # default: [] + # an optional list that can also be specified as a single element + [  + , -  + ] fruit: # required # fruit!! [ Apple diff --git a/autodocodec/src/Autodocodec.hs b/autodocodec/src/Autodocodec.hs index fa3bc62..67b065f 100644 --- a/autodocodec/src/Autodocodec.hs +++ b/autodocodec/src/Autodocodec.hs @@ -60,6 +60,9 @@ module Autodocodec maybeCodec, eitherCodec, listCodec, + nonEmptyCodec, + singleOrListCodec, + singleOrNonEmptyCodec, vectorCodec, valueCodec, nullCodec, diff --git a/autodocodec/src/Autodocodec/Codec.hs b/autodocodec/src/Autodocodec/Codec.hs index 15a3937..46849fb 100644 --- a/autodocodec/src/Autodocodec/Codec.hs +++ b/autodocodec/src/Autodocodec/Codec.hs @@ -513,6 +513,68 @@ nonEmptyCodec = bimapCodec parseNonEmptyList NE.toList . listCodec Nothing -> Left "Expected a nonempty list, but got an empty list." Just ne -> Right ne +-- | Like 'listCodec', except the values may also be simplified as a single value. +-- +-- During parsing, a single element may be parsed as the list of just that element. +-- During rendering, a list with only one element will be rendered as just that element. +-- +-- === Example usage +-- +-- >>> let c = singleOrListCodec codec :: JSONCodec [Int] +-- >>> toJSONVia c [5] +-- Number 5.0 +-- >>> toJSONVia c [5,6] +-- Array [Number 5.0,Number 6.0] +-- >>> JSON.parseMaybe (parseJSONVia c) (Number 5) :: Maybe [Int] +-- Just [5] +-- >>> JSON.parseMaybe (parseJSONVia c) (Array [Number 5, Number 6]) :: Maybe [Int] +-- Just [5,6] +-- +-- === WARNING +-- +-- If you use nested lists, for example when the given value codec is also a +-- 'listCodec', you may get in trouble with ambiguities during parsing. +singleOrListCodec :: ValueCodec input output -> ValueCodec [input] [output] +singleOrListCodec c = dimapCodec f g $ eitherCodec c $ listCodec c + where + f = \case + Left v -> [v] + Right vs -> vs + g = \case + [v] -> Left v + vs -> Right vs + +-- | Like 'nonEmptyCodec', except the values may also be simplified as a single value. +-- +-- During parsing, a single element may be parsed as the list of just that element. +-- During rendering, a list with only one element will be rendered as just that element. +-- +-- === Example usage +-- +-- >>> let c = singleOrNonEmptyCodec codec :: JSONCodec (NonEmpty Int) +-- >>> toJSONVia c (5 :| []) +-- Number 5.0 +-- >>> toJSONVia c (5 :| [6]) +-- Array [Number 5.0,Number 6.0] +-- >>> JSON.parseMaybe (parseJSONVia c) (Number 5) :: Maybe (NonEmpty Int) +-- Just (5 :| []) +-- >>> JSON.parseMaybe (parseJSONVia c) (Array [Number 5, Number 6]) :: Maybe (NonEmpty Int) +-- Just (5 :| [6]) +-- +-- === WARNING +-- +-- If you use nested lists, for example when the given value codec is also a +-- 'nonEmptyCodec', you may get in trouble with ambiguities during parsing. +singleOrNonEmptyCodec :: ValueCodec input output -> ValueCodec (NonEmpty input) (NonEmpty output) +singleOrNonEmptyCodec c = dimapCodec f g $ eitherCodec c $ nonEmptyCodec c + where + f = \case + Left v -> v :| [] + Right vs -> vs + g = \case + v :| [] -> Left v + vs -> Right vs + -- | A required field -- -- During decoding, the field must be in the object.