packages feed

hydra-ext-0.17.0: src/main/haskell/Hydra/Json/Schema/Coder.hs

-- Note: this is an automatically generated file. Do not edit.
-- | JSON Schema code generator: converts Hydra modules to JSON Schema documents

module Hydra.Json.Schema.Coder where
import qualified Hydra.Annotations as Annotations
import qualified Hydra.Ast as Ast
import qualified Hydra.Coders as Coders
import qualified Hydra.Constants as Constants
import qualified Hydra.Core as Core
import qualified Hydra.File as File
import qualified Hydra.Dependencies as Dependencies
import qualified Hydra.Environment as Environment
import qualified Hydra.Error.Checking as Checking
import qualified Hydra.Error.Core as ErrorCore
import qualified Hydra.Error.Packaging as ErrorPackaging
import qualified Hydra.Errors as Errors
import qualified Hydra.Formatting as Formatting
import qualified Hydra.Graph as Graph
import qualified Hydra.Json.Model as Model
import qualified Hydra.Json.Schema as Schema
import qualified Hydra.Json.Schema.Serde as Serde
import qualified Hydra.Overlay.Haskell.Lib.Eithers as Eithers
import qualified Hydra.Overlay.Haskell.Lib.Equality as Equality
import qualified Hydra.Overlay.Haskell.Lib.Lists as Lists
import qualified Hydra.Overlay.Haskell.Lib.Logic as Logic
import qualified Hydra.Overlay.Haskell.Lib.Maps as Maps
import qualified Hydra.Overlay.Haskell.Lib.Optionals as Optionals
import qualified Hydra.Overlay.Haskell.Lib.Pairs as Pairs
import qualified Hydra.Overlay.Haskell.Lib.Sets as Sets
import qualified Hydra.Overlay.Haskell.Lib.Strings as Strings
import qualified Hydra.Names as Names
import qualified Hydra.Packaging as Packaging
import qualified Hydra.Parsing as Parsing
import qualified Hydra.Paths as Paths
import qualified Hydra.Predicates as Predicates
import qualified Hydra.Query as Query
import qualified Hydra.Reflect as Reflect
import qualified Hydra.Relational as Relational
import qualified Hydra.Show.Variants as ShowVariants
import qualified Hydra.Strip as Strip
import qualified Hydra.Tabular as Tabular
import qualified Hydra.Testing as Testing
import qualified Hydra.Topology as Topology
import qualified Hydra.Typed as Typed
import qualified Hydra.Typing as Typing
import qualified Hydra.Util as Util
import qualified Hydra.Validation as Validation
import qualified Hydra.Variables as Variables
import qualified Hydra.Variants as Variants
import Prelude hiding  (Enum, Ordering, decodeFloat, encodeFloat, fail, map, pure, sum)
import qualified Data.Scientific as Sci
import qualified Data.Map as M
import qualified Data.Set as S
-- | Construct JSON Schema documents from type definitions
constructModule :: t0 -> Graph.Graph -> t1 -> [Packaging.TypeDefinition] -> Either Errors.Error (M.Map String Schema.Document)
constructModule cx g mod typeDefs =

      let typeBody = \td -> Core.typeSchemeBody (Packaging.typeDefinitionBody td)
          typeMap = Maps.fromList (Lists.map (\td -> (Packaging.typeDefinitionName td, (typeBody td))) typeDefs)
      in (Eithers.map (\ps -> Maps.fromList ps) (Eithers.mapList (\td -> typeDefToDocument cx g typeMap (Packaging.typeDefinitionName td) (typeBody td)) typeDefs))
-- | Build a single-property record Schema for one branch of an Either oneOf
eitherBranch :: String -> [Schema.Restriction] -> Schema.Schema
eitherBranch label res =
    Schema.Schema [
      Schema.RestrictionType (Schema.TypeSingle Schema.TypeNameObject),
      (Schema.RestrictionObject (Schema.ObjectRestrictionProperties (Maps.singleton (Schema.Keyword label) (Schema.Schema res)))),
      (Schema.RestrictionObject (Schema.ObjectRestrictionRequired [
        Schema.Keyword label])),
      (Schema.RestrictionObject (Schema.ObjectRestrictionAdditionalProperties (Schema.AdditionalItemsAny False)))]
