Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix schema for Maybe #1

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ cabal.sandbox.config
*.aux
*.hp
.stack-work/
stack.yaml.lock
1 change: 0 additions & 1 deletion src/Data/OpenApi.hs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ module Data.OpenApi (

-- * Re-exports
module Data.OpenApi.Lens,
module Data.OpenApi.Optics,
module Data.OpenApi.Operation,
module Data.OpenApi.ParamSchema,
module Data.OpenApi.Schema,
Expand Down
2 changes: 1 addition & 1 deletion src/Data/OpenApi/Declare.hs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ instance (Applicative m, Monad m, Monoid d) => Applicative (DeclareT d m) where
return (mappend d' d'', f x)

instance (Applicative m, Monad m, Monoid d) => Monad (DeclareT d m) where
return x = DeclareT (\_ -> pure (mempty, x))
return = pure
DeclareT dx >>= f = DeclareT $ \d -> do
~(d', x) <- dx d
~(d'', y) <- runDeclareT (f x) (mappend d d')
Expand Down
20 changes: 12 additions & 8 deletions src/Data/OpenApi/Internal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

module Data.OpenApi.Internal where

import Prelude ()
Expand Down Expand Up @@ -335,7 +337,9 @@ instance Data MediaType where

dataTypeOf _ = mediaTypeData

mediaTypeConstr :: Constr
mediaTypeConstr = mkConstr mediaTypeData "MediaType" [] Prefix
mediaTypeData :: DataType
mediaTypeData = mkDataType "MediaType" [mediaTypeConstr]

instance Hashable MediaType where
Expand Down Expand Up @@ -1006,12 +1010,12 @@ deriveGeneric ''OpenApiSpecVersion
-- =======================================================================

instance Semigroup OpenApiSpecVersion where
(<>) (OpenApiSpecVersion a) (OpenApiSpecVersion b) = OpenApiSpecVersion $ max a b
(<>) (OpenApiSpecVersion a) (OpenApiSpecVersion b) = OpenApiSpecVersion $ max a b

instance Monoid OpenApiSpecVersion where
mempty = OpenApiSpecVersion (makeVersion [3,0,0])
mappend = (<>)

instance Semigroup OpenApi where
(<>) = genericMappend
instance Monoid OpenApi where
Expand Down Expand Up @@ -1282,7 +1286,7 @@ instance FromJSON OAuth2AuthorizationCodeFlow where
-- Manual ToJSON instances
-- =======================================================================

instance ToJSON OpenApiSpecVersion where
instance ToJSON OpenApiSpecVersion where
toJSON (OpenApiSpecVersion v)= toJSON . showVersion $ v

instance ToJSON MediaType where
Expand Down Expand Up @@ -1456,15 +1460,15 @@ instance FromJSON OpenApiSpecVersion where
parseJSON = withText "OpenApiSpecVersion" $ \str ->
let validatedVersion :: Either String Version
validatedVersion = do
parsedVersion <- readVersion str
parsedVersion <- readVersion str
unless ((parsedVersion >= lowerOpenApiSpecVersion) && (parsedVersion <= upperOpenApiSpecVersion)) $
Left ("The provided version " <> showVersion parsedVersion <> " is out of the allowed range >=" <> showVersion lowerOpenApiSpecVersion <> " && <=" <> showVersion upperOpenApiSpecVersion)
return parsedVersion
in
in
either fail (return . OpenApiSpecVersion) validatedVersion
where
readVersion :: Text -> Either String Version
readVersion v = case readP_to_S parseVersion (Text.unpack v) of
readVersion v = case readP_to_S parseVersion (Text.unpack v) of
[] -> Left $ "Failed to parse as a version string " <> Text.unpack v
solutions -> Right (fst . last $ solutions)

Expand Down Expand Up @@ -1649,7 +1653,7 @@ instance HasSwaggerAesonOptions Encoding where
instance HasSwaggerAesonOptions Link where
swaggerAesonOptions _ = mkSwaggerAesonOptions "link"

instance AesonDefaultValue Version where
instance AesonDefaultValue Version where
defaultValue = Just (makeVersion [3,0,0])
instance AesonDefaultValue OpenApiSpecVersion
instance AesonDefaultValue Server
Expand Down
1 change: 1 addition & 0 deletions src/Data/OpenApi/Internal/AesonUtils.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableSuperClasses #-}
module Data.OpenApi.Internal.AesonUtils (
-- * Generic functions
Expand Down
9 changes: 5 additions & 4 deletions src/Data/OpenApi/Internal/ParamSchema.hs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ module Data.OpenApi.Internal.ParamSchema where

import Control.Lens
import Data.Aeson (ToJSON (..))
import Data.Kind
import Data.Proxy
import GHC.Generics

Expand Down Expand Up @@ -163,7 +164,7 @@ instance ToParamSchema Word64 where
-- "minimum": -128,
-- "type": "integer"
-- }
toParamSchemaBoundedIntegral :: forall a t. (Bounded a, Integral a) => Proxy a -> Schema
toParamSchemaBoundedIntegral :: forall a. (Bounded a, Integral a) => Proxy a -> Schema
toParamSchemaBoundedIntegral _ = mempty
& type_ ?~ OpenApiInteger
& minimum_ ?~ fromInteger (toInteger (minBound :: a))
Expand Down Expand Up @@ -310,10 +311,10 @@ instance ToParamSchema UUID where
-- ],
-- "type": "string"
-- }
genericToParamSchema :: forall a t. (Generic a, GToParamSchema (Rep a)) => SchemaOptions -> Proxy a -> Schema
genericToParamSchema :: forall a. (Generic a, GToParamSchema (Rep a)) => SchemaOptions -> Proxy a -> Schema
genericToParamSchema opts _ = gtoParamSchema opts (Proxy :: Proxy (Rep a)) mempty

class GToParamSchema (f :: * -> *) where
class GToParamSchema (f :: Type -> Type) where
gtoParamSchema :: SchemaOptions -> Proxy f -> Schema -> Schema

instance GToParamSchema f => GToParamSchema (D1 d f) where
Expand All @@ -331,7 +332,7 @@ instance ToParamSchema c => GToParamSchema (K1 i c) where
instance (GEnumParamSchema f, GEnumParamSchema g) => GToParamSchema (f :+: g) where
gtoParamSchema opts _ = genumParamSchema opts (Proxy :: Proxy (f :+: g))

class GEnumParamSchema (f :: * -> *) where
class GEnumParamSchema (f :: Type -> Type) where
genumParamSchema :: SchemaOptions -> Proxy f -> Schema -> Schema

instance (GEnumParamSchema f, GEnumParamSchema g) => GEnumParamSchema (f :+: g) where
Expand Down
25 changes: 12 additions & 13 deletions src/Data/OpenApi/Internal/Schema.hs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ module Data.OpenApi.Internal.Schema where
import Prelude ()
import Prelude.Compat

import Control.Lens hiding (allOf)
import Control.Lens hiding (allOf, anyOf)
import Data.Data.Lens (template)

import Control.Applicative ((<|>))
Expand All @@ -43,6 +43,7 @@ import qualified Data.HashMap.Strict.InsOrd as InsOrdHashMap
import Data.Int
import Data.IntSet (IntSet)
import Data.IntMap (IntMap)
import Data.Kind
import Data.List (sort)
import Data.List.NonEmpty.Compat (NonEmpty)
import Data.Map (Map)
Expand Down Expand Up @@ -587,7 +588,7 @@ sketchStrictSchema = go . toJSON
where
names = objectKeys o

class GToSchema (f :: * -> *) where
class GToSchema (f :: Type -> Type) where
gdeclareNamedSchema :: SchemaOptions -> Proxy f -> Schema -> Declare (Definitions Schema) NamedSchema

instance {-# OVERLAPPABLE #-} ToSchema a => ToSchema [a] where
Expand Down Expand Up @@ -623,7 +624,10 @@ instance ToSchema Float where declareNamedSchema = plain . paramSchemaToSc
instance (Typeable (Fixed a), HasResolution a) => ToSchema (Fixed a) where declareNamedSchema = plain . paramSchemaToSchema

instance ToSchema a => ToSchema (Maybe a) where
declareNamedSchema _ = declareNamedSchema (Proxy :: Proxy a)
declareNamedSchema _ = do
ref <- declareSchemaRef (Proxy @a)
-- NB: using 'oneOf' goes wrong for nested Maybe's as both subschemas match 'null'.
pure $ unnamed $ mempty & anyOf ?~ [Inline $ mempty & type_ ?~ OpenApiNull, ref]

instance (ToSchema a, ToSchema b) => ToSchema (Either a b) where
-- To match Aeson instance
Expand Down Expand Up @@ -1016,10 +1020,7 @@ instance {-# OVERLAPPING #-} (Selector s, ToSchema c) => GToSchema (S1 s (K1 i (
instance {-# OVERLAPPABLE #-} (Selector s, GToSchema f) => GToSchema (S1 s f) where
gdeclareNamedSchema opts _ = fmap unnamed . withFieldSchema opts (Proxy2 :: Proxy2 s f) True

instance {-# OVERLAPPING #-} ToSchema c => GToSchema (K1 i (Maybe c)) where
gdeclareNamedSchema _ _ _ = declareNamedSchema (Proxy :: Proxy c)

instance {-# OVERLAPPABLE #-} ToSchema c => GToSchema (K1 i c) where
instance ToSchema c => GToSchema (K1 i c) where
gdeclareNamedSchema _ _ _ = declareNamedSchema (Proxy :: Proxy c)

instance ( GSumToSchema f
Expand All @@ -1031,7 +1032,9 @@ instance ( GSumToSchema f

gdeclareNamedSumSchema :: GSumToSchema f => SchemaOptions -> Proxy f -> Schema -> Declare (Definitions Schema) NamedSchema
gdeclareNamedSumSchema opts proxy _
| allNullaryToStringTag opts && allNullary = pure $ unnamed (toStringTag sumSchemas)
| allNullaryToStringTag opts && allNullary = pure $ unnamed $ mempty
& type_ ?~ OpenApiString
& enum_ ?~ map (String . fst) sumSchemas
| otherwise = do
(schemas, _) <- runWriterT declareSumSchema
return $ unnamed $ mempty
Expand All @@ -1040,13 +1043,9 @@ gdeclareNamedSumSchema opts proxy _
declareSumSchema = gsumToSchema opts proxy
(sumSchemas, All allNullary) = undeclare (runWriterT declareSumSchema)

toStringTag schemas = mempty
& type_ ?~ OpenApiString
& enum_ ?~ map (String . fst) sumSchemas

type AllNullary = All

class GSumToSchema (f :: * -> *) where
class GSumToSchema (f :: Type -> Type) where
gsumToSchema :: SchemaOptions -> Proxy f -> WriterT AllNullary (Declare (Definitions Schema)) [(T.Text, Referenced Schema)]

instance (GSumToSchema f, GSumToSchema g) => GSumToSchema (f :+: g) where
Expand Down
4 changes: 3 additions & 1 deletion src/Data/OpenApi/Internal/TypeShape.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -Wno-unticked-promoted-constructors #-}

module Data.OpenApi.Internal.TypeShape where

import Data.Kind
import Data.Proxy
import GHC.Generics
import GHC.TypeLits
Expand Down Expand Up @@ -46,7 +48,7 @@ type family GenericHasSimpleShape t (f :: Symbol) (s :: TypeShape) :: Constraint
)

-- | Infer a 'TypeShape' for a generic representation of a type.
type family GenericShape (g :: * -> *) :: TypeShape
type family GenericShape (g :: Type -> Type) :: TypeShape

type instance GenericShape (f :*: g) = ProdCombine (GenericShape f) (GenericShape g)
type instance GenericShape (f :+: g) = SumCombine (GenericShape f) (GenericShape g)
Expand Down
1 change: 1 addition & 0 deletions src/Data/OpenApi/Optics.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
-- |
Expand Down
2 changes: 2 additions & 0 deletions src/Data/OpenApi/Schema/Generator.hs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ schemaGen _ schema
| Just cases <- schema ^. enum_ = elements cases
schemaGen defns schema
| Just variants <- schema ^. oneOf = schemaGen defns =<< elements (dereference defns <$> variants)
schemaGen defns schema
| Just variants <- schema ^. anyOf = schemaGen defns =<< elements (dereference defns <$> variants)
schemaGen defns schema =
case schema ^. type_ of
Nothing ->
Expand Down
27 changes: 12 additions & 15 deletions src/Data/OpenApi/Schema/Validation.hs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,13 @@ import Data.OpenApi.Internal.Schema.Validation
-- $setup
-- >>> import Control.Lens
-- >>> import Data.Aeson
-- >>> import Data.Aeson.QQ.Simple
-- >>> import Data.Proxy
-- >>> import Data.OpenApi
-- >>> import Data.OpenApi.Declare
-- >>> import GHC.Generics
-- >>> :set -XDeriveGeneric
-- >>> :set -XQuasiQuotes

-- $howto
--
Expand Down Expand Up @@ -67,24 +70,18 @@ import Data.OpenApi.Internal.Schema.Validation

-- $maybe
--
-- Because @'Maybe' a@ has the same schema as @a@, validation
-- generally fails for @null@ JSON:
--
-- >>> validateToJSON (Nothing :: Maybe String)
-- ["expected JSON value of type OpenApiString"]
-- >>> validateToJSON ([Just "hello", Nothing] :: [Maybe String])
-- ["expected JSON value of type OpenApiString"]
-- >>> validateToJSON (123, Nothing :: Maybe String)
-- ["expected JSON value of type OpenApiString"]
--
-- However, when @'Maybe' a@ is a type of a record field,
-- validation takes @'required'@ property of the @'Schema'@
-- into account:
-- The behavior is in line with "aeson" behavior for derived instances.
-- When @'Maybe' a@ is a type of a record field,
-- validation accepts both ommited field and null as a field value:
--
-- >>> data Person = Person { name :: String, age :: Maybe Int } deriving Generic
-- >>> instance ToJSON Person
-- >>> instance ToSchema Person
-- >>> validateToJSON (Person "Nick" (Just 24))
-- >>> let (defs, sch) = runDeclare (declareSchema (Proxy :: Proxy Person)) mempty
-- >>> let validate = validateJSON defs sch
-- >>> validate [aesonQQ|{"name" : "Nick", "age" : 18}|]
-- []
-- >>> validate [aesonQQ|{"name" : "Nick", "age" : null}|]
-- []
-- >>> validateToJSON (Person "Nick" Nothing)
-- >>> validate [aesonQQ|{"name" : "Nick"}|]
-- []
10 changes: 3 additions & 7 deletions stack.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
resolver: lts-16.31
resolver: lts-21.24
packages:
- '.'
extra-deps:
- optics-core-0.3
- optics-th-0.3
- optics-extra-0.3
- indexed-profunctors-0.1
- insert-ordered-containers-0.2.3.1
ghc-options:
$locals: -Wall -Wno-unused-imports -Wno-dodgy-imports -Wno-name-shadowing
18 changes: 16 additions & 2 deletions test/Data/OpenApi/CommonTestTypes.hs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,14 @@ personSchemaJSON = [aesonQQ|
{
"name": { "type": "string" },
"phone": { "type": "integer" },
"email": { "type": "string" }
"email":
{
"anyOf" :
[
{ "type" : "null" },
{ "type": "string" }
]
}
},
"required": ["name", "phone"]
}
Expand Down Expand Up @@ -867,7 +874,14 @@ singleMaybeFieldSchemaJSON = [aesonQQ|
"type": "object",
"properties":
{
"singleMaybeField": { "type": "string" }
"singleMaybeField":
{
"anyOf" :
[
{ "type" : "null" },
{ "type": "string" }
]
}
}
}
|]
Expand Down
2 changes: 1 addition & 1 deletion test/Data/OpenApi/Schema/GeneratorSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ spec = do
prop "T.Text" $ shouldValidate (Proxy :: Proxy T.Text)
prop "TL.Text" $ shouldValidate (Proxy :: Proxy TL.Text)
prop "[String]" $ shouldValidate (Proxy :: Proxy [String])
-- prop "(Maybe [Int])" $ shouldValidate (Proxy :: Proxy (Maybe [Int]))
prop "(Maybe [Int])" $ shouldValidate (Proxy :: Proxy (Maybe [Int]))
prop "(IntMap String)" $ shouldValidate (Proxy :: Proxy (IntMap String))
prop "(Set Bool)" $ shouldValidate (Proxy :: Proxy (Set Bool))
prop "(NonEmpty Bool)" $ shouldValidate (Proxy :: Proxy (NonEmpty Bool))
Expand Down
Loading