packages feed

asyncapi3-1.0.0.0: src/Data/AsyncAPI/Validation.hs

-- | Structural validation of AsyncAPI 3.1.0 documents.
module Data.AsyncAPI.Validation
  ( ValidationError (..),
    ValidationRule (..),
    validateAsyncAPI,
    supportedSchemaFormats,
    validVersion,
    internalRefs,
    resolvesRef,
    multiFormatSchemas,
  )
where

import Data.Aeson (Value (..), toJSON)
import Data.Aeson.Key qualified as Key
import Data.Aeson.KeyMap qualified as KM
import Data.AsyncAPI.Internal
import Data.Text (Text)
import Data.Text qualified as T
import Data.Vector qualified as V

-- | The rule a 'ValidationError' came from.
data ValidationRule
  = -- | The @asyncapi@ version string.
    VersionRule
  | -- | Internal @$ref@ resolution.
    RefResolutionRule
  | -- | Multi Format Schema @schemaFormat@ support (MUST table only).
    SchemaFormatRule
  deriving (Eq, Show)

-- | A single structural validation failure. The pointer is an RFC 6901 JSON
-- pointer into the document (@\"\"@ = root).
data ValidationError = ValidationError
  { -- | Rule that produced the failure.
    validationRule :: ValidationRule,
    -- | Pointer to the offending location.
    validationPointer :: Text,
    -- | Human-readable description of the failure.
    validationMessage :: Text
  }
  deriving (Eq, Show)

-- | Schema formats every implementation MUST support
-- ([Schema formats table](https://www.asyncapi.com/docs/reference/specification/v3.1.0#multiFormatSchemaFormatTable)).
supportedSchemaFormats :: [Text]
supportedSchemaFormats =
  [ "application/vnd.aai.asyncapi;version=3.1.0",
    "application/vnd.aai.asyncapi+json;version=3.1.0",
    "application/vnd.aai.asyncapi+yaml;version=3.1.0",
    "application/schema+json;version=draft-07",
    "application/schema+yaml;version=draft-07"
  ]

-- | Structural rules: version string, internal reference resolution, and
-- Multi Format Schema 'supportedSchemaFormats'. Result order is unspecified.
validateAsyncAPI :: AsyncAPI -> [ValidationError]
validateAsyncAPI doc = versionErrors <> refErrors <> schemaFormatErrors
  where
    json = toJSON doc
    versionErrors =
      [ ValidationError VersionRule "" ("unsupported asyncapi version: " <> v)
      | let v = getVersion (_asyncAPIAsyncAPI doc),
        not (validVersion v)
      ]
    refErrors =
      [ ValidationError RefResolutionRule p ("unresolved internal reference: " <> r)
      | (p, r) <- internalRefs json,
        not (resolvesRef r json)
      ]
    schemaFormatErrors =
      [ ValidationError SchemaFormatRule p ("unsupported schemaFormat: " <> f)
      | (p, f) <- multiFormatSchemas json,
        f `notElem` supportedSchemaFormats
      ]

-- | The @asyncapi@ version string must be @3.<digits>.<digits>(-suffix)?@.
validVersion :: Text -> Bool
validVersion v = case T.splitOn "." v of
  ["3", mi, pa] ->
    digits mi
      && let (pre, post) = T.breakOn "-" pa
          in digits pre && (T.null post || (T.length post > 1))
  _ -> False
  where
    digits t = not (T.null t) && T.all (\c -> c >= '0' && c <= '9') t

-- | All internal @$ref@ values (@#/@-prefixed) with their RFC 6901 pointers.
internalRefs :: Value -> [(Text, Text)]
internalRefs = go ""
  where
    go p (Object o) =
      concatMap
        ( \(k, v) ->
            case (Key.toText k, v) of
              ("$ref", String r)
                | "#/" `T.isPrefixOf` r -> [(p, r)]
              _ -> go (child p k) v
        )
        (KM.toList o)
    go p (Array a) =
      concat
        [go (p <> "/" <> T.pack (show i)) v | (i, v) <- zip [0 :: Int ..] (V.toList a)]
    go _ _ = []
    child p k = p <> "/" <> escapeToken (Key.toText k)

-- | Whether an internal reference (@#\/a\/b@) resolves in the document.
resolvesRef :: Text -> Value -> Bool
resolvesRef r doc = case T.stripPrefix "#/" r of
  Nothing -> False
  Just rest -> go (T.splitOn "/" rest) doc
  where
    go [] _ = True
    go (t : ts) v = case v of
      Object o -> case KM.lookup (Key.fromText (unescapeToken t)) o of
        Just v' -> go ts v'
        Nothing -> False
      _ -> False

-- | All @schemaFormat@ values of embedded Multi Format Schema Objects
-- (objects carrying both @schemaFormat@ and @schema@ keys), with pointers.
multiFormatSchemas :: Value -> [(Text, Text)]
multiFormatSchemas = go ""
  where
    go p (Object o)
      | Just (String f) <- KM.lookup "schemaFormat" o,
        KM.member "schema" o =
          (p, f) : concatMap (go p . snd) (KM.toList o)
      | otherwise = concatMap (go p . snd) (KM.toList o)
    go p (Array a) = concatMap (go p) (V.toList a)
    go _ _ = []