-- | Encode a field type as a JSON Schema keyword-schema pair
encodeField :: t0 -> Graph.Graph -> Core.FieldType -> Either Errors.Error (Schema.Keyword, Schema.Schema)
encodeField cx g ft =

      let name = Core.fieldTypeName ft
          typ = Core.fieldTypeType ft
      in (Eithers.map (\res -> (Schema.Keyword (Core.unName name), (Schema.Schema res))) (typeToExpr cx g False typ))
-- | Encode a Hydra name as a safe identifier string, replacing non-alphanumeric characters with underscores
encodeName :: Core.Name -> String
encodeName name = Formatting.nonAlnumToUnderscores (Core.unName name)
-- | Encode a named type as a list of JSON Schema restrictions with a title
encodeNamedType :: t0 -> Graph.Graph -> Core.Name -> Core.Type -> Either Errors.Error [Schema.Restriction]
encodeNamedType cx g name typ =
    Eithers.map (\res -> Lists.concat [
      [
        Schema.RestrictionTitle (Core.unName name)],
      res]) (typeToExpr cx g False (Strip.deannotateType typ))
-- | Encode a record or union as a list of JSON Schema object restrictions; isUnion adds min/maxProperties=1
encodeRecordOrUnion :: t0 -> Graph.Graph -> Bool -> Bool -> [Core.FieldType] -> Either Errors.Error [Schema.Restriction]
encodeRecordOrUnion cx g optional isUnion fields =
    Eithers.bind (Eithers.mapList (\ft -> encodeField cx g ft) fields) (\props ->
      let objRes = [
            Schema.RestrictionObject (Schema.ObjectRestrictionProperties (Maps.fromList props))]
          reqs =
                  Lists.foldl (\acc -> \f -> Logic.ifElse (isRequiredField f) (Lists.concat2 acc [
                    Schema.Keyword (Core.unName (Core.fieldTypeName f))]) acc) [] fields
          reqRes = Logic.ifElse (Lists.null reqs) [] [
                Schema.RestrictionObject (Schema.ObjectRestrictionRequired reqs)]
          addPropsRes = [
                Schema.RestrictionObject (Schema.ObjectRestrictionAdditionalProperties (Schema.AdditionalItemsAny False))]
          cardRes =
                  Logic.ifElse isUnion [
                    Schema.RestrictionObject (Schema.ObjectRestrictionMinProperties 1),
                    (Schema.RestrictionObject (Schema.ObjectRestrictionMaxProperties 1))] []
      in (Right (Lists.concat [
        jsType optional Schema.TypeNameObject,
        objRes,
        reqRes,
        addPropsRes,
        cardRes])))
-- | Encode a union type, splitting unit-typed (simple) variants into a string-enum branch
encodeUnion :: t0 -> Graph.Graph -> Bool -> [Core.FieldType] -> Either Errors.Error [Schema.Restriction]
encodeUnion cx g optional fields =

      let isSimple = \f -> Predicates.isUnitType (Strip.deannotateType (Core.fieldTypeType f))
          simple = Lists.filter isSimple fields
          nonsimple = Lists.filter (\f -> Logic.not (isSimple f)) fields
      in (Logic.ifElse (Lists.null simple) (encodeRecordOrUnion cx g optional True fields) (Eithers.bind (encodeRecordOrUnion cx g False True nonsimple) (\recRes ->
        let names = Lists.map (\f -> Core.unName (Core.fieldTypeName f)) simple
            simpleSchema =
                    Schema.Schema [
                      Schema.RestrictionType (Schema.TypeSingle Schema.TypeNameString),
                      (Schema.RestrictionMultiple (Schema.MultipleRestrictionEnum (Lists.map (\n -> Model.ValueString n) names)))]
        in (Right [
          Schema.RestrictionMultiple (Schema.MultipleRestrictionOneOf [
            Schema.Schema recRes,
            simpleSchema])]))))
-- | Determine whether a field is required (i.e., not optional/Maybe)
isRequiredField :: Core.FieldType -> Bool
isRequiredField ft =

      let typ = Core.fieldTypeType ft
      in case (Strip.deannotateType typ) of
        Core.TypeOptional _ -> False
        _ -> True
-- | Build the JSON Schema type-restriction list for a type name, optionally widening to allow null
jsType :: Bool -> Schema.TypeName -> [Schema.Restriction]
jsType optional tname =
    [
      Schema.RestrictionType (Logic.ifElse optional (Schema.TypeMultiple [
        tname,
        Schema.TypeNameNull]) (Schema.TypeSingle tname))]
