packages feed

swarm-0.5.0.0: src/Swarm/Doc/Schema/Refined.hs

{-# LANGUAGE OverloadedStrings #-}

-- |
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Refined JSON schema after converting
-- all JSON Value types to their specific sum types
module Swarm.Doc.Schema.Refined where

import Control.Applicative ((<|>))
import Data.Aeson
import Data.List.Extra (replace)
import Data.Map (Map)
import Data.Map qualified as M
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Set (Set)
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Generics (Generic)
import Swarm.Doc.Schema.SchemaType
import System.FilePath (takeBaseName)
import Text.Pandoc
import Text.Pandoc.Builder

-- * Basic

schemaJsonOptions :: Options
schemaJsonOptions =
  defaultOptions
    { fieldLabelModifier = replace "S" "$" . drop 1 -- drops leading underscore
    }

-- | A single record that encompasses all possible objects
-- in a JSON schema. All fields are optional.
data SchemaRaw = SchemaRaw
  { _description :: Maybe Text
  , _default :: Maybe Value
  , _title :: Maybe Text
  , _type :: Maybe (SingleOrList Text)
  , _name :: Maybe Text
  , _properties :: Maybe (Map Text SwarmSchema)
  , _items :: Maybe (ItemDescription SwarmSchema)
  , _examples :: Maybe [Value]
  , _Sref :: Maybe Text
  , _oneOf :: Maybe [SchemaRaw]
  , _footers :: Maybe [FilePath]
  , _additionalProperties :: Maybe Bool
  }
  deriving (Eq, Ord, Show, Generic)

instance FromJSON SchemaRaw where
  parseJSON = genericParseJSON schemaJsonOptions

extractSchemaType :: SchemaRaw -> Maybe SchemaType
extractSchemaType rawSchema =
  mkReference <$> _Sref rawSchema
    <|> getTypeFromItems
    <|> Simple <$> _type rawSchema
    <|> Alternatives . mapMaybe extractSchemaType <$> _oneOf rawSchema
 where
  mkReference = Reference . SchemaIdReference . T.pack . takeBaseName . T.unpack

  getTypeFromItems :: Maybe SchemaType
  getTypeFromItems = do
    itemsThing <- _items rawSchema
    case itemsThing of
      ItemList _ -> Nothing
      ItemType x -> Just $ ListOf $ schemaType x

-- * Refined

data ItemDescription a
  = ItemList [a]
  | ItemType a
  deriving (Eq, Ord, Show)

instance (FromJSON a) => FromJSON (ItemDescription a) where
  parseJSON x =
    ItemList <$> parseJSON x
      <|> ItemType <$> parseJSON x

getSchemaReferences :: SchemaType -> [SchemaIdReference]
getSchemaReferences = \case
  Simple _ -> []
  Alternatives xs -> concatMap getSchemaReferences xs
  Reference x -> pure x
  ListOf x -> getSchemaReferences x

-- | A subset of all JSON schemas, conforming to internal Swarm conventions.
--
-- Conveniently, this extra representation layer
-- is able to enforce (via 'toSwarmSchema') that all "object"
-- definitions in the schema contain the @"additionalProperties": true@ attribute.
data SwarmSchema = SwarmSchema
  { schemaType :: SchemaType
  , defaultValue :: Maybe Value
  , objectDescription :: Maybe Pandoc
  , properties :: Maybe (Map Text SwarmSchema)
  , itemsDescription :: Maybe (ItemDescription SwarmSchema)
  , examples :: [Value]
  }
  deriving (Eq, Ord, Show)

instance FromJSON SwarmSchema where
  parseJSON x = do
    rawSchema :: rawSchema <- parseJSON x
    toSwarmSchema rawSchema

getMarkdown :: MonadFail m => Text -> m Pandoc
getMarkdown desc = case runPure (readMarkdown def desc) of
  Right d -> return d
  Left err -> fail $ T.unpack $ renderError err

toSwarmSchema :: MonadFail m => SchemaRaw -> m SwarmSchema
toSwarmSchema rawSchema = do
  theType <- maybe (fail "Unspecified sub-schema type") return maybeType
  markdownDescription <- mapM getMarkdown $ _description rawSchema

  if null (_properties rawSchema) || not (fromMaybe True (_additionalProperties rawSchema))
    then return ()
    else fail "All objects must specify '\"additionalProperties\": true'"

  return
    SwarmSchema
      { schemaType = theType
      , defaultValue = _default rawSchema
      , objectDescription = markdownDescription <|> doc . plain . text <$> _name rawSchema
      , examples = fromMaybe [] $ _examples rawSchema
      , properties = _properties rawSchema
      , itemsDescription = _items rawSchema
      }
 where
  maybeType = extractSchemaType rawSchema

-- * Utilities

-- | Recursively extract references to other schemas
extractReferences :: SwarmSchema -> Set SchemaIdReference
extractReferences s = thisRefList <> otherRefLists
 where
  thisRefList = Set.fromList . getSchemaReferences $ schemaType s

  otherSchemas = maybe [] M.elems $ properties s
  otherRefLists = Set.unions $ map extractReferences otherSchemas