packages feed

compaREST-0.1.0.0: src/Data/OpenApi/Compare/Validate/Schema/Issues.hs

{-# OPTIONS_GHC -Wno-orphans #-}

module Data.OpenApi.Compare.Validate.Schema.Issues
  ( Issue (..),
    Behave (..),
  )
where

import qualified Data.Aeson as A
import Data.OpenApi
import Data.OpenApi.Compare.Behavior
import Data.OpenApi.Compare.Subtree
import Data.OpenApi.Compare.Validate.Schema.JsonFormula
import Data.OpenApi.Compare.Validate.Schema.Partition
import Data.OpenApi.Compare.Validate.Schema.TypedJson
import Data.Scientific
import Data.Text (Text)
import qualified Data.Text as T
import Text.Pandoc.Builder hiding (Format, Null)

instance Issuable 'TypedSchemaLevel where
  data Issue 'TypedSchemaLevel
    = -- | producer produces a specific value ($1), consumer has a condition that is not satisfied by said value
      EnumDoesntSatisfy A.Value
    | -- | consumer only expects a specific value which the producer does not produce.
      NoMatchingEnum A.Value
    | -- | consumer declares a maximum numeric value ($1), producer doesn't
      NoMatchingMaximum (Bound Scientific)
    | -- | consumer declares a maximum numeric value ($1), producer declares a weaker (higher) limit ($2)
      MatchingMaximumWeak (ProdCons (Bound Scientific))
    | -- | consumer declares a minimum numeric value, producer doesn't
      NoMatchingMinimum (Bound Scientific)
    | -- | consumer declares a minimum numeric value ($1), producer declares a weaker (lower) limit ($2)
      MatchingMinimumWeak (ProdCons (Bound Scientific))
    | -- | consumer declares that the numeric value must be a multiple of $1, producer doesn't
      NoMatchingMultipleOf Scientific
    | -- | consumer declares that the numeric value must be a multiple of $1, producer declares a weaker condition (multiple of $2)
      MatchingMultipleOfWeak (ProdCons Scientific)
    | -- | consumer declares a string/number format, producer declares none or a different format (TODO: improve via regex #32)
      NoMatchingFormat Format
    | -- | consumer declares a maximum length of the string ($1), producer doesn't.
      NoMatchingMaxLength Integer
    | -- | consumer declares a maximum length of the string ($1), producer declares a weaker (higher) limit ($2)
      MatchingMaxLengthWeak (ProdCons Integer)
    | -- | consumer declares a minimum length of the string ($1), producer doesn't.
      NoMatchingMinLength Integer
    | -- | consumer declares a minimum length of the string ($1), producer declares a weaker (lower) limit ($2)
      MatchingMinLengthWeak (ProdCons Integer)
    | -- | consumer declares the string value must match a regex ($1), producer doesn't declare or declares different regex (TODO: #32)
      NoMatchingPattern Pattern
    | -- | consumer declares the items of an array must satisfy some condition, producer doesn't
      NoMatchingItems
    | -- | producer and consumer declare that an array must be a tuple of a fixed length, but the lengths don't match
      TupleItemsLengthChanged (ProdCons Integer)
    | -- | consumer declares that the array is a tuple, but the producer doesn't, the length constraints match, but there were issues with the components
      ArrayToTuple
    | -- | producer declares that the array is a tuple, but the consumer doesn't, and there were issues with the components
      TupleToArray
    | -- | consumer declares that the array is a tuple, but the producer doesn't, and there aren't sufficient length constraints
      NoMatchingTupleItems
    | -- | consumer declares a maximum length of the array ($1), producer doesn't.
      NoMatchingMaxItems Integer
    | -- | consumer declares a maximum length of the array ($1), producer declares a weaker (higher) limit ($2)
      MatchingMaxItemsWeak (ProdCons Integer)
    | -- | consumer declares a minimum length of the array ($1), producer doesn't.
      NoMatchingMinItems Integer
    | -- | consumer declares a minimum length of the array ($1), producer declares a weaker (lower) limit ($2)
      MatchingMinItemsWeak (ProdCons Integer)
    | -- | consumer declares that items must be unique, producer doesn't
      NoMatchingUniqueItems
    | -- | consumer declares the properties of an object must satisfy some condition, producer doesn't
      NoMatchingProperties
    | -- | producer allows additional properties, consumer doesn't
      NoAdditionalProperties
    | -- | consumer declares a maximum number of properties in the object ($1), producer doesn't.
      NoMatchingMaxProperties Integer
    | -- | consumer declares a maximum number of properties in the object ($1), producer declares a weaker (higher) limit ($2)
      MatchingMaxPropertiesWeak (ProdCons Integer)
    | -- | consumer declares a minimum number of properties in the object ($1), producer doesn't.
      NoMatchingMinProperties Integer
    | -- | consumer declares a minimum number of properties in the object ($1), producer declares a weaker (lower) limit ($2)
      MatchingMinPropertiesWeak (ProdCons Integer)
    | -- | producer declares that the value must satisfy a disjunction of some conditions, but consumer's requirements couldn't be matched against any single one of them (TODO: split heuristic #71)
      NoMatchingCondition (Maybe Partition) [SomeCondition]
    | -- | consumer indicates that no values of this type are allowed, but we weren't able to conclude that in the producer (currently only immediate contradictions are checked, c.f. #70)
      TypeBecomesEmpty
    | -- | consumer indicates that no values in a particular partition are allowed, but we weren't able to conclude this in the producer
      PartitionBecomesEmpty Partition
    deriving stock (Eq, Ord, Show)
  issueKind = \case
    NoMatchingEnum _ -> ProbablyIssue
    MatchingMaximumWeak _ -> ProbablyIssue -- interplay with MultipleOf could make this not an issue
    MatchingMinimumWeak _ -> ProbablyIssue -- ditto
    MatchingMultipleOfWeak _ -> ProbablyIssue -- ditto
    NoMatchingFormat _ -> Unsupported
    NoMatchingPattern _ -> Unsupported
    ArrayToTuple -> Comment
    TupleToArray -> Comment
    NoMatchingProperties -> ProbablyIssue -- TODO: #109
    TypeBecomesEmpty -> ProbablyIssue -- TODO: #70
    PartitionBecomesEmpty _ -> ProbablyIssue -- ditto
    _ -> CertainIssue
  relatedIssues =
    (==) `withClass` \case
      EnumDoesntSatisfy v -> Just v
      NoMatchingEnum v -> Just v
      _ -> Nothing
      `withClass` \case
        NoMatchingMaximum _ -> Just ()
        MatchingMaximumWeak _ -> Just ()
        _ -> Nothing
      `withClass` \case
        NoMatchingMinimum _ -> Just ()
        MatchingMinimumWeak _ -> Just ()
        _ -> Nothing
      `withClass` \case
        NoMatchingMultipleOf _ -> Just ()
        MatchingMultipleOfWeak _ -> Just ()
        _ -> Nothing
      `withClass` \case
        NoMatchingMaxLength _ -> Just ()
        MatchingMaxLengthWeak _ -> Just ()
        _ -> Nothing
      `withClass` \case
        NoMatchingMinLength _ -> Just ()
        MatchingMinLengthWeak _ -> Just ()
        _ -> Nothing
      `withClass` \case
        NoMatchingItems -> Just ()
        ArrayToTuple -> Just ()
        TupleToArray -> Just ()
        NoMatchingTupleItems -> Just ()
        TupleItemsLengthChanged _ -> Just ()
        _ -> Nothing
      `withClass` \case
        NoMatchingMaxItems _ -> Just ()
        MatchingMaxItemsWeak _ -> Just ()
        _ -> Nothing
      `withClass` \case
        NoMatchingMinItems _ -> Just ()
        MatchingMinItemsWeak _ -> Just ()
        _ -> Nothing
      `withClass` \case
        NoMatchingMaxProperties _ -> Just ()
        MatchingMaxPropertiesWeak _ -> Just ()
        _ -> Nothing
      `withClass` \case
        NoMatchingMinProperties _ -> Just ()
        MatchingMinPropertiesWeak _ -> Just ()
        _ -> Nothing
  describeIssue Forward (EnumDoesntSatisfy v) = para "The following enum value was removed:" <> showJSONValue v
  describeIssue Backward (EnumDoesntSatisfy v) = para "The following enum value was added:" <> showJSONValue v
  describeIssue Forward (NoMatchingEnum v) = para "The following enum value has been added:" <> showJSONValue v
  describeIssue Backward (NoMatchingEnum v) = para "The following enum value has been removed:" <> showJSONValue v
  describeIssue Forward (NoMatchingMaximum b) = para $ "Upper bound has been added:" <> showBound b <> "."
  describeIssue Backward (NoMatchingMaximum b) = para $ "Upper bound has been removed:" <> showBound b <> "."
  describeIssue ori (MatchingMaximumWeak (orientProdCons ori -> ProdCons p c)) =
    para $ "Upper bound changed from " <> showBound p <> " to " <> showBound c <> "."
  describeIssue Forward (NoMatchingMinimum b) = para $ "Lower bound has been added: " <> showBound b <> "."
  describeIssue Backward (NoMatchingMinimum b) = para $ "Lower bound has been removed: " <> showBound b <> "."
  describeIssue ori (MatchingMinimumWeak (orientProdCons ori -> ProdCons p c)) =
    para $ "Lower bound changed from " <> showBound p <> " to " <> showBound c <> "."
  describeIssue Forward (NoMatchingMultipleOf n) = para $ "Value is now a multiple of " <> show' n <> "."
  describeIssue Backward (NoMatchingMultipleOf n) = para $ "Value is no longer a multiple of " <> show' n <> "."
  describeIssue ori (MatchingMultipleOfWeak (orientProdCons ori -> ProdCons p c)) =
    para $ "Value changed from being a multiple of " <> show' p <> " to being a multiple of " <> show' c <> "."
  describeIssue Forward (NoMatchingFormat f) = para $ "Format added: " <> code f <> "."
  describeIssue Backward (NoMatchingFormat f) = para $ "Format removed: " <> code f <> "."
  describeIssue Forward (NoMatchingMaxLength n) = para $ "Maximum length added: " <> show' n <> "."
  describeIssue Backward (NoMatchingMaxLength n) = para $ "Maximum length removed: " <> show' n <> "."
  describeIssue ori (MatchingMaxLengthWeak (orientProdCons ori -> ProdCons p c)) =
    para $ "Maximum length of the string changed from " <> show' p <> " to " <> show' c <> "."
  describeIssue Forward (NoMatchingMinLength n) = para $ "Minimum length of the string added: " <> show' n <> "."
  describeIssue Backward (NoMatchingMinLength n) = para $ "Minimum length of the string removed: " <> show' n <> "."
  describeIssue ori (MatchingMinLengthWeak (orientProdCons ori -> ProdCons p c)) =
    para $ "Minimum length of the string changed from " <> show' p <> " to " <> show' c <> "."
  describeIssue Forward (NoMatchingPattern p) = para "Pattern (regular expression) added: " <> codeBlock p
  describeIssue Backward (NoMatchingPattern p) = para "Pattern (regular expression) removed: " <> codeBlock p
  describeIssue Forward NoMatchingItems = para "Array item schema has been added."
  describeIssue Backward NoMatchingItems = para "Array item schema has been removed."
  describeIssue ori (TupleItemsLengthChanged (orientProdCons ori -> ProdCons p c)) =
    para $ "Tuple length changed from " <> show' p <> " to " <> show' c <> "."
  describeIssue Forward ArrayToTuple = para "The array is now explicitly defined as a tuple."
  describeIssue Backward ArrayToTuple = para "The array is no longer explicitly defined as a tuple."
  describeIssue Forward TupleToArray = para "The array is no longer explicitly defined as a tuple."
  describeIssue Backward TupleToArray = para "The array is now explicitly defined as a tuple."
  describeIssue Forward NoMatchingTupleItems = para "The array is now explicitly defined as a tuple."
  describeIssue Backward NoMatchingTupleItems = para "The array is no longer explicitly defined as a tuple."
  describeIssue Forward (NoMatchingMaxItems n) = para $ "Maximum length of the array has been added " <> show' n <> "."
  describeIssue Backward (NoMatchingMaxItems n) = para $ "Maximum length of the array has been removed " <> show' n <> "."
  describeIssue ori (MatchingMaxItemsWeak (orientProdCons ori -> ProdCons p c)) =
    para $ "Maximum length of the array changed from " <> show' p <> " to " <> show' c <> "."
  describeIssue Forward (NoMatchingMinItems n) = para $ "Minimum length of the array added: " <> show' n <> "."
  describeIssue Backward (NoMatchingMinItems n) = para $ "Minimum length of the array removed: " <> show' n <> "."
  describeIssue ori (MatchingMinItemsWeak (orientProdCons ori -> ProdCons p c)) =
    para $ "Minimum length of the array changed from " <> show' p <> " to " <> show' c <> "."
  describeIssue Forward NoMatchingUniqueItems = para "Items are now required to be unique."
  describeIssue Backward NoMatchingUniqueItems = para "Items are no longer required to be unique."
  describeIssue Forward NoMatchingProperties = para "Property added."
  describeIssue Backward NoMatchingProperties = para "Property removed."
  describeIssue Forward NoAdditionalProperties = para "Additional properties have been removed."
  describeIssue Backward NoAdditionalProperties = para "Additional properties have been added."
  describeIssue Forward (NoMatchingMaxProperties n) = para $ "Maximum number of properties has been added: " <> show' n <> "."
  describeIssue Backward (NoMatchingMaxProperties n) = para $ "Maximum number of properties has been removed: " <> show' n <> "."
  describeIssue ori (MatchingMaxPropertiesWeak (orientProdCons ori -> ProdCons p c)) =
    para $ "Maximum number of properties has changed from " <> show' p <> " to " <> show' c <> "."
  describeIssue Forward (NoMatchingMinProperties n) = para $ "Minimum number of properties added: " <> show' n <> "."
  describeIssue Backward (NoMatchingMinProperties n) = para $ "Minimum number of properties removed: " <> show' n <> "."
  describeIssue ori (MatchingMinPropertiesWeak (orientProdCons ori -> ProdCons p c)) =
    para $ "Minimum number of properties has changed from " <> show' p <> " to " <> show' c <> "."
  describeIssue _ (NoMatchingCondition mPart conds) =
    para
      ( case mPart of
          Nothing -> "Could not verify that the following conditions hold (please file a bug if you see this):"
          Just locPart ->
            "In cases where " <> showPartition locPart
              <> " – could not verify that the following conditions hold (please file a bug if you see this):"
      )
      <> bulletList ((\(SomeCondition c) -> showCondition c) <$> conds)
  describeIssue Forward TypeBecomesEmpty = para "The type has been removed."
  describeIssue Backward TypeBecomesEmpty = para "The type has been added."
  describeIssue Forward (PartitionBecomesEmpty part) = para $ "The case where " <> showPartition part <> " – has been removed."
  describeIssue Backward (PartitionBecomesEmpty part) = para $ "The case where " <> showPartition part <> " – has been added."

show' :: Show x => x -> Inlines
show' = str . T.pack . show

instance Issuable 'SchemaLevel where
  data Issue 'SchemaLevel
    = -- | Some (openapi-supported) feature that we do not support was encountered in the schema
      NotSupported Text
    | -- | We couldn't prove that the branches of a oneOf are disjoint, and we will treat it as an anyOf, meaning we don't check whether the overlaps are excluded in a compatible way
      OneOfNotDisjoint
    | -- | The schema is actually invalid
      InvalidSchema Text
    | -- | The schema contains a reference loop along "anyOf"/"allOf"/"oneOf".
      UnguardedRecursion
    | -- | Producer doesn't place any restrictions on the types, but the consumer does. List what types remain available in the consumer.
      TypesRestricted [JsonType]
    | -- | in the producer this field used to be handled as part of "additionalProperties", and the consumer this is a specific "properties" entry. Only thrown when this change actually causes other issues
      AdditionalToProperty
    | -- | in the consumer this field used to be handled as part of "additionalProperties", and the producer this is a specific "properties" entry. Only thrown when this change actually causes other issues
      PropertyToAdditional
    | -- | consumer requires a property that is not required/allowed in the producer
      PropertyNowRequired
    | -- | producer allows a property that is not allowed in the consumer
      UnexpectedProperty
    deriving stock (Eq, Ord, Show)
  issueKind = \case
    NotSupported _ -> Unsupported
    OneOfNotDisjoint -> Unsupported
    InvalidSchema _ -> SchemaInvalid
    UnguardedRecursion -> Unsupported
    AdditionalToProperty -> Comment
    PropertyToAdditional -> Comment
    TypesRestricted _ -> ProbablyIssue -- TODO: #70
    _ -> CertainIssue
  relatedIssues =
    (==) `withClass` \case
      AdditionalToProperty -> Just ()
      PropertyToAdditional -> Just ()
      _ -> Nothing
      `withClass` \case
        PropertyNowRequired -> Just ()
        UnexpectedProperty -> Just ()
        _ -> Nothing
  describeIssue _ (NotSupported i) =
    para (emph "Encountered a feature that CompaREST does not support: " <> text i <> ".")
  describeIssue _ OneOfNotDisjoint =
    para $
      "Could not deduce that " <> code "oneOf"
        <> " cases don't overlap. Treating the "
        <> code "oneOf"
        <> " as an "
        <> code "anyOf"
        <> ". Reported errors might not be accurate."
  describeIssue _ (InvalidSchema i) =
    para (emph "The schema is invalid: " <> text i <> ".")
  describeIssue _ UnguardedRecursion =
    para "Encountered recursion that is too complex for CompaREST to untangle."
  describeIssue Forward (TypesRestricted tys) = case tys of
    [] -> para "No longer has any valid values." -- weird
    _ -> para "Values are now limited to the following types: " <> bulletList (para . describeJSONType <$> tys)
  describeIssue Backward (TypesRestricted tys) = case tys of
    [] -> para "Any value of any type is now allowed." -- weird
    _ -> para "Values are no longer limited to the following types: " <> bulletList (para . describeJSONType <$> tys)
  describeIssue Forward AdditionalToProperty = para "The property was previously implicitly described by the catch-all \"additional properties\" case. It is now explicitly defined."
  describeIssue Backward AdditionalToProperty = para "The property was previously explicitly defined. It is now implicitly described by the catch-all \"additional properties\" case."
  describeIssue Forward PropertyToAdditional = para "The property was previously explicitly defined. It is now implicitly described by the catch-all \"additional properties\" case."
  describeIssue Backward PropertyToAdditional = para "The property was previously implicitly described by the catch-all \"additional properties\" case. It is now explicitly defined."
  describeIssue Forward PropertyNowRequired = para "The property has become required."
  describeIssue Backward PropertyNowRequired = para "The property may not be present."
  describeIssue Forward UnexpectedProperty = para "The property has been removed."
  describeIssue Backward UnexpectedProperty = para "The property has been added."

instance Behavable 'SchemaLevel 'TypedSchemaLevel where
  data Behave 'SchemaLevel 'TypedSchemaLevel
    = OfType JsonType
    deriving stock (Eq, Ord, Show)

  describeBehavior (OfType t) = describeJSONType t

instance Behavable 'TypedSchemaLevel 'TypedSchemaLevel where
  data Behave 'TypedSchemaLevel 'TypedSchemaLevel
    = InPartition Partition
    deriving stock (Eq, Ord, Show)

  describeBehavior (InPartition partition) = "In cases where " <> showPartition partition

instance Behavable 'TypedSchemaLevel 'SchemaLevel where
  data Behave 'TypedSchemaLevel 'SchemaLevel
    = InItems
    | InItem Integer
    | InProperty Text
    | InAdditionalProperty
    deriving stock (Eq, Ord, Show)

  describeBehavior InItems = "Items"
  describeBehavior (InItem i) = "Item " <> show' i
  describeBehavior (InProperty p) = "Property " <> code p
  describeBehavior InAdditionalProperty = "Additional properties"