-- | Map a Hydra literal type to a JSON Schema type name
literalTypeName :: Core.LiteralType -> Schema.TypeName
literalTypeName lt =
    case lt of
      Core.LiteralTypeBinary -> Schema.TypeNameString
      Core.LiteralTypeBoolean -> Schema.TypeNameBoolean
      Core.LiteralTypeFloat _ -> Schema.TypeNameNumber
      Core.LiteralTypeInteger _ -> Schema.TypeNameInteger
      Core.LiteralTypeString -> Schema.TypeNameString
      _ -> Schema.TypeNameString
-- | Convert a Hydra module to a map from file path to JSON Schema document string
moduleToJsonSchema :: t0 -> [Packaging.Definition] -> t1 -> Graph.Graph -> Either Errors.Error (M.Map String String)
moduleToJsonSchema mod defs cx g =

      let partitioned = Environment.partitionDefinitions defs
          typeDefs = Pairs.first partitioned
      in (Eithers.map (\docs -> Maps.map (\doc -> Serde.jsonSchemaDocumentToString doc) docs) (constructModule cx g mod typeDefs))
-- | Compute the JSON Schema output file path for a named type
nameToPath :: Core.Name -> String
nameToPath name =

      let qn = Names.qualifyName name
          mns = Util.qualifiedNameModuleName qn
          local = Util.qualifiedNameLocal qn
          nsPart = Optionals.cases mns "" (\ns -> Strings.cat2 (Packaging.unModuleName ns) ".")
      in (Names.moduleNameToFilePath Util.CaseConventionCamel (File.FileExtension "json") (Packaging.ModuleName (Strings.cat2 nsPart local)))
-- | Build the JSON Schema restriction list for a pair type
pairRestrictions :: Bool -> [Schema.Restriction] -> [Schema.Restriction] -> [Schema.Restriction]
pairRestrictions optional firstRes secondRes =
    Lists.concat [
      jsType optional Schema.TypeNameObject,
      [
        Schema.RestrictionObject (Schema.ObjectRestrictionProperties (Maps.fromList [
          (Schema.Keyword "first", (Schema.Schema firstRes)),
          (Schema.Keyword "second", (Schema.Schema secondRes))]))],
      [
        Schema.RestrictionObject (Schema.ObjectRestrictionRequired [
          Schema.Keyword "first",
          (Schema.Keyword "second")])],
      [
        Schema.RestrictionObject (Schema.ObjectRestrictionAdditionalProperties (Schema.AdditionalItemsAny False))]]
-- | Create a JSON Schema reference restriction for a named type
referenceRestriction :: Core.Name -> Schema.Restriction
referenceRestriction name =
    Schema.RestrictionReference (Schema.SchemaReference (Strings.cat [
      "#/$defs/",
      (encodeName name)]))
-- | Walk the transitive named-type dependency closure of a root type through typeMap; the visited set guards against cycles in self-/mutually-recursive types
transitiveTypeDeps :: M.Map Core.Name Core.Type -> S.Set Core.Name -> Core.Type -> S.Set Core.Name
transitiveTypeDeps typeMap visited rootType =

      let directDeps = Dependencies.typeDependencyNames True rootType
          step =
                  \acc -> \n -> Logic.ifElse (Sets.member n acc) acc (
                    let acc1 = Sets.insert n acc
                    in (Optionals.cases (Maps.lookup n typeMap) acc1 (\t -> transitiveTypeDeps typeMap acc1 t)))
      in (Lists.foldl step visited (Sets.toList directDeps))
-- | Build a JSON Schema document for a single named type, with $defs covering its transitive dependencies and short-name substitution applied
typeDefToDocument :: t0 -> Graph.Graph -> M.Map Core.Name Core.Type -> Core.Name -> Core.Type -> Either Errors.Error (String, Schema.Document)
typeDefToDocument cx g typeMap rootName rootType =

      let depNames = Sets.toList (transitiveTypeDeps typeMap Sets.empty rootType)
          allNames = Lists.concat2 [
                rootName] (Lists.filter (\n -> Logic.not (Equality.equal n rootName)) depNames)
          allTypes = Lists.map (\n -> Optionals.fromOptional (Core.TypeVariable n) (Maps.lookup n typeMap)) allNames
          nameSubst = Dependencies.toShortNames allNames
          types = Lists.map (\t -> Variables.substituteTypeVariables nameSubst t) allTypes
          names = Lists.map (\n -> Optionals.fromOptional n (Maps.lookup n nameSubst)) allNames
          subRoot = Optionals.fromOptional rootName (Maps.lookup rootName nameSubst)
          pairs = Lists.zip names types
      in (Eithers.bind (Eithers.mapList (\p -> typeToKeywordSchemaPair cx g (Pairs.first p) (Pairs.second p)) pairs) (\schemas -> Right (
        nameToPath rootName,
        Schema.Document {
          Schema.documentId = Nothing,
          Schema.documentDefinitions = (Just (Maps.fromList schemas)),
          Schema.documentRoot = (Schema.Schema [
            referenceRestriction subRoot])})))
