diff --git a/src/Data/OpenApi/Internal.hs b/src/Data/OpenApi/Internal.hs index b9be5292..259a27e6 100644 --- a/src/Data/OpenApi/Internal.hs +++ b/src/Data/OpenApi/Internal.hs @@ -111,8 +111,9 @@ lowerOpenApiSpecVersion :: Version lowerOpenApiSpecVersion = makeVersion [3, 0, 0] -- | This is the upper version of the OpenApi Spec this library can parse or produce +-- Note: Extended to 3.1.x with partial support for 3.1 features (exclusiveMinimum/Maximum as numbers) upperOpenApiSpecVersion :: Version -upperOpenApiSpecVersion = makeVersion [3, 0, 3] +upperOpenApiSpecVersion = makeVersion [3, 1, 99] -- | The object provides metadata about the API. -- The metadata MAY be used by the clients if needed, @@ -627,6 +628,24 @@ type Format = Text type ParamName = Text +-- | Exclusive bound for minimum/maximum values. +-- In OpenAPI 3.0, this is a Bool indicating whether the bound is exclusive. +-- In OpenAPI 3.1, this is a Scientific value representing the exclusive bound itself. +data ExclusiveBound + = ExclusiveBool Bool -- ^ OpenAPI 3.0 style: true means the minimum/maximum is exclusive + | ExclusiveValue Scientific -- ^ OpenAPI 3.1 style: the actual exclusive bound value + deriving (Eq, Show, Generic, Data, Typeable) + +-- | Check if the bound is exclusive (for backward compatibility with 3.0 code) +isExclusive :: ExclusiveBound -> Bool +isExclusive (ExclusiveBool b) = b +isExclusive (ExclusiveValue _) = True + +-- | Get the exclusive value if using 3.1 style, Nothing for 3.0 style +exclusiveValue :: ExclusiveBound -> Maybe Scientific +exclusiveValue (ExclusiveValue v) = Just v +exclusiveValue (ExclusiveBool _) = Nothing + data Schema = Schema { _schemaTitle :: Maybe Text , _schemaDescription :: Maybe Text @@ -662,9 +681,9 @@ data Schema = Schema , _schemaFormat :: Maybe Format , _schemaItems :: Maybe OpenApiItems , _schemaMaximum :: Maybe Scientific - , _schemaExclusiveMaximum :: Maybe Bool + , _schemaExclusiveMaximum :: Maybe ExclusiveBound , _schemaMinimum :: Maybe Scientific - , _schemaExclusiveMinimum :: Maybe Bool + , _schemaExclusiveMinimum :: Maybe ExclusiveBound , _schemaMaxLength :: Maybe Integer , _schemaMinLength :: Maybe Integer , _schemaPattern :: Maybe Pattern @@ -1181,6 +1200,11 @@ instance ToJSON Style where instance ToJSON OpenApiType where toJSON = genericToJSON (jsonPrefix "Swagger") +-- | Serialize ExclusiveBound - uses 3.1 style (Number) for values, 3.0 style (Bool) for bools +instance ToJSON ExclusiveBound where + toJSON (ExclusiveBool b) = Bool b + toJSON (ExclusiveValue n) = Number n + instance ToJSON ParamLocation where toJSON = genericToJSON (jsonPrefix "Param") @@ -1236,6 +1260,12 @@ instance FromJSON Style where instance FromJSON OpenApiType where parseJSON = genericParseJSON (jsonPrefix "Swagger") +-- | Parse ExclusiveBound from either Bool (3.0) or Number (3.1) +instance FromJSON ExclusiveBound where + parseJSON (Bool b) = pure (ExclusiveBool b) + parseJSON (Number n) = pure (ExclusiveValue n) + parseJSON _ = empty + instance FromJSON ParamLocation where parseJSON = genericParseJSON (jsonPrefix "Param") diff --git a/src/Data/OpenApi/Internal/ParamSchema.hs b/src/Data/OpenApi/Internal/ParamSchema.hs index 75b637a2..18c0f882 100644 --- a/src/Data/OpenApi/Internal/ParamSchema.hs +++ b/src/Data/OpenApi/Internal/ParamSchema.hs @@ -133,7 +133,7 @@ instance ToParamSchema Natural where toParamSchema _ = mempty & type_ ?~ OpenApiInteger & minimum_ ?~ 0 - & exclusiveMinimum ?~ False + & exclusiveMinimum ?~ ExclusiveBool False instance ToParamSchema Int where toParamSchema = toParamSchemaBoundedIntegral instance ToParamSchema Int8 where toParamSchema = toParamSchemaBoundedIntegral diff --git a/src/Data/OpenApi/Internal/Schema/Validation.hs b/src/Data/OpenApi/Internal/Schema/Validation.hs index 5554ccf8..01cdcfad 100644 --- a/src/Data/OpenApi/Internal/Schema/Validation.hs +++ b/src/Data/OpenApi/Internal/Schema/Validation.hs @@ -319,16 +319,34 @@ validateInteger n = do validateNumber :: Scientific -> Validation Schema () validateNumber n = withConfig $ \_cfg -> withSchema $ \sch -> do - let exMax = Just True == sch ^. exclusiveMaximum - exMin = Just True == sch ^. exclusiveMinimum + -- Handle OpenAPI 3.0 style (exclusiveMinimum/Maximum as Bool modifying minimum/maximum) + let exMaxBool = case sch ^. exclusiveMaximum of + Just (ExclusiveBool True) -> True + _ -> False + exMinBool = case sch ^. exclusiveMinimum of + Just (ExclusiveBool True) -> True + _ -> False + + -- Handle OpenAPI 3.1 style (exclusiveMinimum/Maximum as the actual bound value) + case sch ^. exclusiveMaximum of + Just (ExclusiveValue m) -> + when (n >= m) $ + invalid ("value " ++ show n ++ " exceeds exclusive maximum (should be <" ++ show m ++ ")") + _ -> pure () + + case sch ^. exclusiveMinimum of + Just (ExclusiveValue m) -> + when (n <= m) $ + invalid ("value " ++ show n ++ " falls below exclusive minimum (should be >" ++ show m ++ ")") + _ -> pure () check maximum_ $ \m -> - when (if exMax then (n >= m) else (n > m)) $ - invalid ("value " ++ show n ++ " exceeds maximum (should be " ++ (if exMax then "<" else "<=") ++ show m ++ ")") + when (if exMaxBool then (n >= m) else (n > m)) $ + invalid ("value " ++ show n ++ " exceeds maximum (should be " ++ (if exMaxBool then "<" else "<=") ++ show m ++ ")") check minimum_ $ \m -> - when (if exMin then (n <= m) else (n < m)) $ - invalid ("value " ++ show n ++ " falls below minimum (should be " ++ (if exMin then ">" else ">=") ++ show m ++ ")") + when (if exMinBool then (n <= m) else (n < m)) $ + invalid ("value " ++ show n ++ " falls below minimum (should be " ++ (if exMinBool then ">" else ">=") ++ show m ++ ")") check multipleOf $ \k -> when (not (isInteger (n / k))) $ diff --git a/src/Data/OpenApi/Lens.hs b/src/Data/OpenApi/Lens.hs index b8e23101..1d3a4210 100644 --- a/src/Data/OpenApi/Lens.hs +++ b/src/Data/OpenApi/Lens.hs @@ -122,7 +122,7 @@ instance maximum_ = schema.maximum_ instance {-# OVERLAPPABLE #-} HasSchema s Schema - => HasExclusiveMaximum s (Maybe Bool) where + => HasExclusiveMaximum s (Maybe ExclusiveBound) where exclusiveMaximum = schema.exclusiveMaximum instance {-# OVERLAPPABLE #-} HasSchema s Schema @@ -130,7 +130,7 @@ instance {-# OVERLAPPABLE #-} HasSchema s Schema minimum_ = schema.minimum_ instance {-# OVERLAPPABLE #-} HasSchema s Schema - => HasExclusiveMinimum s (Maybe Bool) where + => HasExclusiveMinimum s (Maybe ExclusiveBound) where exclusiveMinimum = schema.exclusiveMinimum instance {-# OVERLAPPABLE #-} HasSchema s Schema diff --git a/src/Data/OpenApi/Optics.hs b/src/Data/OpenApi/Optics.hs index 3d0a42e8..b735038e 100644 --- a/src/Data/OpenApi/Optics.hs +++ b/src/Data/OpenApi/Optics.hs @@ -243,7 +243,7 @@ instance -- #exclusiveMaximum instance - ( a ~ Maybe Bool, b ~ Maybe Bool + ( a ~ Maybe ExclusiveBound, b ~ Maybe ExclusiveBound ) => LabelOptic "exclusiveMaximum" A_Lens NamedSchema NamedSchema a b where labelOptic = #schema % #exclusiveMaximum {-# INLINE labelOptic #-} @@ -259,7 +259,7 @@ instance -- #exclusiveMinimum instance - ( a ~ Maybe Bool, b ~ Maybe Bool + ( a ~ Maybe ExclusiveBound, b ~ Maybe ExclusiveBound ) => LabelOptic "exclusiveMinimum" A_Lens NamedSchema NamedSchema a b where labelOptic = #schema % #exclusiveMinimum {-# INLINE labelOptic #-}