-- | Encode a Hydra type as a list of JSON Schema restrictions
typeToExpr :: t0 -> Graph.Graph -> Bool -> Core.Type -> Either Errors.Error [Schema.Restriction]
typeToExpr cx g optional typ =
    case typ of
      Core.TypeAnnotated _ -> Eithers.bind (typeToExpr cx g optional (Strip.deannotateType typ)) (\res -> Eithers.bind (Annotations.getTypeDescription cx g typ) (\mdesc -> Right (Lists.concat2 (Optionals.cases mdesc [] (\d -> [
        Schema.RestrictionDescription d])) res)))
      Core.TypeApplication v0 -> typeToExpr cx g optional (Core.applicationTypeFunction v0)
      Core.TypeEither v0 ->
        let lt = Core.eitherTypeLeft v0
            rt = Core.eitherTypeRight v0
        in (Eithers.bind (typeToExpr cx g False lt) (\leftRes -> Eithers.bind (typeToExpr cx g False rt) (\rightRes -> Right [
          Schema.RestrictionMultiple (Schema.MultipleRestrictionOneOf [
            eitherBranch "left" leftRes,
            (eitherBranch "right" rightRes)])])))
      Core.TypeForall v0 -> typeToExpr cx g optional (Core.forallTypeBody v0)
      Core.TypeList v0 -> Eithers.bind (typeToExpr cx g False v0) (\els -> Right (Lists.concat2 (jsType optional Schema.TypeNameArray) [
        Schema.RestrictionArray (Schema.ArrayRestrictionItems (Schema.ItemsSameItems (Schema.Schema els)))]))
      Core.TypeLiteral v0 -> Right (jsType optional (literalTypeName v0))
      Core.TypeMap v0 ->
        let vt = Core.mapTypeValues v0
        in (Eithers.bind (typeToExpr cx g False vt) (\vRes -> Right (Lists.concat2 (jsType optional Schema.TypeNameObject) [
          Schema.RestrictionObject (Schema.ObjectRestrictionAdditionalProperties (Schema.AdditionalItemsSchema (Schema.Schema vRes)))])))
      Core.TypeOptional v0 -> typeToExpr cx g True v0
      Core.TypePair v0 ->
        let ft = Core.pairTypeFirst v0
            st = Core.pairTypeSecond v0
        in (Eithers.bind (typeToExpr cx g False ft) (\firstRes -> Eithers.bind (typeToExpr cx g False st) (\secondRes -> Right (pairRestrictions optional firstRes secondRes))))
      Core.TypeRecord v0 -> encodeRecordOrUnion cx g optional False v0
      Core.TypeSet v0 -> Eithers.bind (typeToExpr cx g False v0) (\els -> Right (Lists.concat2 (jsType optional Schema.TypeNameArray) [
        Schema.RestrictionArray (Schema.ArrayRestrictionItems (Schema.ItemsSameItems (Schema.Schema els)))]))
      Core.TypeUnion v0 -> encodeUnion cx g optional v0
      Core.TypeVariable v0 -> Right [
        referenceRestriction v0]
      Core.TypeWrap v0 -> typeToExpr cx g optional v0
      _ -> Left (Errors.ErrorOther (Errors.OtherError (Strings.cat2 "JSON Schema: unsupported type variant: " (ShowVariants.typeVariant (Reflect.typeVariant typ)))))
-- | Build a (Keyword, Schema) pair for a named type, used as a $defs entry
typeToKeywordSchemaPair :: t0 -> Graph.Graph -> Core.Name -> Core.Type -> Either Errors.Error (Schema.Keyword, Schema.Schema)
typeToKeywordSchemaPair cx g name typ =
    Eithers.map (\res -> (Schema.Keyword (encodeName name), (Schema.Schema res))) (encodeNamedType cx g name typ)