packages feed

aeson-schemas 1.2.0 → 1.3.0

raw patch · 162 files changed

+3895/−1400 lines, 162 filesdep +aeson-qqdep +criteriondep +deepseqdep −bytestringdep ~QuickCheckdep ~raw-strings-qqdep ~tastyPVP ok

version bump matches the API change (PVP)

Dependencies added: aeson-qq, criterion, deepseq, hashable, hint, interpolate, th-orphans

Dependencies removed: bytestring

Dependency ranges changed: QuickCheck, raw-strings-qq, tasty, tasty-golden, tasty-hunit, tasty-quickcheck, th-test-utils

API changes (from Hackage documentation)

- Data.Aeson.Schema: data SchemaType
- Data.Aeson.Schema: type IsSchemaObject schema = (IsSchemaType schema, SchemaResult schema ~ Object schema)
- Data.Aeson.Schema: type family SchemaResult (schema :: SchemaType)
- Data.Aeson.Schema.Internal: NormalKey :: Symbol -> SchemaKey
- Data.Aeson.Schema.Internal: PhantomKey :: Symbol -> SchemaKey
- Data.Aeson.Schema.Internal: SchemaBool :: SchemaType
- Data.Aeson.Schema.Internal: SchemaCustom :: Type -> SchemaType
- Data.Aeson.Schema.Internal: SchemaDouble :: SchemaType
- Data.Aeson.Schema.Internal: SchemaInt :: SchemaType
- Data.Aeson.Schema.Internal: SchemaList :: SchemaType -> SchemaType
- Data.Aeson.Schema.Internal: SchemaMaybe :: SchemaType -> SchemaType
- Data.Aeson.Schema.Internal: SchemaObject :: [(SchemaKey, SchemaType)] -> SchemaType
- Data.Aeson.Schema.Internal: SchemaText :: SchemaType
- Data.Aeson.Schema.Internal: SchemaTry :: SchemaType -> SchemaType
- Data.Aeson.Schema.Internal: SchemaUnion :: [SchemaType] -> SchemaType
- Data.Aeson.Schema.Internal: class Typeable schema => IsSchemaType (schema :: SchemaType)
- Data.Aeson.Schema.Internal: class (Typeable schemaKey, KnownSymbol (FromSchemaKey schemaKey)) => KnownSchemaKey (schemaKey :: SchemaKey)
- Data.Aeson.Schema.Internal: data SchemaKey
- Data.Aeson.Schema.Internal: data SchemaType
- Data.Aeson.Schema.Internal: fromSchemaKey :: forall schemaKey. KnownSymbol (FromSchemaKey schemaKey) => Text
- Data.Aeson.Schema.Internal: getContext :: KnownSchemaKey schemaKey => HashMap Text Value -> Value
- Data.Aeson.Schema.Internal: instance (Data.Aeson.Schema.Internal.IsSchemaType inner, GHC.Show.Show (Data.Aeson.Schema.Internal.SchemaResult inner)) => Data.Aeson.Schema.Internal.IsSchemaType ('Data.Aeson.Schema.Internal.SchemaList inner)
- Data.Aeson.Schema.Internal: instance (Data.Aeson.Schema.Internal.IsSchemaType inner, GHC.Show.Show (Data.Aeson.Schema.Internal.SchemaResult inner)) => Data.Aeson.Schema.Internal.IsSchemaType ('Data.Aeson.Schema.Internal.SchemaMaybe inner)
- Data.Aeson.Schema.Internal: instance (Data.Aeson.Schema.Internal.IsSchemaType inner, GHC.Show.Show (Data.Aeson.Schema.Internal.SchemaResult inner)) => Data.Aeson.Schema.Internal.IsSchemaType ('Data.Aeson.Schema.Internal.SchemaTry inner)
- Data.Aeson.Schema.Internal: instance (Data.Aeson.Schema.Internal.KnownSchemaKey schemaKey, Data.Aeson.Schema.Internal.IsSchemaType inner, GHC.Show.Show (Data.Aeson.Schema.Internal.SchemaResult inner), Data.Typeable.Internal.Typeable (Data.Aeson.Schema.Internal.SchemaResult inner), Data.Aeson.Schema.Internal.IsSchemaObject ('Data.Aeson.Schema.Internal.SchemaObject rest), Data.Typeable.Internal.Typeable rest) => Data.Aeson.Schema.Internal.IsSchemaType ('Data.Aeson.Schema.Internal.SchemaObject ('(schemaKey, inner) : rest))
- Data.Aeson.Schema.Internal: instance (Data.Aeson.Schema.Utils.TypeFamilies.All Data.Aeson.Schema.Internal.IsSchemaType schemas, Data.Typeable.Internal.Typeable schemas, GHC.Show.Show (Data.Aeson.Schema.Internal.SchemaResult ('Data.Aeson.Schema.Internal.SchemaUnion schemas)), Data.Aeson.Types.FromJSON.FromJSON (Data.Aeson.Schema.Internal.SchemaResult ('Data.Aeson.Schema.Internal.SchemaUnion schemas))) => Data.Aeson.Schema.Internal.IsSchemaType ('Data.Aeson.Schema.Internal.SchemaUnion schemas)
- Data.Aeson.Schema.Internal: instance (GHC.Show.Show inner, Data.Typeable.Internal.Typeable inner, Data.Aeson.Types.FromJSON.FromJSON inner) => Data.Aeson.Schema.Internal.IsSchemaType ('Data.Aeson.Schema.Internal.SchemaCustom inner)
- Data.Aeson.Schema.Internal: instance Data.Aeson.Schema.Internal.IsSchemaObject schema => Data.Aeson.Types.FromJSON.FromJSON (Data.Aeson.Schema.Internal.Object schema)
- Data.Aeson.Schema.Internal: instance Data.Aeson.Schema.Internal.IsSchemaObject schema => GHC.Show.Show (Data.Aeson.Schema.Internal.Object schema)
- Data.Aeson.Schema.Internal: instance Data.Aeson.Schema.Internal.IsSchemaType 'Data.Aeson.Schema.Internal.SchemaBool
- Data.Aeson.Schema.Internal: instance Data.Aeson.Schema.Internal.IsSchemaType 'Data.Aeson.Schema.Internal.SchemaDouble
- Data.Aeson.Schema.Internal: instance Data.Aeson.Schema.Internal.IsSchemaType 'Data.Aeson.Schema.Internal.SchemaInt
- Data.Aeson.Schema.Internal: instance Data.Aeson.Schema.Internal.IsSchemaType 'Data.Aeson.Schema.Internal.SchemaText
- Data.Aeson.Schema.Internal: instance Data.Aeson.Schema.Internal.IsSchemaType ('Data.Aeson.Schema.Internal.SchemaObject '[])
- Data.Aeson.Schema.Internal: instance GHC.TypeLits.KnownSymbol key => Data.Aeson.Schema.Internal.KnownSchemaKey ('Data.Aeson.Schema.Internal.NormalKey key)
- Data.Aeson.Schema.Internal: instance GHC.TypeLits.KnownSymbol key => Data.Aeson.Schema.Internal.KnownSchemaKey ('Data.Aeson.Schema.Internal.PhantomKey key)
- Data.Aeson.Schema.Internal: toSchemaTypeShow :: forall (a :: SchemaType). Typeable a => SchemaType
- Data.Aeson.Schema.Internal: type IsSchemaObject schema = (IsSchemaType schema, SchemaResult schema ~ Object schema)
- Data.Aeson.Schema.Key: data SchemaKey
- Data.Aeson.Schema.Key: instance GHC.Show.Show Data.Aeson.Schema.Key.SchemaKey
- Data.Aeson.Schema.Show: NormalKey :: String -> SchemaKey
- Data.Aeson.Schema.Show: PhantomKey :: String -> SchemaKey
- Data.Aeson.Schema.Show: SchemaBool :: SchemaType
- Data.Aeson.Schema.Show: SchemaCustom :: String -> SchemaType
- Data.Aeson.Schema.Show: SchemaDouble :: SchemaType
- Data.Aeson.Schema.Show: SchemaInt :: SchemaType
- Data.Aeson.Schema.Show: SchemaText :: SchemaType
- Data.Aeson.Schema.Show: data SchemaKey
- Data.Aeson.Schema.Utils.TypeFamilies: type family All (f :: a -> Constraint) (xs :: [a]) :: Constraint
+ Data.Aeson.Schema: showSchema :: forall (schema :: Schema). IsSchema schema => String
+ Data.Aeson.Schema: toMap :: IsSchema ( 'Schema schema) => Object ( 'Schema schema) -> Object
+ Data.Aeson.Schema: type IsSchema (schema :: Schema) = (HasSchemaResult (ToSchemaObject schema), All HasSchemaResultPair (FromSchema schema), SchemaResult (ToSchemaObject schema) ~ Object schema)
+ Data.Aeson.Schema: type Schema = Schema' Symbol Type
+ Data.Aeson.Schema.Internal: class IsSchemaType schema => HasSchemaResult (schema :: SchemaType)
+ Data.Aeson.Schema.Internal: class HasSchemaResultPair (a :: (SchemaKey, SchemaType))
+ Data.Aeson.Schema.Internal: class ParseSumType xs
+ Data.Aeson.Schema.Internal: instance (Data.Aeson.Schema.Internal.HasSchemaResult inner, GHC.Show.Show (Data.Aeson.Schema.Internal.SchemaResult inner), Data.Aeson.Types.ToJSON.ToJSON (Data.Aeson.Schema.Internal.SchemaResult inner)) => Data.Aeson.Schema.Internal.HasSchemaResult ('Data.Aeson.Schema.Type.SchemaList inner)
+ Data.Aeson.Schema.Internal: instance (Data.Aeson.Schema.Internal.HasSchemaResult inner, GHC.Show.Show (Data.Aeson.Schema.Internal.SchemaResult inner), Data.Aeson.Types.ToJSON.ToJSON (Data.Aeson.Schema.Internal.SchemaResult inner)) => Data.Aeson.Schema.Internal.HasSchemaResult ('Data.Aeson.Schema.Type.SchemaMaybe inner)
+ Data.Aeson.Schema.Internal: instance (Data.Aeson.Schema.Internal.HasSchemaResult inner, GHC.Show.Show (Data.Aeson.Schema.Internal.SchemaResult inner), Data.Aeson.Types.ToJSON.ToJSON (Data.Aeson.Schema.Internal.SchemaResult inner)) => Data.Aeson.Schema.Internal.HasSchemaResult ('Data.Aeson.Schema.Type.SchemaTry inner)
+ Data.Aeson.Schema.Internal: instance (Data.Aeson.Schema.Internal.HasSchemaResult schema, Data.Aeson.Schema.Internal.ParseSumType schemas) => Data.Aeson.Schema.Internal.ParseSumType (schema : schemas)
+ Data.Aeson.Schema.Internal: instance (Data.Aeson.Schema.Key.IsSchemaKey key, Data.Aeson.Schema.Internal.HasSchemaResult inner, Data.Typeable.Internal.Typeable (Data.Aeson.Schema.Internal.SchemaResult inner)) => Data.Aeson.Schema.Internal.HasSchemaResultPair '(key, inner)
+ Data.Aeson.Schema.Internal: instance (Data.Aeson.Schema.Utils.All.All Data.Aeson.Schema.Internal.HasSchemaResult schemas, Data.Aeson.Schema.Utils.All.All Data.Aeson.Schema.Type.IsSchemaType schemas, GHC.Show.Show (Data.Aeson.Schema.Internal.SchemaResult ('Data.Aeson.Schema.Type.SchemaUnion schemas)), Data.Aeson.Types.FromJSON.FromJSON (Data.Aeson.Schema.Internal.SchemaResult ('Data.Aeson.Schema.Type.SchemaUnion schemas)), Data.Aeson.Types.ToJSON.ToJSON (Data.Aeson.Schema.Internal.SchemaResult ('Data.Aeson.Schema.Type.SchemaUnion schemas)), Data.Aeson.Schema.Internal.ParseSumType schemas) => Data.Aeson.Schema.Internal.HasSchemaResult ('Data.Aeson.Schema.Type.SchemaUnion schemas)
+ Data.Aeson.Schema.Internal: instance (Data.Aeson.Schema.Utils.All.All Data.Aeson.Schema.Internal.HasSchemaResultPair pairs, Data.Aeson.Schema.Type.IsSchemaObjectMap pairs) => Data.Aeson.Schema.Internal.HasSchemaResult ('Data.Aeson.Schema.Type.SchemaObject pairs)
+ Data.Aeson.Schema.Internal: instance (GHC.Show.Show inner, Data.Typeable.Internal.Typeable inner, Data.Aeson.Types.FromJSON.FromJSON inner, Data.Aeson.Types.ToJSON.ToJSON inner) => Data.Aeson.Schema.Internal.HasSchemaResult ('Data.Aeson.Schema.Type.SchemaScalar inner)
+ Data.Aeson.Schema.Internal: instance Data.Aeson.Schema.Internal.IsSchema schema => Data.Aeson.Types.FromJSON.FromJSON (Data.Aeson.Schema.Internal.Object schema)
+ Data.Aeson.Schema.Internal: instance Data.Aeson.Schema.Internal.IsSchema schema => Data.Aeson.Types.ToJSON.ToJSON (Data.Aeson.Schema.Internal.Object schema)
+ Data.Aeson.Schema.Internal: instance Data.Aeson.Schema.Internal.IsSchema schema => GHC.Classes.Eq (Data.Aeson.Schema.Internal.Object schema)
+ Data.Aeson.Schema.Internal: instance Data.Aeson.Schema.Internal.IsSchema schema => GHC.Show.Show (Data.Aeson.Schema.Internal.Object schema)
+ Data.Aeson.Schema.Internal: instance Data.Aeson.Schema.Internal.ParseSumType '[]
+ Data.Aeson.Schema.Internal: parseSumType :: ParseSumType xs => [Text] -> Value -> Parser (SumType (SchemaResultList xs))
+ Data.Aeson.Schema.Internal: parseValuePair :: HasSchemaResultPair a => Proxy a -> [Text] -> Object -> Parser (Text, Dynamic)
+ Data.Aeson.Schema.Internal: showSchemaType :: forall (schemaType :: SchemaType). HasSchemaResult schemaType => String
+ Data.Aeson.Schema.Internal: showValuePair :: HasSchemaResultPair a => Proxy a -> Object schema -> (String, ShowS)
+ Data.Aeson.Schema.Internal: toMap :: IsSchema ( 'Schema schema) => Object ( 'Schema schema) -> Object
+ Data.Aeson.Schema.Internal: toValue :: (HasSchemaResult schema, ToJSON (SchemaResult schema)) => SchemaResult schema -> Value
+ Data.Aeson.Schema.Internal: toValueMap :: forall pairs. All HasSchemaResultPair pairs => Object ( 'Schema pairs) -> Object
+ Data.Aeson.Schema.Internal: toValuePair :: HasSchemaResultPair a => Proxy a -> Object schema -> Object
+ Data.Aeson.Schema.Internal: type IsSchema (schema :: Schema) = (HasSchemaResult (ToSchemaObject schema), All HasSchemaResultPair (FromSchema schema), SchemaResult (ToSchemaObject schema) ~ Object schema)
+ Data.Aeson.Schema.Internal: unsafeGetKey :: forall (endSchema :: SchemaType) (key :: Symbol) (schema :: Schema). (KnownSymbol key, Typeable (SchemaResult endSchema)) => Proxy key -> Object schema -> SchemaResult endSchema
+ Data.Aeson.Schema.Key: class KnownSymbol (FromSchemaKey key) => IsSchemaKey (key :: SchemaKey) where {
+ Data.Aeson.Schema.Key: data SchemaKey' s
+ Data.Aeson.Schema.Key: fromSchemaKeyV :: SchemaKeyV -> String
+ Data.Aeson.Schema.Key: getContext :: SchemaKeyV -> Object -> Value
+ Data.Aeson.Schema.Key: instance Data.Hashable.Class.Hashable s => Data.Hashable.Class.Hashable (Data.Aeson.Schema.Key.SchemaKey' s)
+ Data.Aeson.Schema.Key: instance GHC.Classes.Eq s => GHC.Classes.Eq (Data.Aeson.Schema.Key.SchemaKey' s)
+ Data.Aeson.Schema.Key: instance GHC.Generics.Generic (Data.Aeson.Schema.Key.SchemaKey' s)
+ Data.Aeson.Schema.Key: instance GHC.Show.Show s => GHC.Show.Show (Data.Aeson.Schema.Key.SchemaKey' s)
+ Data.Aeson.Schema.Key: instance GHC.TypeLits.KnownSymbol key => Data.Aeson.Schema.Key.IsSchemaKey ('Data.Aeson.Schema.Key.NormalKey key)
+ Data.Aeson.Schema.Key: instance GHC.TypeLits.KnownSymbol key => Data.Aeson.Schema.Key.IsSchemaKey ('Data.Aeson.Schema.Key.PhantomKey key)
+ Data.Aeson.Schema.Key: instance Language.Haskell.TH.Syntax.Lift s => Language.Haskell.TH.Syntax.Lift (Data.Aeson.Schema.Key.SchemaKey' s)
+ Data.Aeson.Schema.Key: showSchemaKey :: forall key. IsSchemaKey key => String
+ Data.Aeson.Schema.Key: showSchemaKeyV :: SchemaKeyV -> String
+ Data.Aeson.Schema.Key: toContext :: SchemaKeyV -> Value -> Object
+ Data.Aeson.Schema.Key: toSchemaKeyV :: IsSchemaKey key => Proxy key -> SchemaKeyV
+ Data.Aeson.Schema.Key: type SchemaKey = SchemaKey' Symbol
+ Data.Aeson.Schema.Key: type SchemaKeyV = SchemaKey' String
+ Data.Aeson.Schema.Key: type family FromSchemaKey key :: Symbol;
+ Data.Aeson.Schema.Key: }
+ Data.Aeson.Schema.Show: SchemaScalar :: String -> SchemaType
+ Data.Aeson.Schema.Show: instance GHC.Classes.Eq Data.Aeson.Schema.Show.SchemaType
+ Data.Aeson.Schema.Type: NameRef :: String -> NameLike
+ Data.Aeson.Schema.Type: NameTH :: Name -> NameLike
+ Data.Aeson.Schema.Type: Schema :: SchemaObjectMap' s ty -> Schema' s ty
+ Data.Aeson.Schema.Type: SchemaList :: SchemaType' s ty -> SchemaType' s ty
+ Data.Aeson.Schema.Type: SchemaMaybe :: SchemaType' s ty -> SchemaType' s ty
+ Data.Aeson.Schema.Type: SchemaObject :: SchemaObjectMap' s ty -> SchemaType' s ty
+ Data.Aeson.Schema.Type: SchemaScalar :: ty -> SchemaType' s ty
+ Data.Aeson.Schema.Type: SchemaTry :: SchemaType' s ty -> SchemaType' s ty
+ Data.Aeson.Schema.Type: SchemaUnion :: [SchemaType' s ty] -> SchemaType' s ty
+ Data.Aeson.Schema.Type: class IsSchemaType (schemaType :: SchemaType)
+ Data.Aeson.Schema.Type: data NameLike
+ Data.Aeson.Schema.Type: data Schema' s ty
+ Data.Aeson.Schema.Type: data SchemaType' s ty
+ Data.Aeson.Schema.Type: fromSchemaV :: SchemaV -> SchemaObjectMapV
+ Data.Aeson.Schema.Type: instance (Data.Aeson.Schema.Key.IsSchemaKey key, Data.Aeson.Schema.Type.IsSchemaType inner) => Data.Aeson.Schema.Type.IsSchemaObjectPair '(key, inner)
+ Data.Aeson.Schema.Type: instance (GHC.Classes.Eq s, GHC.Classes.Eq ty) => GHC.Classes.Eq (Data.Aeson.Schema.Type.Schema' s ty)
+ Data.Aeson.Schema.Type: instance (GHC.Classes.Eq ty, GHC.Classes.Eq s) => GHC.Classes.Eq (Data.Aeson.Schema.Type.SchemaType' s ty)
+ Data.Aeson.Schema.Type: instance (GHC.Show.Show s, GHC.Show.Show ty) => GHC.Show.Show (Data.Aeson.Schema.Type.Schema' s ty)
+ Data.Aeson.Schema.Type: instance (GHC.Show.Show ty, GHC.Show.Show s) => GHC.Show.Show (Data.Aeson.Schema.Type.SchemaType' s ty)
+ Data.Aeson.Schema.Type: instance (Language.Haskell.TH.Syntax.Lift s, Language.Haskell.TH.Syntax.Lift ty) => Language.Haskell.TH.Syntax.Lift (Data.Aeson.Schema.Type.Schema' s ty)
+ Data.Aeson.Schema.Type: instance (Language.Haskell.TH.Syntax.Lift ty, Language.Haskell.TH.Syntax.Lift s) => Language.Haskell.TH.Syntax.Lift (Data.Aeson.Schema.Type.SchemaType' s ty)
+ Data.Aeson.Schema.Type: instance Data.Aeson.Schema.Type.IsSchemaObjectMap pairs => Data.Aeson.Schema.Type.IsSchemaType ('Data.Aeson.Schema.Type.SchemaObject pairs)
+ Data.Aeson.Schema.Type: instance Data.Aeson.Schema.Type.IsSchemaType inner => Data.Aeson.Schema.Type.IsSchemaType ('Data.Aeson.Schema.Type.SchemaList inner)
+ Data.Aeson.Schema.Type: instance Data.Aeson.Schema.Type.IsSchemaType inner => Data.Aeson.Schema.Type.IsSchemaType ('Data.Aeson.Schema.Type.SchemaMaybe inner)
+ Data.Aeson.Schema.Type: instance Data.Aeson.Schema.Type.IsSchemaType inner => Data.Aeson.Schema.Type.IsSchemaType ('Data.Aeson.Schema.Type.SchemaTry inner)
+ Data.Aeson.Schema.Type: instance Data.Aeson.Schema.Utils.All.All Data.Aeson.Schema.Type.IsSchemaType schemas => Data.Aeson.Schema.Type.IsSchemaType ('Data.Aeson.Schema.Type.SchemaUnion schemas)
+ Data.Aeson.Schema.Type: instance Data.Typeable.Internal.Typeable inner => Data.Aeson.Schema.Type.IsSchemaType ('Data.Aeson.Schema.Type.SchemaScalar inner)
+ Data.Aeson.Schema.Type: instance GHC.Classes.Eq Data.Aeson.Schema.Type.NameLike
+ Data.Aeson.Schema.Type: instance GHC.Show.Show Data.Aeson.Schema.Type.NameLike
+ Data.Aeson.Schema.Type: showSchemaTypeV :: SchemaTypeV -> String
+ Data.Aeson.Schema.Type: showSchemaV :: SchemaV -> String
+ Data.Aeson.Schema.Type: toSchemaObjectV :: SchemaV -> SchemaTypeV
+ Data.Aeson.Schema.Type: toSchemaTypeV :: IsSchemaType schemaType => Proxy schemaType -> SchemaTypeV
+ Data.Aeson.Schema.Type: type IsSchemaObjectMap (pairs :: SchemaObjectMap) = All IsSchemaObjectPair pairs
+ Data.Aeson.Schema.Type: type Schema = Schema' Symbol Type
+ Data.Aeson.Schema.Type: type SchemaObjectMapV = SchemaObjectMap' String NameLike
+ Data.Aeson.Schema.Type: type SchemaType = SchemaType' Symbol Type
+ Data.Aeson.Schema.Type: type SchemaTypeV = SchemaType' String NameLike
+ Data.Aeson.Schema.Type: type SchemaV = Schema' String NameLike
+ Data.Aeson.Schema.Type: type family FromSchema (schema :: Schema) :: SchemaObjectMap
+ Data.Aeson.Schema.Utils.All: class All f xs
+ Data.Aeson.Schema.Utils.All: foldrAll :: All f xs => (forall x. f x => Proxy x -> a -> a) -> a -> a
+ Data.Aeson.Schema.Utils.All: instance forall a (f :: a -> GHC.Types.Constraint) (x :: a) (xs :: [a]). (f x, Data.Aeson.Schema.Utils.All.All f xs) => Data.Aeson.Schema.Utils.All.All f (x : xs)
+ Data.Aeson.Schema.Utils.All: instance forall k1 k2 (f :: k2 -> GHC.Types.Constraint). Data.Aeson.Schema.Utils.All.All f '[]
+ Data.Aeson.Schema.Utils.All: mapAll :: forall a. All f xs => (forall x. f x => Proxy x -> a) -> [a]
+ Data.Aeson.Schema.Utils.Invariant: unreachable :: String -> a
+ Data.Aeson.Schema.Utils.Sum: instance (Data.Aeson.Types.ToJSON.ToJSON x, Data.Aeson.Types.ToJSON.ToJSON (Data.Aeson.Schema.Utils.Sum.SumType xs)) => Data.Aeson.Types.ToJSON.ToJSON (Data.Aeson.Schema.Utils.Sum.SumType (x : xs))
+ Data.Aeson.Schema.Utils.Sum: instance Data.Aeson.Types.ToJSON.ToJSON (Data.Aeson.Schema.Utils.Sum.SumType '[])
- Data.Aeson.Schema: data Object (schema :: SchemaType)
+ Data.Aeson.Schema: data Object (schema :: Schema)
- Data.Aeson.Schema.Internal: getKey :: forall key schema endSchema result. (endSchema ~ LookupSchema key schema, result ~ SchemaResult endSchema, KnownSymbol key, Typeable result, Typeable endSchema) => Object schema -> result
+ Data.Aeson.Schema.Internal: getKey :: forall (key :: Symbol) (schema :: Schema) (endSchema :: SchemaType) result. (endSchema ~ LookupSchema key schema, result ~ SchemaResult endSchema, KnownSymbol key, Typeable result, Typeable endSchema) => Proxy key -> Object schema -> result
- Data.Aeson.Schema.Internal: newtype Object (schema :: SchemaType)
+ Data.Aeson.Schema.Internal: newtype Object (schema :: Schema)
- Data.Aeson.Schema.Internal: parseFail :: forall (schema :: SchemaType) m a. (MonadFail m, Typeable schema) => [Text] -> Value -> m a
+ Data.Aeson.Schema.Internal: parseFail :: forall (schema :: SchemaType) m a. (MonadFail m, HasSchemaResult schema) => [Text] -> Value -> m a
- Data.Aeson.Schema.Internal: parseValue :: (IsSchemaType schema, FromJSON (SchemaResult schema)) => [Text] -> Value -> Parser (SchemaResult schema)
+ Data.Aeson.Schema.Internal: parseValue :: (HasSchemaResult schema, FromJSON (SchemaResult schema)) => [Text] -> Value -> Parser (SchemaResult schema)
- Data.Aeson.Schema.Internal: showSchema :: forall (a :: SchemaType). Typeable a => String
+ Data.Aeson.Schema.Internal: showSchema :: forall (schema :: Schema). IsSchema schema => String
- Data.Aeson.Schema.Internal: showValue :: (IsSchemaType schema, Show (SchemaResult schema)) => SchemaResult schema -> String
+ Data.Aeson.Schema.Internal: showValue :: (HasSchemaResult schema, Show (SchemaResult schema)) => SchemaResult schema -> ShowS
- Data.Aeson.Schema.Internal: type family LookupSchema (key :: Symbol) (schema :: SchemaType) :: SchemaType
+ Data.Aeson.Schema.Internal: type family LookupSchema (key :: Symbol) (schema :: Schema) :: SchemaType
- Data.Aeson.Schema.Key: NormalKey :: String -> SchemaKey
+ Data.Aeson.Schema.Key: NormalKey :: s -> SchemaKey' s
- Data.Aeson.Schema.Key: PhantomKey :: String -> SchemaKey
+ Data.Aeson.Schema.Key: PhantomKey :: s -> SchemaKey' s
- Data.Aeson.Schema.Key: fromSchemaKey :: SchemaKey -> String
+ Data.Aeson.Schema.Key: fromSchemaKey :: forall key. IsSchemaKey key => String
- Data.Aeson.Schema.Show: SchemaObject :: [(SchemaKey, SchemaType)] -> SchemaType
+ Data.Aeson.Schema.Show: SchemaObject :: [(SchemaKeyV, SchemaType)] -> SchemaType

Files

CHANGELOG.md view
@@ -1,5 +1,43 @@ ## Upcoming +## 1.3.0++Breaking changes:++* Refactored types to be correct by construction. Namely, the `schema` parameter in `Object schema` now has kind `Schema` instead of `SchemaType`, which prevents the possibility of a non-object schema stored in an `Object`. This means that any schemas previously annotated with the `SchemaType` kind should now be annotated as `Schema`.+* Instead of using `IsSchemaObject` is obviated because of this change, so it's been removed. You may use the new `IsSchema` instead, if you need it.+* `SchemaResult` has been removed from the export list of `Data.Aeson.Schema`. You probably won't need this in typical usage of this library, but if you need it, you can always get it from `Data.Aeson.Schema.Internal`.++New features:++* Add support for unwrapping into included schemas+* Add `toMap`+* Re-export `showSchema` in `Data.Aeson.Schema`++Bug fixes:++* Avoid requiring `TypeApplications` when using `get` quasiquoter ([#16](https://github.com/LeapYear/aeson-schemas/issues/16))+* Allow optional quotes around keys, both in getter-expressions and in schema definitions+* Allow `//` at the beginning of phantom keys (were previously parsed as comments)++Performance:++* We've added benchmarks! To view performance metrics, you can clone the repo and run `stack bench`. You may also view the benchmark statistics in CI, but due to Circle CI's memory limitations, we're forced to run them with `--fast`, so it'll be a factor slower than it would actually be at runtime.+* Fixed the `Show` instance from being `O(n^2)` to `O(n)`, where `n` is the depth of the object.+* In order to fix some bugs and implement new features, the `schema` quasiquoter took a performance hit. The biggest slowdown occurs if you're including other schemas like:++    ```+    {+        user: #UserSchema+    }+    ```++    If this causes your build to be noticeably slower, please open an issue. Thanks!++Miscellaneous changes:++* The `Show` instance for objects added some whitespace, from `{"foo": 0}` to `{ "foo": 0 }`+ ## 1.2.0  New features:
README.md view
@@ -2,6 +2,7 @@  ![CircleCI](https://img.shields.io/hackage/v/aeson-schemas) ![Hackage](https://img.shields.io/circleci/build/github/LeapYear/aeson-schemas)+[![codecov](https://codecov.io/gh/LeapYear/aeson-schemas/branch/master/graph/badge.svg)](https://codecov.io/gh/LeapYear/aeson-schemas)  A library that extracts information from JSON input using type-level schemas and quasiquoters, consuming JSON data in a type-safe manner. Better than@@ -57,8 +58,9 @@  ### Type safe -Since schemas are defined at the type level, parsing JSON objects is checked at-compile-time:+Since schemas are defined at the type level, extracting data from JSON objects+is checked at compile-time, meaning that using the `get` quasiquoter should+never throw an error at runtime.  ``` -- using schema from above@@ -77,11 +79,11 @@                 ('Data.Aeson.Schema.SchemaObject                    '[ '("id", 'Data.Aeson.Schema.SchemaInt),                       '("name", 'Data.Aeson.Schema.SchemaText)])))]-    • In the second argument of ‘(.)’, namely ‘getKey @"isEnabled"’+    • In the second argument of ‘(.)’, namely ‘getKey (Proxy :: Proxy "isEnabled")’       In the first argument of ‘(<$:>)’, namely-        ‘(id . getKey @"isEnabled")’+        ‘(id . getKey (Proxy :: Proxy "isEnabled"))’       In the first argument of ‘(.)’, namely-        ‘((id . getKey @"isEnabled") <$:>)’+        ‘((id . getKey (Proxy :: Proxy "isEnabled")) <$:>)’ ```  ### Point-free definitions@@ -93,7 +95,8 @@ getNames = [get| .users[].name |] ``` -You can use the `unwrap` quasiquoter to define intermediate schemas:+If you'd like to extract intermediate schemas, you can use the `unwrap`+quasiquoter:  ```haskell type User = [unwrap| MySchema.users[] |]@@ -177,22 +180,21 @@ ```haskell data Result = Result   { permissions :: [Permission]-  }-  deriving (Generic, FromJSON)+  } deriving (Show, Generic, FromJSON)  data Permission = Permission   { resource :: Resource   , access :: String-  } deriving (Generic, FromJSON)+  } deriving (Show, Generic, FromJSON)  data Resource = Resource   { name :: String   , owner :: Owner-  } deriving (Generic, FromJSON)+  } deriving (Show, Generic, FromJSON)  data Owner = Owner   { username :: String-  }+  } deriving (Show, Generic, FromJSON) ```  It might be fine for a single example like this, but if you have to parse this@@ -220,7 +222,7 @@ |] ``` -The only identifier added to the namespace is `Result`, and parsing out data+The only identifier added to the namespace is `Result`, and extracting data is easier and more readable:  ```haskell@@ -298,7 +300,7 @@     groupNames = map (\GroupNode{node = Group{name = name}} -> name) userGroups ``` -With this library, parsing is much more straightforward+With this library, extraction is much more straightforward  ```haskell let groupNames = [get| userNode.node.groups[].node.name |]
aeson-schemas.cabal view
@@ -1,13 +1,13 @@ cabal-version: >= 1.10 --- This file has been generated from package.yaml by hpack version 0.31.1.+-- This file has been generated from package.yaml by hpack version 0.33.0. -- -- see: https://github.com/sol/hpack ----- hash: f264efe1e9e5d79645f971635649d0dea5c3dedf8e8aee3513f201dd9386a438+-- hash: d195684258f0bf8fc5b3ab749824dc20c842da7acdb82d6e7aeb9e03c6165e28  name:           aeson-schemas-version:        1.2.0+version:        1.3.0 synopsis:       Easily consume JSON data on-demand with type-safety description:    Parse JSON data easily and safely without defining new data types. Useful                 for deeply nested JSON data, which is difficult to parse using the default@@ -23,92 +23,30 @@ extra-source-files:     README.md     CHANGELOG.md-    test/all_types.json-    test/nested.json-    test/goldens/bool.golden-    test/goldens/bool_int_double.golden-    test/goldens/double.golden-    test/goldens/enum.golden-    test/goldens/from_object_all_types.golden-    test/goldens/from_object_namespaced.golden-    test/goldens/from_object_nested.golden-    test/goldens/get_empty.golden-    test/goldens/get_just_start.golden-    test/goldens/get_ops_after_list.golden-    test/goldens/get_ops_after_tuple.golden-    test/goldens/getter_all_types_list.golden-    test/goldens/getter_all_types_list_item.golden-    test/goldens/int.golden-    test/goldens/int_int2.golden-    test/goldens/lambda_bool.golden-    test/goldens/list.golden-    test/goldens/list_explicit.golden-    test/goldens/list_maybeBool.golden-    test/goldens/list_maybeInt.golden-    test/goldens/list_type.golden-    test/goldens/maybeList.golden-    test/goldens/maybeList_bang.golden-    test/goldens/maybeList_bang_list.golden-    test/goldens/maybeList_bang_list_text.golden-    test/goldens/maybeList_list.golden-    test/goldens/maybeList_list_text.golden-    test/goldens/maybeListNull.golden-    test/goldens/maybeListNull_bang.golden-    test/goldens/maybeListNull_list.golden-    test/goldens/maybeListNull_list_text.golden-    test/goldens/maybeObj.golden-    test/goldens/maybeObj_bang.golden-    test/goldens/maybeObj_bang_text.golden-    test/goldens/maybeObj_text.golden-    test/goldens/maybeObjNull.golden-    test/goldens/maybeObjNull_text.golden-    test/goldens/nonexistent.golden-    test/goldens/phantom.golden+    test/goldens/fromjson_error_messages_truncate.golden+    test/goldens/fromjson_list_inner_invalid.golden+    test/goldens/fromjson_list_invalid.golden+    test/goldens/fromjson_maybe_invalid.golden+    test/goldens/fromjson_nested_inner_invalid.golden+    test/goldens/fromjson_nested_invalid.golden+    test/goldens/fromjson_object_invalid.golden+    test/goldens/fromjson_object_later_keys_invalid.golden+    test/goldens/fromjson_phantom_inner_invalid.golden+    test/goldens/fromjson_phantom_inner_missing.golden+    test/goldens/fromjson_phantom_invalid.golden+    test/goldens/fromjson_scalar_invalid.golden+    test/goldens/fromjson_union_invalid.golden+    test/goldens/getqq_empty_expression.golden+    test/goldens/getqq_missing_key.golden+    test/goldens/getqq_no_operators.golden+    test/goldens/getqq_ops_after_list.golden+    test/goldens/getqq_ops_after_tuple.golden     test/goldens/README_Quickstart.golden-    test/goldens/scalar.golden-    test/goldens/schema_def_bool.golden-    test/goldens/schema_def_custom.golden-    test/goldens/schema_def_double.golden-    test/goldens/schema_def_duplicate.golden-    test/goldens/schema_def_duplicate_extend.golden-    test/goldens/schema_def_duplicate_phantom.golden-    test/goldens/schema_def_extend.golden-    test/goldens/schema_def_import_user.golden-    test/goldens/schema_def_int.golden-    test/goldens/schema_def_invalid_extend.golden-    test/goldens/schema_def_list.golden-    test/goldens/schema_def_list_obj.golden-    test/goldens/schema_def_maybe.golden-    test/goldens/schema_def_maybe_obj.golden-    test/goldens/schema_def_nonobject_phantom.golden-    test/goldens/schema_def_not_object.golden-    test/goldens/schema_def_obj.golden-    test/goldens/schema_def_shadow.golden-    test/goldens/schema_def_text.golden-    test/goldens/schema_def_union.golden-    test/goldens/schema_def_union_grouped.golden-    test/goldens/schema_def_unknown_type.golden-    test/goldens/text.golden-    test/goldens/tryObj.golden-    test/goldens/tryObj_a.golden-    test/goldens/tryObj_bang.golden-    test/goldens/tryObj_bang_a.golden-    test/goldens/tryObjNull.golden-    test/goldens/tryObjNull_a.golden-    test/goldens/union.golden-    test/goldens/union_0.golden-    test/goldens/union_0_a.golden-    test/goldens/union_1.golden-    test/goldens/union_2.golden-    test/goldens/unwrap_schema.golden-    test/goldens/unwrap_schema_bad_bang.golden-    test/goldens/unwrap_schema_bad_branch.golden-    test/goldens/unwrap_schema_bad_key.golden-    test/goldens/unwrap_schema_bad_list.golden-    test/goldens/unwrap_schema_bad_question.golden-    test/goldens/unwrap_schema_branch_out_of_bounds.golden-    test/goldens/unwrap_schema_nested_list.golden-    test/goldens/unwrap_schema_nested_object.golden+    test/goldens/schemaqq_key_with_invalid_character.golden+    test/goldens/schemaqq_key_with_trailing_escape.golden+    test/goldens/sumtype_decode_invalid.golden+    test/goldens/unwrapqq_unwrap_past_list.golden+    test/goldens/unwrapqq_unwrap_past_tuple.golden  source-repository head   type: git@@ -121,8 +59,10 @@       Data.Aeson.Schema.Key       Data.Aeson.Schema.Show       Data.Aeson.Schema.TH+      Data.Aeson.Schema.Type+      Data.Aeson.Schema.Utils.All+      Data.Aeson.Schema.Utils.Invariant       Data.Aeson.Schema.Utils.Sum-      Data.Aeson.Schema.Utils.TypeFamilies   other-modules:       Data.Aeson.Schema.TH.Enum       Data.Aeson.Schema.TH.Get@@ -137,9 +77,9 @@   build-depends:       aeson >=1.1.2.0 && <1.6     , base >=4.9 && <5-    , bytestring >=0.10.8.1 && <0.11     , first-class-families >=0.3.0.0 && <0.9-    , megaparsec >=6.0.0 && <9+    , hashable >=1.2.7.0 && <1.4+    , megaparsec >=6.0.0 && <10     , template-haskell >=2.12.0.0 && <2.17     , text >=1.2.2.2 && <1.3     , unordered-containers >=0.2.8.0 && <0.3@@ -153,32 +93,85 @@   type: exitcode-stdio-1.0   main-is: Main.hs   other-modules:-      AllTypes-      Enums-      Nested-      Schema-      SumType-      Util+      Tests.EnumTH+      Tests.GetQQ+      Tests.GetQQ.TH+      Tests.MkGetter+      Tests.Object+      Tests.Object.Eq+      Tests.Object.FromJSON+      Tests.Object.FromJSON.TH+      Tests.Object.Show+      Tests.Object.Show.TH+      Tests.Object.ToJSON+      Tests.SchemaQQ+      Tests.SchemaQQ.TH+      Tests.SumType+      Tests.UnwrapQQ+      Tests.UnwrapQQ.TH+      TestUtils+      TestUtils.Arbitrary+      TestUtils.DeepSeq       Paths_aeson_schemas   hs-source-dirs:       test   ghc-options: -Wall   build-depends:-      QuickCheck >=2.7 && <3+      QuickCheck     , aeson >=1.1.2.0 && <1.6+    , aeson-qq     , aeson-schemas     , base >=4.9 && <5-    , bytestring >=0.10.8.1 && <0.11+    , deepseq     , first-class-families >=0.3.0.0 && <0.9-    , megaparsec >=6.0.0 && <9-    , raw-strings-qq >=1.1 && <1.2-    , tasty >=0.11.3 && <1.4-    , tasty-golden >=2.3.1.2 && <2.4-    , tasty-hunit >=0.9 && <0.11-    , tasty-quickcheck >=0.9 && <0.11+    , hashable >=1.2.7.0 && <1.4+    , hint+    , interpolate+    , megaparsec >=6.0.0 && <10+    , raw-strings-qq+    , tasty+    , tasty-golden+    , tasty-hunit+    , tasty-quickcheck     , template-haskell >=2.12.0.0 && <2.17     , text >=1.2.2.2 && <1.3-    , th-test-utils >=1.0.0 && <1.1+    , th-orphans+    , th-test-utils+    , unordered-containers >=0.2.8.0 && <0.3+  if impl(ghc >= 8.0)+    ghc-options: -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wnoncanonical-monad-instances+  if impl(ghc < 8.8)+    ghc-options: -Wnoncanonical-monadfail-instances+  default-language: Haskell2010++benchmark aeson-schemas-bench+  type: exitcode-stdio-1.0+  main-is: Main.hs+  other-modules:+      Benchmarks.Data.Objects+      Benchmarks.Data.Schemas+      Benchmarks.Data.Schemas.TH+      Benchmarks.FromJSON+      Benchmarks.SchemaQQ+      Benchmarks.Show+      Benchmarks.ToJSON+      Utils.DeepSeq+      Paths_aeson_schemas+  hs-source-dirs:+      bench+  ghc-options: -Wall+  build-depends:+      aeson >=1.1.2.0 && <1.6+    , aeson-schemas+    , base >=4.9 && <5+    , criterion+    , deepseq+    , first-class-families >=0.3.0.0 && <0.9+    , hashable >=1.2.7.0 && <1.4+    , megaparsec >=6.0.0 && <10+    , template-haskell >=2.12.0.0 && <2.17+    , text >=1.2.2.2 && <1.3+    , th-test-utils     , unordered-containers >=0.2.8.0 && <0.3   if impl(ghc >= 8.0)     ghc-options: -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wnoncanonical-monad-instances
+ bench/Benchmarks/Data/Objects.hs view
@@ -0,0 +1,55 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeInType #-}++module Benchmarks.Data.Objects where++import Data.Aeson (ToJSON(..), Value)+import Data.Dynamic (Dynamic, Typeable, toDyn)+import qualified Data.HashMap.Strict as HashMap+import Data.Proxy (Proxy(..))+import Data.Text (Text)+import qualified Data.Text as Text++import Data.Aeson.Schema.Internal (Object(..), SchemaResult)+import Data.Aeson.Schema.Key (IsSchemaKey, SchemaKey, fromSchemaKey)+import Data.Aeson.Schema.Type+    (IsSchemaObjectMap, SchemaType, SchemaType'(..), ToSchemaObject)+import Data.Aeson.Schema.Utils.All (All(..))++type MockSchema schema =+  ( MockSchemaResult (ToSchemaObject schema)+  , Object schema ~ SchemaResult (ToSchemaObject schema)+  , ToJSON (Object schema)+  )++schemaObject :: forall schema. MockSchema schema => Object schema+schemaObject = schemaResult (Proxy @(ToSchemaObject schema))++schemaValue :: forall schema. MockSchema schema => Value+schemaValue = toJSON $ schemaObject @schema++class Typeable (SchemaResult schema) => MockSchemaResult (schema :: SchemaType) where+  schemaResult :: Proxy schema -> SchemaResult schema++instance MockSchemaResult ('SchemaScalar Int) where+  schemaResult _ = 42++instance+  ( All MockSchemaResultPair pairs+  , IsSchemaObjectMap pairs+  , Typeable pairs+  ) => MockSchemaResult ('SchemaObject pairs) where+  schemaResult _ = UnsafeObject $ HashMap.fromList $ mapAll @MockSchemaResultPair @pairs schemaResultPair++class MockSchemaResultPair (pair :: (SchemaKey, SchemaType)) where+  schemaResultPair :: Proxy pair -> (Text, Dynamic)++instance (IsSchemaKey key, MockSchemaResult inner) => MockSchemaResultPair '(key, inner) where+  schemaResultPair _ = (Text.pack $ fromSchemaKey @key, toDyn $ schemaResult $ Proxy @inner)
+ bench/Benchmarks/Data/Schemas.hs view
@@ -0,0 +1,96 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TemplateHaskell #-}++module Benchmarks.Data.Schemas+  ( module Benchmarks.Data.Schemas+  , module Benchmarks.Data.Schemas.TH+  ) where++import Control.Monad (forM)+import Data.Char (chr, toUpper)+import Language.Haskell.TH+import Language.Haskell.TH.Syntax (lift)+import Language.Haskell.TH.TestUtils ()++import Benchmarks.Data.Schemas.TH++-- Generates:+--+--   type Schema1   = { a1: Int }+--   type Schema5   = { a1: Int, a2: Int, ... a5: Int }+--   type Schema10  = { a1: Int, a2: Int, ... a10: Int }+--   type Schema100 = { a1: Int, a2: Int, ... a100: Int }+$(do+  -- The sizes of schemas to generate+  let schemaSizes = [1, 5, 10, 100]+      allSchemas = flip map schemaSizes $ \n ->+        let name = "Schema" ++ show n+        in (n, name, mkName name)++  concat <$> sequence+    [ forM allSchemas $ \(n, _, name) -> genSchema name $ keysTo n+    , [d|+        sizedSchemas :: [(Int, String)]+        sizedSchemas = $(lift $ flip map allSchemas $ \(n, name, _) -> (n, name))++        sizedSchemasNames :: [(String, Name)]+        sizedSchemasNames = $(lift $ flip map allSchemas $ \(_, name, thName) -> (name, thName))+      |]+    ]+  )++-- Generates:+--+--   type SchemaNest1   = { a1: Int }+--   type SchemaNest5   = { a1: { a2: { ... a5: Int } }+--   type SchemaNest10  = { a1: { a2: { ... a10: Int } }+--   type SchemaNest100 = { a1: { a2: { ... a100: Int } }+$(do+  -- The depths of schemas to generate+  let schemaSizes = [1, 5, 10, 100]+      allSchemas = flip map schemaSizes $ \n ->+        let name = "SchemaNest" ++ show n+        in (n, name, mkName name)++  concat <$> sequence+    [ forM allSchemas $ \(n, _, name) -> genSchema' name $+        foldr (\i inner -> genSchemaDef [Field (mkField i) inner]) "Int" [1..n]+    , [d|+        nestedSchemas :: [(Int, String)]+        nestedSchemas = $(lift $ flip map allSchemas $ \(n, name, _) -> (n, name))++        nestedSchemasNames :: [(String, Name)]+        nestedSchemasNames = $(lift $ flip map allSchemas $ \(_, name, thName) -> (name, thName))+      |]+    ]+  )++-- Generates:+--+--   type SchemaA1 = { a1: Int }+--   type SchemaB1 = { b1: Int }+--   ...+--   type SchemaZ1 = { z1: Int }+--   type SchemaA2 = { a2: Int }+--   type SchemaB2 = { a2: Int }+--   ...+$(do+  let numSchemas = 100+      allSchemas = flip map [1..numSchemas] $ \n ->+        let (q, r) = n `divMod` 26+            c = chr $ 97 + r -- a .. z+            field = c : show q+            name = "Schema" ++ map toUpper field+        in (field, name, mkName name)++  concat <$> sequence+    [ forM allSchemas $ \(field, _, name) -> genSchema name [Field field "Int"]+    , [d|+        singleSchemas :: [String]+        singleSchemas = $(lift $ flip map allSchemas $ \(_, name, _) -> name)++        singleSchemasNames :: [(String, Name)]+        singleSchemasNames = $(lift $ flip map allSchemas $ \(_, name, thName) -> (name, thName))+      |]+    ]+  )
+ bench/Benchmarks/Data/Schemas/TH.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE LambdaCase #-}++module Benchmarks.Data.Schemas.TH+  ( SchemaDef(..)+  , genSchema+  , genSchema'+  , genSchemaDef+  , keysTo+  , mkField+  ) where++import Data.List (intercalate)+import Language.Haskell.TH+import Language.Haskell.TH.Quote++import Data.Aeson.Schema (schema)++data SchemaDef+  = Field String String   -- ^ { a: Int }+  | Include String String -- ^ { a: #OtherSchema }+  | Ref String            -- ^ { #OtherSchema }++genSchema :: Name -> [SchemaDef] -> DecQ+genSchema name = genSchema' name . genSchemaDef++genSchema' :: Name -> String -> DecQ+genSchema' name = tySynD name [] . quoteType schema++genSchemaDef :: [SchemaDef] -> String+genSchemaDef schemaDef = "{" ++ intercalate "," (map fromSchemaDef schemaDef) ++ "}"+  where+    fromSchemaDef = \case+      Field key ty -> key ++ ": " ++ ty+      Include key name -> key ++ ": #" ++ name+      Ref name -> "#" ++ name++keysTo :: Int -> [SchemaDef]+keysTo n = map (\i -> Field (mkField i) "Int") [1..n]++mkField :: Int -> String+mkField i = "a" ++ show i
+ bench/Benchmarks/FromJSON.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE UndecidableInstances #-}+{-# OPTIONS_GHC -Wno-orphans #-}+{-# OPTIONS_GHC -freduction-depth=0 #-}++module Benchmarks.FromJSON where++import Control.DeepSeq (NFData(..))+import Criterion.Main+import qualified Data.Aeson as Aeson++import Data.Aeson.Schema (Object)++import Benchmarks.Data.Objects+import Benchmarks.Data.Schemas++benchmarks :: Benchmark+benchmarks = bgroup "FromJSON instance"+  [ byKeys+  , byNestedKeys+  ]+  where+    byKeys = bgroup "# of keys"+      [ bench "1" $ nf (Aeson.fromJSON @(Object Schema1)) (schemaValue @Schema1)+      , bench "5" $ nf (Aeson.fromJSON @(Object Schema5)) (schemaValue @Schema5)+      , bench "10" $ nf (Aeson.fromJSON @(Object Schema10)) (schemaValue @Schema10)+      , bench "100" $ nf (Aeson.fromJSON @(Object Schema100)) (schemaValue @Schema100)+      ]++    byNestedKeys = bgroup "# of nested keys"+      [ bench "1" $ nf (Aeson.fromJSON @(Object SchemaNest1)) (schemaValue @SchemaNest1)+      , bench "5" $ nf (Aeson.fromJSON @(Object SchemaNest5)) (schemaValue @SchemaNest5)+      , bench "10" $ nf (Aeson.fromJSON @(Object SchemaNest10)) (schemaValue @SchemaNest10)+      , bench "100" $ nf (Aeson.fromJSON @(Object SchemaNest100)) (schemaValue @SchemaNest100)+      ]++{- Orphans -}++instance Show (Object schema) => NFData (Object schema) where+  rnf = rnf . show
+ bench/Benchmarks/SchemaQQ.hs view
@@ -0,0 +1,69 @@+{-# LANGUAGE TemplateHaskell #-}++module Benchmarks.SchemaQQ where++import Criterion.Main+import Language.Haskell.TH.Quote (QuasiQuoter(quoteType))+import Language.Haskell.TH.TestUtils+    (QMode(..), QState(..), loadNames, runTestQ)++import qualified Data.Aeson.Schema++import Benchmarks.Data.Schemas+import Utils.DeepSeq ()++benchmarks :: Benchmark+benchmarks = bgroup "schema quasiquoter"+  [ byKeys+  , byNestedKeys+  , byNumOfIncluded+  , byKeysInIncluded+  , byNumOfExtended+  , byKeysInExtended+  ]+  where+    byKeys = bgroup "# of keys" $+      flip map [1, 5, 10, 100] $ \n ->+        let schemaDef = genSchemaDef $ keysTo n+        in bench (show n) $ nf runSchema schemaDef++    byNestedKeys = bgroup "# of nested keys" $+      flip map [1, 5, 10, 100] $ \n ->+        let schemaDef = iterateN n (\prev -> genSchemaDef [Field "a" prev]) "Int"+        in bench (show n) $ nf runSchema schemaDef++    byNumOfIncluded = bgroup "Include given # of schemas" $+      flip map [1, 5, 10, 100] $ \n ->+        let schemaDef = genSchemaDef $ map (\name -> Include name name) $ take n singleSchemas+        in bench (show n) $ nf runSchema schemaDef++    byKeysInIncluded = bgroup "Include schema with given # of keys" $+      flip map sizedSchemas $ \(n, schema) ->+        let schemaDef = genSchemaDef [Include "a" schema]+        in bench (show n) $ nf runSchema schemaDef++    byNumOfExtended = bgroup "Extend given # of schemas" $+      flip map [1, 5, 10, 100] $ \n ->+        let schemaDef = genSchemaDef $ map Ref $ take n singleSchemas+        in bench (show n) $ nf runSchema schemaDef++    byKeysInExtended = bgroup "Extend schema with given # of keys" $+      flip map sizedSchemas $ \(n, schema) ->+        let schemaDef = genSchemaDef [Field "a" "Int", Ref schema]+        in bench (show n) $ nf runSchema schemaDef++    runSchema =+      let qstate = QState+            { mode = MockQ+            , knownNames = sizedSchemasNames ++ singleSchemasNames+            , reifyInfo = $(loadNames $ map snd $ sizedSchemasNames ++ singleSchemasNames)+            }+      in runTestQ qstate . quoteType Data.Aeson.Schema.schema++{- Utilities -}++-- | Apply the given functions the given number of times.+--+-- The first parameter must be >= 0.+iterateN :: Int -> (a -> a) -> a -> a+iterateN n f x = iterate f x !! n
+ bench/Benchmarks/Show.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE TypeApplications #-}+{-# OPTIONS_GHC -freduction-depth=0 #-}++module Benchmarks.Show where++import Criterion.Main++import Benchmarks.Data.Objects+import Benchmarks.Data.Schemas++benchmarks :: Benchmark+benchmarks = bgroup "Show instance"+  [ byKeys+  , byNestedKeys+  ]+  where+    byKeys = bgroup "# of keys"+      [ bench "1" $ nf show (schemaObject @Schema1)+      , bench "5" $ nf show (schemaObject @Schema5)+      , bench "10" $ nf show (schemaObject @Schema10)+      , bench "100" $ nf show (schemaObject @Schema100)+      ]++    byNestedKeys = bgroup "# of nested keys"+      [ bench "1" $ nf show (schemaObject @SchemaNest1)+      , bench "5" $ nf show (schemaObject @SchemaNest5)+      , bench "10" $ nf show (schemaObject @SchemaNest10)+      , bench "100" $ nf show (schemaObject @SchemaNest100)+      ]
+ bench/Benchmarks/ToJSON.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE TypeApplications #-}+{-# OPTIONS_GHC -freduction-depth=0 #-}++module Benchmarks.ToJSON where++import Criterion.Main+import qualified Data.Aeson as Aeson++import Benchmarks.Data.Objects+import Benchmarks.Data.Schemas++benchmarks :: Benchmark+benchmarks = bgroup "ToJSON instance"+  [ byKeys+  , byNestedKeys+  ]+  where+    byKeys = bgroup "# of keys"+      [ bench "1" $ nf Aeson.toJSON (schemaObject @Schema1)+      , bench "5" $ nf Aeson.toJSON (schemaObject @Schema5)+      , bench "10" $ nf Aeson.toJSON (schemaObject @Schema10)+      , bench "100" $ nf Aeson.toJSON (schemaObject @Schema100)+      ]++    byNestedKeys = bgroup "# of nested keys"+      [ bench "1" $ nf Aeson.toJSON (schemaObject @SchemaNest1)+      , bench "5" $ nf Aeson.toJSON (schemaObject @SchemaNest5)+      , bench "10" $ nf Aeson.toJSON (schemaObject @SchemaNest10)+      , bench "100" $ nf Aeson.toJSON (schemaObject @SchemaNest100)+      ]
+ bench/Main.hs view
@@ -0,0 +1,14 @@+import Criterion.Main++import qualified Benchmarks.FromJSON+import qualified Benchmarks.SchemaQQ+import qualified Benchmarks.Show+import qualified Benchmarks.ToJSON++main :: IO ()+main = defaultMain+  [ Benchmarks.SchemaQQ.benchmarks+  , Benchmarks.Show.benchmarks+  , Benchmarks.FromJSON.benchmarks+  , Benchmarks.ToJSON.benchmarks+  ]
+ bench/Utils/DeepSeq.hs view
@@ -0,0 +1,64 @@+{-# LANGUAGE CPP #-}+{-# OPTIONS_GHC -Wno-orphans #-}++module Utils.DeepSeq () where++import Control.DeepSeq (NFData(..))+#if MIN_VERSION_template_haskell(2,16,0)+import Control.DeepSeq (rwhnf)+import GHC.ForeignPtr (ForeignPtr)+#endif+import Language.Haskell.TH.Syntax++instance NFData AnnTarget+instance NFData Bang+instance NFData Body+instance NFData Callconv+instance NFData Clause+instance NFData Con+instance NFData Dec+instance NFData DerivClause+instance NFData DerivStrategy+instance NFData Exp+instance NFData FamilyResultSig+instance NFData Fixity+instance NFData FixityDirection+instance NFData Foreign+instance NFData FunDep+instance NFData Guard+instance NFData InjectivityAnn+instance NFData Inline+instance NFData Lit+instance NFData Match+instance NFData ModName+instance NFData Name+instance NFData NameFlavour+instance NFData NameSpace+instance NFData OccName+instance NFData Overlap+instance NFData Pat+instance NFData PatSynArgs+instance NFData PatSynDir+instance NFData Phases+instance NFData PkgName+instance NFData Pragma+instance NFData Range+instance NFData Role+instance NFData RuleBndr+instance NFData RuleMatch+instance NFData Safety+instance NFData SourceStrictness+instance NFData SourceUnpackedness+instance NFData Stmt+instance NFData Type+instance NFData TypeFamilyHead+instance NFData TyLit+instance NFData TySynEqn+instance NFData TyVarBndr++#if MIN_VERSION_template_haskell(2,16,0)+instance NFData Bytes++instance NFData (ForeignPtr a) where+  rnf = rwhnf+#endif
src/Data/Aeson/Schema.hs view
@@ -10,11 +10,13 @@ -}  module Data.Aeson.Schema-  ( -- * Types+  ( -- * Object     Object-  , SchemaType-  , IsSchemaObject-  , SchemaResult+  , toMap+    -- * Schemas+  , Schema+  , IsSchema+  , showSchema     -- * Quasiquoters for extracting or manipulating JSON data or schemas   , schema   , get@@ -24,3 +26,4 @@  import Data.Aeson.Schema.Internal import Data.Aeson.Schema.TH+import Data.Aeson.Schema.Type
src/Data/Aeson/Schema/Internal.hs view
@@ -9,18 +9,16 @@ {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE CPP #-} {-# LANGUAGE ConstraintKinds #-}-{-# LANGUAGE DataKinds #-} {-# LANGUAGE DefaultSignatures #-}-{-# LANGUAGE ExplicitNamespaces #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-}-{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilyDependencies #-} {-# LANGUAGE TypeInType #-} {-# LANGUAGE TypeOperators #-}@@ -28,240 +26,239 @@  module Data.Aeson.Schema.Internal where -import Control.Applicative ((<|>))+import Control.Applicative (Alternative(..)) #if !MIN_VERSION_base(4,13,0) import Control.Monad.Fail (MonadFail) #endif-import Data.Aeson (FromJSON(..), Value(..))+import Data.Aeson (FromJSON(..), ToJSON(..), Value(..))+import qualified Data.Aeson as Aeson import Data.Aeson.Types (Parser)-import Data.Bifunctor (first)-import Data.Dynamic (Dynamic, fromDyn, fromDynamic, toDyn)+import Data.Dynamic (Dynamic, fromDynamic, toDyn) import Data.HashMap.Strict (HashMap, (!)) import qualified Data.HashMap.Strict as HashMap-import Data.Kind (Type)+import Data.List (intersperse) import Data.Maybe (fromMaybe) import Data.Proxy (Proxy(..)) import Data.Text (Text) import qualified Data.Text as Text-import Data.Typeable (Typeable, splitTyConApp, tyConName, typeRep, typeRepTyCon)+import Data.Typeable (Typeable) import Fcf (type (<=<), type (=<<)) import qualified Fcf import GHC.Exts (toList) import GHC.TypeLits     (ErrorMessage(..), KnownSymbol, Symbol, TypeError, symbolVal) -import qualified Data.Aeson.Schema.Show as SchemaShow-import Data.Aeson.Schema.Utils.Sum (SumType)-import Data.Aeson.Schema.Utils.TypeFamilies (All)+import Data.Aeson.Schema.Key+    ( IsSchemaKey(..)+    , SchemaKey+    , SchemaKey'(..)+    , fromSchemaKeyV+    , getContext+    , showSchemaKey+    , toContext+    )+import Data.Aeson.Schema.Type+    ( FromSchema+    , IsSchemaObjectMap+    , IsSchemaType(..)+    , Schema+    , Schema'(..)+    , SchemaType+    , SchemaType'(..)+    , ToSchemaObject+    , showSchemaTypeV+    )+import Data.Aeson.Schema.Utils.All (All(..))+import Data.Aeson.Schema.Utils.Invariant (unreachable)+import Data.Aeson.Schema.Utils.Sum (SumType(..))  {- Schema-validated JSON object -}  -- | The object containing JSON data and its schema. ----- Has a 'FromJSON' instance, so you can use the usual 'Data.Aeson' decoding functions.+-- Has a 'FromJSON' instance, so you can use the usual @Data.Aeson@ decoding functions. -- -- > obj = decode "{\"a\": 1}" :: Maybe (Object [schema| { a: Int } |])-newtype Object (schema :: SchemaType) = UnsafeObject (HashMap Text Dynamic)+newtype Object (schema :: Schema) = UnsafeObject (HashMap Text Dynamic) --- | A constraint that checks if the given schema is a 'SchemaObject.-type IsSchemaObject schema = (IsSchemaType schema, SchemaResult schema ~ Object schema)+instance IsSchema schema => Show (Object schema) where+  showsPrec _ = showValue @(ToSchemaObject schema) -instance IsSchemaObject schema => Show (Object schema) where-  show = showValue @schema+instance IsSchema schema => Eq (Object schema) where+  a == b = toJSON a == toJSON b -instance IsSchemaObject schema => FromJSON (Object schema) where-  parseJSON = parseValue @schema []+instance IsSchema schema => FromJSON (Object schema) where+  parseJSON = parseValue @(ToSchemaObject schema) [] -{- Type-level schema definitions -}+instance IsSchema schema => ToJSON (Object schema) where+  toJSON = toValue @(ToSchemaObject schema) --- | The type-level schema definition for JSON data.+-- | Convert an 'Object' into a 'HashMap', losing the type information in the schema. ----- To view a schema for debugging, use 'showSchema'.-data SchemaType-  = SchemaBool-  | SchemaInt-  | SchemaDouble-  | SchemaText-  | SchemaCustom Type-  | SchemaMaybe SchemaType-  | SchemaTry SchemaType-  | SchemaList SchemaType-  | SchemaObject [(SchemaKey, SchemaType)]-  | SchemaUnion [SchemaType] -- ^ @since v1.1.0---- | Convert 'SchemaType' into 'SchemaShow.SchemaType'.-toSchemaTypeShow :: forall (a :: SchemaType). Typeable a => SchemaShow.SchemaType-toSchemaTypeShow = cast $ typeRep (Proxy @a)-  where-    cast tyRep = case splitTypeRep tyRep of-      ("'SchemaBool", _) -> SchemaShow.SchemaBool-      ("'SchemaInt", _) -> SchemaShow.SchemaInt-      ("'SchemaDouble", _) -> SchemaShow.SchemaDouble-      ("'SchemaText", _) -> SchemaShow.SchemaText-      ("'SchemaCustom", [inner]) -> SchemaShow.SchemaCustom $ typeRepName inner-      ("'SchemaMaybe", [inner]) -> SchemaShow.SchemaMaybe $ cast inner-      ("'SchemaTry", [inner]) -> SchemaShow.SchemaTry $ cast inner-      ("'SchemaList", [inner]) -> SchemaShow.SchemaList $ cast inner-      ("'SchemaObject", [pairs]) -> SchemaShow.SchemaObject $ map getSchemaObjectPair $ typeRepToList pairs-      ("'SchemaUnion", [schemas]) -> SchemaShow.SchemaUnion $ map cast $ typeRepToList schemas-      _ -> error $ "Unknown schema type: " ++ show tyRep--    getSchemaObjectPair tyRep =-      let (key, val) = typeRepToPair tyRep-          fromTypeRep = tail . init . typeRepName -- strip leading + trailing quote-          schemaKey = case splitTypeRep key of-            ("'NormalKey", [key']) -> SchemaShow.NormalKey $ fromTypeRep key'-            ("'PhantomKey", [key']) -> SchemaShow.PhantomKey $ fromTypeRep key'-            _ -> error $ "Unknown schema key: " ++ show key-      in (schemaKey, cast val)--    typeRepToPair tyRep = case splitTypeRep tyRep of-      ("'(,)", [a, b]) -> (a, b)-      _ -> error $ "Unknown pair: " ++ show tyRep--    typeRepToList tyRep = case splitTypeRep tyRep of-      ("'[]", []) -> []-      ("':", [x, rest]) -> x : typeRepToList rest-      _ -> error $ "Unknown list: " ++ show tyRep--    splitTypeRep = first tyConName . splitTyConApp-    typeRepName = tyConName . typeRepTyCon---- | Pretty show the given SchemaType.-showSchema :: forall (a :: SchemaType). Typeable a => String-showSchema = SchemaShow.showSchemaType $ toSchemaTypeShow @a---- | The type-level analogue of 'Data.Aeson.Schema.Key.SchemaKey'.-data SchemaKey-  = NormalKey Symbol-  | PhantomKey Symbol--type family FromSchemaKey (schemaKey :: SchemaKey) where-  FromSchemaKey ('NormalKey key) = key-  FromSchemaKey ('PhantomKey key) = key+-- @since 1.3.0+toMap :: IsSchema ('Schema schema) => Object ('Schema schema) -> Aeson.Object+toMap = toValueMap -fromSchemaKey :: forall schemaKey. KnownSymbol (FromSchemaKey schemaKey) => Text-fromSchemaKey = Text.pack $ symbolVal $ Proxy @(FromSchemaKey schemaKey)+{- Type-level schema definitions -} -class-  ( Typeable schemaKey-  , KnownSymbol (FromSchemaKey schemaKey)-  ) => KnownSchemaKey (schemaKey :: SchemaKey) where-  getContext :: HashMap Text Value -> Value+-- | The constraint for most operations involving @Object schema@. If you're writing functions+-- on general Objects, you should use this constraint. e.g.+--+-- > logObject :: (MonadLogger m, IsSchema schema) => Object schema -> m ()+-- > logObject = logInfoN . Text.pack . show+--+-- @since 1.3.0+type IsSchema (schema :: Schema) =+  ( HasSchemaResult (ToSchemaObject schema)+  , All HasSchemaResultPair (FromSchema schema)+  , SchemaResult (ToSchemaObject schema) ~ Object schema+  ) -instance KnownSymbol key => KnownSchemaKey ('NormalKey key) where-  getContext = fromMaybe Null . HashMap.lookup (fromSchemaKey @('NormalKey key))+-- | Show the given schema.+--+-- Usage:+--+-- > type MySchema = [schema| { a: Int } |]+-- > showSchema @MySchema+--+showSchema :: forall (schema :: Schema). IsSchema schema => String+showSchema = showSchemaType @(ToSchemaObject schema) -instance KnownSymbol key => KnownSchemaKey ('PhantomKey key) where-  getContext = Object+showSchemaType :: forall (schemaType :: SchemaType). HasSchemaResult schemaType => String+showSchemaType = showSchemaTypeV schemaType+  where+    schemaType = toSchemaTypeV $ Proxy @schemaType  {- Conversions from schema types into Haskell types -}  -- | A type family mapping SchemaType to the corresponding Haskell type. type family SchemaResult (schema :: SchemaType) where-  SchemaResult 'SchemaBool = Bool-  SchemaResult 'SchemaInt = Int-  SchemaResult 'SchemaDouble = Double-  SchemaResult 'SchemaText = Text-  SchemaResult ('SchemaCustom inner) = inner+  SchemaResult ('SchemaScalar inner) = inner   SchemaResult ('SchemaMaybe inner) = Maybe (SchemaResult inner)   SchemaResult ('SchemaTry inner) = Maybe (SchemaResult inner)   SchemaResult ('SchemaList inner) = [SchemaResult inner]-  SchemaResult ('SchemaObject inner) = Object ('SchemaObject inner)   SchemaResult ('SchemaUnion schemas) = SumType (SchemaResultList schemas)+  SchemaResult ('SchemaObject inner) = Object ('Schema inner)  type family SchemaResultList (xs :: [SchemaType]) where   SchemaResultList '[] = '[]   SchemaResultList (x ': xs) = SchemaResult x ': SchemaResultList xs  -- | A type-class for types that can be parsed from JSON for an associated schema type.-class Typeable schema => IsSchemaType (schema :: SchemaType) where+class IsSchemaType schema => HasSchemaResult (schema :: SchemaType) where   parseValue :: [Text] -> Value -> Parser (SchemaResult schema)   default parseValue :: FromJSON (SchemaResult schema) => [Text] -> Value -> Parser (SchemaResult schema)   parseValue path value = parseJSON value <|> parseFail @schema path value -  showValue :: SchemaResult schema -> String-  default showValue :: Show (SchemaResult schema) => SchemaResult schema -> String-  showValue = show--instance IsSchemaType 'SchemaBool--instance IsSchemaType 'SchemaInt--instance IsSchemaType 'SchemaDouble+  toValue :: SchemaResult schema -> Value+  default toValue :: ToJSON (SchemaResult schema) => SchemaResult schema -> Value+  toValue = toJSON -instance IsSchemaType 'SchemaText+  -- Note: Using ShowS here instead of just returning String to avoid quadratic performance when+  -- using (++)+  showValue :: SchemaResult schema -> ShowS+  default showValue :: Show (SchemaResult schema) => SchemaResult schema -> ShowS+  showValue = shows -instance (Show inner, Typeable inner, FromJSON inner) => IsSchemaType ('SchemaCustom inner)+instance (Show inner, Typeable inner, FromJSON inner, ToJSON inner) => HasSchemaResult ('SchemaScalar inner) -instance (IsSchemaType inner, Show (SchemaResult inner)) => IsSchemaType ('SchemaMaybe inner) where+instance (HasSchemaResult inner, Show (SchemaResult inner), ToJSON (SchemaResult inner)) => HasSchemaResult ('SchemaMaybe inner) where   parseValue path = \case     Null -> return Nothing     value -> (Just <$> parseValue @inner path value) -instance (IsSchemaType inner, Show (SchemaResult inner)) => IsSchemaType ('SchemaTry inner) where+instance (HasSchemaResult inner, Show (SchemaResult inner), ToJSON (SchemaResult inner)) => HasSchemaResult ('SchemaTry inner) where   parseValue path = wrapTry . parseValue @inner path     where       wrapTry parser = (Just <$> parser) <|> pure Nothing -instance (IsSchemaType inner, Show (SchemaResult inner)) => IsSchemaType ('SchemaList inner) where-  parseValue path value = case value of+instance (HasSchemaResult inner, Show (SchemaResult inner), ToJSON (SchemaResult inner)) => HasSchemaResult ('SchemaList inner) where+  parseValue path = \case     Array a -> traverse (parseValue @inner path) (toList a)-    _ -> parseFail @('SchemaList inner) path value+    value -> parseFail @('SchemaList inner) path value -instance IsSchemaType ('SchemaObject '[]) where-  parseValue path = \case-    Object _ -> return $ UnsafeObject mempty-    value -> parseFail @('SchemaObject '[]) path value+instance+  ( All HasSchemaResult schemas+  , All IsSchemaType schemas+  , Show (SchemaResult ('SchemaUnion schemas))+  , FromJSON (SchemaResult ('SchemaUnion schemas))+  , ToJSON (SchemaResult ('SchemaUnion schemas))+  , ParseSumType schemas+  ) => HasSchemaResult ('SchemaUnion (schemas :: [SchemaType])) where+  parseValue path value = parseSumType @schemas path value <|> parseFail @('SchemaUnion schemas) path value -  showValue _ = "{}"+class ParseSumType xs where+  parseSumType :: [Text] -> Value -> Parser (SumType (SchemaResultList xs)) -instance-  ( KnownSchemaKey schemaKey-  , IsSchemaType inner-  , Show (SchemaResult inner)-  , Typeable (SchemaResult inner)-  , IsSchemaObject ('SchemaObject rest)-  , Typeable rest-  ) => IsSchemaType ('SchemaObject ('(schemaKey, inner) ': rest)) where-  parseValue path value = case value of-    Object o -> do-      let key = fromSchemaKey @schemaKey-          innerVal = getContext @schemaKey o+instance ParseSumType '[] where+  parseSumType _ _ = empty -      inner <- parseValue @inner (key:path) innerVal-      UnsafeObject rest <- parseValue @('SchemaObject rest) path value+instance (HasSchemaResult schema, ParseSumType schemas) => ParseSumType (schema ': schemas) where+  parseSumType path value = parseHere <|> parseThere+    where+      parseHere = Here <$> parseValue @schema path value+      parseThere = There <$> parseSumType @schemas path value -      return $ UnsafeObject $ HashMap.insert key (toDyn inner) rest-    _ -> parseFail @('SchemaObject ('(schemaKey, inner) ': rest)) path value+instance (All HasSchemaResultPair pairs, IsSchemaObjectMap pairs) => HasSchemaResult ('SchemaObject pairs) where+  parseValue path = \case+    Aeson.Object o -> UnsafeObject . HashMap.fromList <$> parseValueMap o+    value -> parseFail @('SchemaObject pairs) path value+    where+      parseValueMap :: Aeson.Object -> Parser [(Text, Dynamic)]+      parseValueMap o = sequence $ mapAll @HasSchemaResultPair @pairs $ \proxy -> parseValuePair proxy path o -  showValue (UnsafeObject hm) = case showValue @('SchemaObject rest) (UnsafeObject hm) of-    "{}" -> "{" ++ pair ++ "}"-    '{':s -> "{" ++ pair ++ ", " ++ s-    s -> error $ "Unknown result when showing Object: " ++ s+  toValue = Aeson.Object . toValueMap++  showValue o = showString "{ " . intercalateShowS ", " (map fromPair pairs) . showString " }"     where-      key = fromSchemaKey @schemaKey-      value =-        let dynValue = hm ! key-        in maybe (show dynValue) show $ fromDynamic @(SchemaResult inner) dynValue-      pair = show key ++ ": " ++ value+      fromPair (k, v) = showString k . showString ": " . v+      pairs = mapAll @HasSchemaResultPair @pairs $ \proxy -> showValuePair proxy o +      -- intercalate for ShowS+      intercalateShowS :: String -> [ShowS] -> ShowS+      intercalateShowS s = concatShowS . intersperse (showString s)++      concatShowS :: [ShowS] -> ShowS+      concatShowS = foldr (.) id++toValueMap :: forall pairs. All HasSchemaResultPair pairs => Object ('Schema pairs) -> Aeson.Object+toValueMap o = HashMap.unions $ mapAll @HasSchemaResultPair @pairs (\proxy -> toValuePair proxy o)++class HasSchemaResultPair (a :: (SchemaKey, SchemaType)) where+  parseValuePair :: Proxy a -> [Text] -> Aeson.Object -> Parser (Text, Dynamic)+  toValuePair :: Proxy a -> Object schema -> Aeson.Object+  showValuePair :: Proxy a -> Object schema -> (String, ShowS)+ instance-  ( All IsSchemaType schemas-  , Typeable schemas-  , Show (SchemaResult ('SchemaUnion schemas))-  , FromJSON (SchemaResult ('SchemaUnion schemas))-  ) => IsSchemaType ('SchemaUnion schemas)+  ( IsSchemaKey key+  , HasSchemaResult inner+  , Typeable (SchemaResult inner)+  ) => HasSchemaResultPair '(key, inner) where+  parseValuePair _ path o = do+    inner <- parseValue @inner (key:path) $ getContext schemaKey o+    return (key, toDyn inner)+    where+      schemaKey = toSchemaKeyV $ Proxy @key+      key = Text.pack $ fromSchemaKeyV schemaKey +  toValuePair _ o = toContext schemaKey (toValue @inner val)+    where+      schemaKey = toSchemaKeyV $ Proxy @key+      val = unsafeGetKey @inner (Proxy @(FromSchemaKey key)) o++  showValuePair _ o = (showSchemaKey @key, showValue @inner val)+    where+      val = unsafeGetKey @inner (Proxy @(FromSchemaKey key)) o+ -- | A helper for creating fail messages when parsing a schema.-parseFail :: forall (schema :: SchemaType) m a. (MonadFail m, Typeable schema) => [Text] -> Value -> m a+parseFail :: forall (schema :: SchemaType) m a. (MonadFail m, HasSchemaResult schema) => [Text] -> Value -> m a parseFail path value = fail $ msg ++ ": " ++ ellipses 200 (show value)   where     msg = if null path       then "Could not parse schema " ++ schema'       else "Could not parse path '" ++ path' ++ "' with schema " ++ schema'     path' = Text.unpack . Text.intercalate "." $ reverse path-    schema' = "`" ++ showSchema @schema ++ "`"+    schema' = "`" ++ showSchemaType @schema ++ "`"     ellipses n s = if length s > n then take n s ++ "..." else s  {- Lookups within SchemaObject -}@@ -274,8 +271,8 @@ type Lookup a = Fcf.Map Fcf.Snd <=< Fcf.Find (Fcf.TyEq a <=< Fcf.Fst)  -- | The type-level function that return the schema of the given key in a 'SchemaObject'.-type family LookupSchema (key :: Symbol) (schema :: SchemaType) :: SchemaType where-  LookupSchema key ('SchemaObject schema) = Fcf.Eval+type family LookupSchema (key :: Symbol) (schema :: Schema) :: SchemaType where+  LookupSchema key ('Schema schema) = Fcf.Eval     ( Fcf.FromMaybe (TypeError         (     'Text "Key '"         ':<>: 'Text key@@ -285,12 +282,6 @@       )       =<< Lookup key =<< Fcf.Map (Fcf.Bimap UnSchemaKey Fcf.Pure) schema     )-  LookupSchema key schema = TypeError-    (     'Text "Attempted to lookup key '"-    ':<>: 'Text key-    ':<>: 'Text "' in the following schema:"-    ':$$: 'ShowType schema-    )  -- | Get a key from the given 'Data.Aeson.Schema.Internal.Object', returned as the type encoded in -- its schema.@@ -305,22 +296,30 @@ -- >                 ] -- >             ) -- >--- > getKey @"foo" o                  :: Bool--- > getKey @"bar" o                  :: Object ('SchemaObject '[ '("name", 'SchemaText) ])--- > getKey @"name" $ getKey @"bar" o :: Text--- > getKey @"baz" o                  :: Maybe Bool+-- > getKey (Proxy @"foo") o                  :: Bool+-- > getKey (Proxy @"bar") o                  :: Object ('SchemaObject '[ '("name", 'SchemaText) ])+-- > getKey (Proxy @"name") $ getKey @"bar" o :: Text+-- > getKey (Proxy @"baz") o                  :: Maybe Bool -- getKey-  :: forall key schema endSchema result-   . ( endSchema ~ LookupSchema key schema+  :: forall (key :: Symbol) (schema :: Schema) (endSchema :: SchemaType) result.+     ( endSchema ~ LookupSchema key schema      , result ~ SchemaResult endSchema      , KnownSymbol key      , Typeable result      , Typeable endSchema      )-  => Object schema+  => Proxy key+  -> Object schema   -> result-getKey (UnsafeObject object) = fromDyn (object ! Text.pack key) badCast+getKey = unsafeGetKey @endSchema++unsafeGetKey+  :: forall (endSchema :: SchemaType) (key :: Symbol) (schema :: Schema)+  . (KnownSymbol key, Typeable (SchemaResult endSchema))+  => Proxy key -> Object schema -> SchemaResult endSchema+unsafeGetKey keyProxy (UnsafeObject object) =+  fromMaybe (unreachable $ "Could not load key: " ++ key) $+    fromDynamic (object ! Text.pack key)   where-    key = symbolVal (Proxy @key)-    badCast = error $ "Could not load key: " ++ key+    key = symbolVal keyProxy
src/Data/Aeson/Schema/Key.hs view
@@ -6,20 +6,100 @@  Defines a SchemaKey. -}+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DeriveLift #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeInType #-}  module Data.Aeson.Schema.Key-  ( SchemaKey(..)+  ( SchemaKey'(..)+  , SchemaKeyV+  , fromSchemaKeyV+  , showSchemaKeyV+  , getContext+  , toContext+  , SchemaKey+  , IsSchemaKey(..)   , fromSchemaKey+  , showSchemaKey   ) where +import qualified Data.Aeson as Aeson+import Data.Hashable (Hashable)+import qualified Data.HashMap.Strict as HashMap+import Data.Proxy (Proxy(..))+import qualified Data.Text as Text+import GHC.Generics (Generic)+import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)+import Language.Haskell.TH.Syntax (Lift)++import Data.Aeson.Schema.Utils.Invariant (unreachable)+ -- | A key in a JSON object schema.-data SchemaKey-  = NormalKey String-  | PhantomKey String+data SchemaKey' s+  = NormalKey s+  | PhantomKey s     -- ^ A key that doesn't actually exist in the object, but whose content should be parsed from     -- the current object.-  deriving (Show)+  deriving (Show, Eq, Generic, Hashable, Lift) -fromSchemaKey :: SchemaKey -> String-fromSchemaKey (NormalKey key) = key-fromSchemaKey (PhantomKey key) = key+-- | A value-level SchemaKey+type SchemaKeyV = SchemaKey' String++fromSchemaKeyV :: SchemaKeyV -> String+fromSchemaKeyV (NormalKey key) = key+fromSchemaKeyV (PhantomKey key) = key++showSchemaKeyV :: SchemaKeyV -> String+showSchemaKeyV (NormalKey key) = show key+showSchemaKeyV (PhantomKey key) = "[" ++ key ++ "]"++-- | Given schema `{ key: innerSchema }` for JSON data `{ key: val1 }`, get the JSON+-- Value that `innerSchema` should parse.+getContext :: SchemaKeyV -> Aeson.Object -> Aeson.Value+getContext = \case+  -- `innerSchema` should parse `val1`+  NormalKey key -> HashMap.lookupDefault Aeson.Null (Text.pack key)+  -- `innerSchema` should parse the same object that `key` is in+  PhantomKey _ -> Aeson.Object++-- | Given JSON data `val` adhering to `innerSchema`, get the JSON object that should be+-- merged with the outer JSON object.+toContext :: SchemaKeyV -> Aeson.Value -> Aeson.Object+toContext = \case+  -- `val` should be inserted with key `key`+  NormalKey key -> HashMap.singleton (Text.pack key)+  -- If `val` is an object, it should be merged with the outer JSON object+  PhantomKey _ -> \case+    Aeson.Object o -> o+    -- `Try` schema could store `Nothing`, which would return `Null`. In this case, there is no+    -- context to merge+    Aeson.Null -> mempty+    v -> unreachable $ "Invalid value for phantom key: " ++ show v++-- | A type-level SchemaKey+type SchemaKey = SchemaKey' Symbol++class KnownSymbol (FromSchemaKey key) => IsSchemaKey (key :: SchemaKey) where+  type FromSchemaKey key :: Symbol+  toSchemaKeyV :: Proxy key -> SchemaKeyV++instance KnownSymbol key => IsSchemaKey ('NormalKey key) where+  type FromSchemaKey ('NormalKey key) = key+  toSchemaKeyV _ = NormalKey $ symbolVal $ Proxy @key++instance KnownSymbol key => IsSchemaKey ('PhantomKey key) where+  type FromSchemaKey ('PhantomKey key) = key+  toSchemaKeyV _ = PhantomKey $ symbolVal $ Proxy @key++fromSchemaKey :: forall key. IsSchemaKey key => String+fromSchemaKey = fromSchemaKeyV $ toSchemaKeyV $ Proxy @key++showSchemaKey :: forall key. IsSchemaKey key => String+showSchemaKey = showSchemaKeyV $ toSchemaKeyV $ Proxy @key
src/Data/Aeson/Schema/Show.hs view
@@ -11,53 +11,39 @@ module Data.Aeson.Schema.Show   ( SchemaType(..)   , showSchemaType-    -- * Re-exports-  , SchemaKey(..)   ) where  import Data.List (intercalate) -import Data.Aeson.Schema.Key (SchemaKey(..), fromSchemaKey)+import Data.Aeson.Schema.Key (SchemaKeyV, showSchemaKeyV)  -- | 'Data.Aeson.Schema.Internal.SchemaType', but for printing. data SchemaType-  = SchemaBool-  | SchemaInt-  | SchemaDouble-  | SchemaText-  | SchemaCustom String+  = SchemaScalar String   | SchemaMaybe SchemaType   | SchemaTry SchemaType   | SchemaList SchemaType-  | SchemaObject [(SchemaKey, SchemaType)]+  | SchemaObject [(SchemaKeyV, SchemaType)]   | SchemaUnion [SchemaType]-  deriving (Show)+  deriving (Show,Eq)  -- | Pretty show the given SchemaType. showSchemaType :: SchemaType -> String showSchemaType schema = case schema of-  SchemaBool -> "SchemaBool"-  SchemaInt -> "SchemaInt"-  SchemaDouble -> "SchemaDouble"-  SchemaText -> "SchemaText"-  SchemaCustom s -> "SchemaCustom " ++ s+  SchemaScalar s -> "SchemaScalar " ++ s   SchemaMaybe inner -> "SchemaMaybe " ++ showSchemaType' inner   SchemaTry inner -> "SchemaTry " ++ showSchemaType' inner   SchemaList inner -> "SchemaList " ++ showSchemaType' inner-  SchemaObject _ -> "SchemaObject " ++ showSchemaType' schema   SchemaUnion _ -> "SchemaUnion " ++ showSchemaType' schema+  SchemaObject _ -> "SchemaObject " ++ showSchemaType' schema   where     showSchemaType' = \case-      SchemaBool -> "Bool"-      SchemaInt -> "Int"-      SchemaDouble -> "Double"-      SchemaText -> "Text"-      SchemaCustom s -> s+      SchemaScalar s -> s       SchemaMaybe inner -> "Maybe " ++ showSchemaType' inner       SchemaTry inner -> "Try " ++ showSchemaType' inner       SchemaList inner -> "List " ++ showSchemaType' inner-      SchemaObject pairs -> "{" ++ mapJoin showPair ", " pairs ++ "}"       SchemaUnion schemas -> "( " ++ mapJoin showSchemaType' " | " schemas ++ " )"-    showPair (key, inner) = "\"" ++ fromSchemaKey key ++ "\": " ++ showSchemaType' inner+      SchemaObject pairs -> "{ " ++ mapJoin showPair ", " pairs ++ " }"+    showPair (key, inner) = showSchemaKeyV key ++ ": " ++ showSchemaType' inner      mapJoin f delim = intercalate delim . map f
src/Data/Aeson/Schema/TH/Get.hs view
@@ -13,17 +13,19 @@  module Data.Aeson.Schema.TH.Get where -import Control.Monad (unless, (>=>))+import Control.Monad ((>=>))+import Data.List (intercalate)+import Data.List.NonEmpty (NonEmpty)+import qualified Data.List.NonEmpty as NonEmpty import qualified Data.Maybe as Maybe import Data.Proxy (Proxy(..)) import GHC.Stack (HasCallStack) import Language.Haskell.TH import Language.Haskell.TH.Quote (QuasiQuoter(..))-import Language.Haskell.TH.Syntax (lift)  import Data.Aeson.Schema.Internal (getKey)-import Data.Aeson.Schema.TH.Parse (GetterExp(..), getterExp, parse)-import Data.Aeson.Schema.TH.Utils (GetterOperation(..), showGetterOps)+import Data.Aeson.Schema.TH.Parse+    (GetterExp(..), GetterOperation(..), GetterOps, parseGetterExp) import Data.Aeson.Schema.Utils.Sum (fromSumType)  -- | Defines a QuasiQuoter for extracting JSON data.@@ -80,52 +82,57 @@ --   the sum type contains an 'Int'. get :: QuasiQuoter get = QuasiQuoter-  { quoteExp = parse getterExp >=> generateGetterExp+  { quoteExp = parseGetterExp >=> generateGetterExp   , quoteDec = error "Cannot use `get` for Dec"   , quoteType = error "Cannot use `get` for Type"   , quotePat = error "Cannot use `get` for Pat"   }  generateGetterExp :: GetterExp -> ExpQ-generateGetterExp GetterExp{..} = maybe expr (appE expr . varE . mkName) start+generateGetterExp GetterExp{..} = applyStart $ resolveGetterOpExps $ mkGetterOpExps [] getterOps   where+    applyStart expr = maybe expr (appE expr . varE . mkName) start+     startDisplay = case start of       Nothing -> ""       Just s -> if '.' `elem` s then "(" ++ s ++ ")" else s-    expr = mkGetterExp [] getterOps -    applyToNext next = \case-      Right f -> [| $next . $f |]-      Left f -> infixE (Just next) f Nothing+    mkGetterOpExps :: [GetterOperation] -> GetterOps -> GetterOpExps+    mkGetterOpExps historyPrefix = mapWithHistory (mkGetterOpExp . (historyPrefix ++)) -    applyToEach history fromElems elems = do-      val <- newName "v"-      let mkElem ops = appE (mkGetterExp history ops) (varE val)-      lamE [varP val] $ fromElems $ map mkElem elems+    mkGetterOpExp :: [GetterOperation] -> GetterOperation -> GetterOpExp+    mkGetterOpExp history = \case+      GetterKey key ->+        let keyType = litT $ strTyLit key+        in ApplyOp [| getKey (Proxy :: Proxy $keyType) |] -    mkGetterExp history = \case-      [] -> [| id |]-      op:ops ->-        let applyToNext' = applyToNext $ mkGetterExp (op:history) ops-            applyToEach' = applyToEach history-            checkLast label = unless (null ops) $ fail $ label ++ " operation MUST be last."-            fromJustMsg = startDisplay ++ showGetterOps (reverse history)-        in case op of-          GetterKey key       -> applyToNext' $ Right $ appTypeE [| getKey |] (litT $ strTyLit key)-          GetterList elems    -> checkLast ".[*]" >> applyToEach' listE elems-          GetterTuple elems   -> checkLast ".(*)" >> applyToEach' tupE elems-          GetterBang          -> applyToNext' $ Right [| fromJust $(lift fromJustMsg) |]-          GetterMapMaybe      -> applyToNext' $ Left [| (<$?>) |]-          GetterMapList       -> applyToNext' $ Left [| (<$:>) |]-          GetterBranch branch ->-            let branchTyLit = litT $ numTyLit $ fromIntegral branch-            in applyToNext' $ Right [| fromSumType (Proxy :: Proxy $branchTyLit) |]+      GetterBang ->+        let expr = startDisplay ++ showGetterOps history+        in ApplyOp [| fromJust expr |] +      GetterMapMaybe ->+        ApplyOpInfix [| (<$?>) |]++      GetterMapList ->+        ApplyOpInfix [| (<$:>) |]++      GetterBranch branch ->+        let branchType = litT $ numTyLit $ fromIntegral branch+        in ApplyOp [| fromSumType (Proxy :: Proxy $branchType) |]++      GetterList elemOps ->+        ApplyOpsIntoList $ mkGetterOpExps history <$> elemOps++      GetterTuple elemOps ->+        ApplyOpsIntoTuple $ mkGetterOpExps history <$> elemOps++{- Runtime helpers -}+ -- | fromJust with helpful error message fromJust :: HasCallStack => String -> Maybe a -> a-fromJust msg = Maybe.fromMaybe (error errMsg)+fromJust expr = Maybe.fromMaybe (error errMsg)   where-    errMsg = "Called 'fromJust' on null expression" ++ if null msg then "" else ": " ++ msg+    errMsg = "Called 'fromJust' on null expression" ++ if null expr then "" else ": " ++ expr  -- | fmap specialized to Maybe (<$?>) :: (a -> b) -> Maybe a -> Maybe b@@ -134,3 +141,54 @@ -- | fmap specialized to [a] (<$:>) :: (a -> b) -> [a] -> [b] (<$:>) = (<$>)++{- Code generation helpers -}++data GetterOpExp+  = ApplyOp ExpQ                              -- ^ next . f+  | ApplyOpInfix ExpQ                         -- ^ (next `f`)+  | ApplyOpsIntoList (NonEmpty GetterOpExps)  -- ^ \v -> [f1 v, f2 v, ...]+  | ApplyOpsIntoTuple (NonEmpty GetterOpExps) -- ^ \v -> (f1 v, f2 v, ...)++type GetterOpExps = NonEmpty GetterOpExp++resolveGetterOpExps :: GetterOpExps -> ExpQ+resolveGetterOpExps (op NonEmpty.:| ops) =+  case op of+    ApplyOp f -> [| $next . $f |]+    ApplyOpInfix f -> infixE (Just next) f Nothing++    -- suffixes; ops should be empty+    ApplyOpsIntoList elemOps -> resolveEach listE elemOps+    ApplyOpsIntoTuple elemOps -> resolveEach tupE elemOps+  where+    next = maybe [| id |] resolveGetterOpExps $ NonEmpty.nonEmpty ops++    resolveEach fromElems elemOps = do+      val <- newName "v"+      let applyVal expr = appE expr (varE val)+      lamE [varP val] $ fromElems $ map (applyVal . resolveGetterOpExps) $ NonEmpty.toList elemOps++showGetterOps :: Foldable t => t GetterOperation -> String+showGetterOps = concatMap showGetterOp+  where+    showGetterOp = \case+      GetterKey key -> '.':key+      GetterBang -> "!"+      GetterMapList -> "[]"+      GetterMapMaybe -> "?"+      GetterBranch x -> '@' : show x+      GetterList elemOps -> ".[" ++ showGetterOpsList elemOps ++ "]"+      GetterTuple elemOps -> ".(" ++ showGetterOpsList elemOps ++ ")"++    showGetterOpsList = intercalate "," . NonEmpty.toList . fmap showGetterOps++{- Utilities -}++-- | Run the given function for each element in the list, providing all elements seen so far.+--+-- e.g. for a list [1,2,3], this will return the result of+--+--   [f [] 1, f [1] 2, f [1,2] 3]+mapWithHistory :: ([a] -> a -> b) -> NonEmpty a -> NonEmpty b+mapWithHistory f xs = NonEmpty.zipWith f (NonEmpty.inits xs) xs
src/Data/Aeson/Schema/TH/Getter.hs view
@@ -17,18 +17,23 @@ import Language.Haskell.TH  import Data.Aeson.Schema.TH.Get (generateGetterExp)-import Data.Aeson.Schema.TH.Parse (GetterExp(..), getterExp, parse)-import Data.Aeson.Schema.TH.Utils (reifySchema, unwrapType)+import Data.Aeson.Schema.TH.Parse (GetterExp(..), parseGetterExp)+import Data.Aeson.Schema.TH.Unwrap+    (FunctorHandler(..), unwrapSchema, unwrapSchemaUsing)+import Data.Aeson.Schema.TH.Utils (reifySchemaName, schemaVToTypeQ)  -- | A helper that generates a 'Data.Aeson.Schema.TH.get' expression and a type alias for the result -- of the expression. ----- > mkGetter "Node" "getNodes" ''MySchema ".nodes![]"+-- > mkGetter "Node" "getNodes" ''MySchema ".nodes[]" -- >--- > -- is equivalent to:--- > type Node = [unwrap| MySchema.nodes[] |] -- Object [schema| { b: Maybe Bool } |]+-- > {- is equivalent to -}+-- >+-- > -- | Node ~ { b: Maybe Bool }+-- > type Node = [unwrap| MySchema.nodes[] |]+-- > -- > getNodes :: Object MySchema -> [Node]--- > getNodes = [get| .nodes![] |]+-- > getNodes = [get| .nodes[] |] -- -- 'mkGetter' takes four arguments: --@@ -44,42 +49,47 @@ -- There is one subtlety that occurs from the use of the same @ops@ string for both the -- 'Data.Aeson.Schema.TH.unwrap' and 'Data.Aeson.Schema.TH.get' quasiquoters: -- 'Data.Aeson.Schema.TH.unwrap' strips out intermediate functors, while 'Data.Aeson.Schema.TH.get'--- applies within the functor. So in the above example, @".nodes![]"@ strips out the list when--- saving the schema to @Node@, while in the below example, @".nodes!"@ doesn't strip out the list+-- applies within the functor. So in the above example, @".nodes[]"@ strips out the list when+-- saving the schema to @Node@, while in the below example, @".nodes"@ doesn't strip out the list -- when saving the schema to @Nodes@. -- -- > mkGetter "Nodes" "getNodes" ''MySchema ".nodes" -- >--- > -- is equivalent to:--- > type Nodes = [unwrap| MySchema.nodes! |] -- [Object [schema| { b: Maybe Bool } |]]+-- > {- is equivalent to -}+-- >+-- > -- | Nodes ~ List { b: Maybe Bool }+-- > type Nodes = [unwrap| MySchema.nodes |]+-- > -- > getNodes :: Object MySchema -> Nodes--- > getNodes = [get| .nodes! |]+-- > getNodes = [get| .nodes |] -- -- As another example, -- -- > mkGetter "MyName" "getMyName" ''MySchema ".f?[].name" -- >--- > -- is equivalent to:--- > type MyName = [unwrap| MySchema.f?[].name |] -- Text+-- > {- is equivalent to -}+-- >+-- > -- | MyName ~ Text+-- > type MyName = [unwrap| MySchema.f?[].name |]+-- > -- > getMyBool :: Object MySchema -> Maybe [MyName] -- > getMyBool = [get| .f?[].name |] mkGetter :: String -> String -> Name -> String -> DecsQ mkGetter unwrapName funcName startSchemaName ops = do-  -- TODO: allow (Object schema)-  startSchemaType <- reifySchema startSchemaName--  getterExp'@GetterExp{..} <- parse getterExp ops+  getterExp@GetterExp{..} <- parseGetterExp ops   unless (isNothing start) $     fail $ "Getter expression should start with '.': " ++ ops -  let unwrapResult = unwrapType False getterOps startSchemaType-      funcResult = unwrapType True getterOps startSchemaType-      getterFunc = generateGetterExp getterExp'+  startSchema <- reifySchemaName startSchemaName++  let unwrapResult = unwrapSchema getterOps startSchema+      funcResult = unwrapSchemaUsing ApplyFunctors getterOps startSchema+      getterFunc = generateGetterExp getterExp       unwrapName' = mkName unwrapName       funcName' = mkName funcName    sequence     [ tySynD unwrapName' [] unwrapResult-    , sigD funcName' [t| Object $(pure startSchemaType) -> $funcResult |]+    , sigD funcName' [t| Object $(schemaVToTypeQ startSchema) -> $funcResult |]     , funD funcName' [clause [] (normalB getterFunc) []]     ]
src/Data/Aeson/Schema/TH/Parse.hs view
@@ -12,22 +12,20 @@  module Data.Aeson.Schema.TH.Parse where -#if !MIN_VERSION_megaparsec(6,4,0)-import Control.Applicative (empty)-#endif-import Control.Monad (void)+import Control.Monad (MonadPlus, void) #if !MIN_VERSION_base(4,13,0) import Control.Monad.Fail (MonadFail) #endif import Data.Functor (($>)) import Data.List (intercalate)+import Data.List.NonEmpty (NonEmpty)+import qualified Data.List.NonEmpty as NonEmpty import Data.Void (Void)-import Text.Megaparsec+import Text.Megaparsec hiding (sepBy1, sepEndBy1, some)+import qualified Text.Megaparsec as Megaparsec import Text.Megaparsec.Char import qualified Text.Megaparsec.Char.Lexer as L -import Data.Aeson.Schema.TH.Utils (GetterOperation(..), GetterOps)- type Parser = Parsec Void String  #if !MIN_VERSION_megaparsec(7,0,0)@@ -35,31 +33,43 @@ errorBundlePretty = parseErrorPretty #endif -parse :: MonadFail m => Parser a -> String -> m a-parse parser s = either (fail . errorBundlePretty) return $ runParser parser s s+runParserFail :: MonadFail m => Parser a -> String -> m a+runParserFail parser s = either (fail . errorBundlePretty) return $ runParser parser s s -{- Parser primitives -}+{- SchemaDef -} -parseGetterOp :: Parser GetterOperation-parseGetterOp = choice-  [ lexeme "!" $> GetterBang-  , lexeme "[]" $> GetterMapList-  , lexeme "?" $> GetterMapMaybe-  , lexeme "@" *> (GetterBranch . read <$> some digitChar)-  , optional (lexeme ".") *> choice-      [ GetterKey <$> jsonKey-      , fmap GetterList $ between (lexeme "[") (lexeme "]") $ some parseGetterOp `sepBy1` lexeme ","-      , fmap GetterTuple $ between (lexeme "(") (lexeme ")") $ some parseGetterOp `sepBy1` lexeme ","-      ]-  ]+data SchemaDef+  = SchemaDefType String+  | SchemaDefMaybe SchemaDef+  | SchemaDefTry SchemaDef+  | SchemaDefList SchemaDef+  | SchemaDefInclude String+  | SchemaDefObj (NonEmpty SchemaDefObjItem)+  | SchemaDefUnion (NonEmpty SchemaDef)+  deriving (Show) -parseSchemaDef :: Parser SchemaDef-parseSchemaDef = parseSchemaDefWithUnions+data SchemaDefObjItem+  = SchemaDefObjPair (SchemaDefObjKey, SchemaDef)+  | SchemaDefObjExtend String+  deriving (Show)++data SchemaDefObjKey+  = SchemaDefObjKeyNormal String+  | SchemaDefObjKeyPhantom String+  deriving (Show)++parseSchemaDef :: MonadFail m => String -> m SchemaDef+parseSchemaDef = runParserFail $ do+  space+  def <- parseSchemaDefWithUnions+  space+  void eof+  return def   where     parseSchemaDefWithUnions =-      let parseSchemaUnion [] = error "Parsed no schema definitions" -- should not happen; sepBy1 returns one or more-          parseSchemaUnion [schemaDef'] = schemaDef'-          parseSchemaUnion schemaDefs = SchemaDefUnion schemaDefs+      let parseSchemaUnion schemaDefs+            | length schemaDefs == 1 = NonEmpty.head schemaDefs+            | otherwise = SchemaDefUnion schemaDefs       in fmap parseSchemaUnion $ parseSchemaDefWithoutUnions `sepBy1` lexeme "|"      parseSchemaDefWithoutUnions = choice@@ -80,20 +90,97 @@     parseSchemaDefPair = do       key <- choice         [ SchemaDefObjKeyNormal <$> jsonKey-        , SchemaDefObjKeyPhantom <$> between (lexeme "[") (lexeme "]") jsonKey+        , SchemaDefObjKeyPhantom <$> between (lexeme' "[") (lexeme' "]") jsonKey'         ]       lexeme ":"       value <- parseSchemaDefWithUnions       return (key, value)     parseSchemaReference = char '#' *> namespacedIdentifier upperChar +{- GetterExp -}++data GetterExp = GetterExp+  { start     :: Maybe String+  , getterOps :: GetterOps+  } deriving (Show)++parseGetterExp :: MonadFail m => String -> m GetterExp+parseGetterExp = runParserFail $ do+  space+  start <- optional $ namespacedIdentifier lowerChar+  getterOps <- parseGetterOps+  space+  void eof+  return GetterExp{..}++{- UnwrapSchema -}++data UnwrapSchema = UnwrapSchema+  { startSchema :: String+  , getterOps   :: GetterOps+  } deriving (Show)++parseUnwrapSchema :: MonadFail m => String -> m UnwrapSchema+parseUnwrapSchema = runParserFail $ do+  space+  startSchema <- namespacedIdentifier upperChar+  getterOps <- parseGetterOps+  space+  void eof+  return UnwrapSchema{..}++{- GetterOps -}++-- | A non-empty list of GetterOperations.+--+-- Invariant: Any GetterList/GetterTuple operations MUST be last.+type GetterOps = NonEmpty GetterOperation++parseGetterOps :: Parser GetterOps+parseGetterOps = someWith [parseGetterOp, parseGetterOpSuffix]++data GetterOperation+  = GetterKey String+  | GetterBang+  | GetterMapList+  | GetterMapMaybe+  | GetterBranch Int+  -- suffixes+  | GetterList (NonEmpty GetterOps)+  | GetterTuple (NonEmpty GetterOps)+  deriving (Show)++parseGetterOp :: Parser GetterOperation+parseGetterOp = choice+  [ lexeme "!" $> GetterBang+  , lexeme "[]" $> GetterMapList+  , lexeme "?" $> GetterMapMaybe+  , lexeme "@" *> (GetterBranch . read . NonEmpty.toList <$> some digitChar)+  , optional (lexeme ".") *> (GetterKey <$> jsonKey)+  ]++parseGetterOpSuffix :: Parser GetterOperation+parseGetterOpSuffix = optional (lexeme ".") *> choice+  [ fmap GetterList $ between (lexeme "[") (lexeme "]") $ parseGetterOps `sepBy1` lexeme ","+  , fmap GetterTuple $ between (lexeme "(") (lexeme ")") $ parseGetterOps `sepBy1` lexeme ","+  ]++{- Parser primitives -}+ -- | A Haskell identifier, with the given first character. identifier :: Parser Char -> Parser String identifier start = (:) <$> start <*> many (alphaNumChar <|> char '\'')  lexeme :: String -> Parser ()-lexeme = void . L.lexeme (L.space space1 (L.skipLineComment "//") empty) . string+lexeme = lexemeUsingLineComment $ L.skipLineComment "//" +-- | Same as 'lexeme', but without parsing comments.+lexeme' :: String -> Parser ()+lexeme' = lexemeUsingLineComment empty++lexemeUsingLineComment :: Parser () -> String -> Parser ()+lexemeUsingLineComment lineComment = void . L.lexeme (L.space space1 lineComment empty) . string+ -- | Parses `identifier`, but if parentheses are provided, parses a namespaced identifier. namespacedIdentifier :: Parser Char -> Parser String namespacedIdentifier start = choice [lexeme "(" *> namespaced <* lexeme ")", ident]@@ -105,73 +192,52 @@       , (:[]) <$> end       ] --- | A string that can be used as a JSON key.+-- | An optionally quoted JSON key. jsonKey :: Parser String-jsonKey = some $ noneOf $ " " ++ schemaChars ++ getChars+jsonKey = choice [char '"' *> jsonKey' <* char '"', jsonKey']++-- | A string that can be used as a JSON key.+jsonKey' :: Parser String+jsonKey' = fmap NonEmpty.toList $ some $ choice+  [ try $ char '\\' *> anySingle'+  , noneOf $ [' ', '\\', '"'] ++ schemaChars ++ getChars+  ]   where+#if MIN_VERSION_megaparsec(7,0,0)+    anySingle' = anySingle+#else+    anySingle' = anyChar+#endif+     -- characters that cause ambiguity when parsing 'get' expressions     getChars = "!?[](),.@"     -- characters that should not indicate the start of a key when parsing 'schema' definitions     schemaChars = ":{}#" -{- SchemaDef -}--data SchemaDef-  = SchemaDefType String-  | SchemaDefMaybe SchemaDef-  | SchemaDefTry SchemaDef-  | SchemaDefList SchemaDef-  | SchemaDefInclude String-  | SchemaDefObj [SchemaDefObjItem]-  | SchemaDefUnion [SchemaDef]-  deriving (Show)--data SchemaDefObjItem-  = SchemaDefObjPair (SchemaDefObjKey, SchemaDef)-  | SchemaDefObjExtend String-  deriving (Show)--data SchemaDefObjKey-  = SchemaDefObjKeyNormal String-  | SchemaDefObjKeyPhantom String-  deriving (Show)--schemaDef :: Parser SchemaDef-schemaDef = do-  space-  def <- parseSchemaDef-  space-  void eof-  return def--{- GetterExp -}--data GetterExp = GetterExp-  { start     :: Maybe String-  , getterOps :: GetterOps-  } deriving (Show)+{- Parsing utilities -} -getterExp :: Parser GetterExp-getterExp = do-  space-  start <- optional $ namespacedIdentifier lowerChar-  getterOps <- some parseGetterOp-  space-  void eof-  return GetterExp{..}+-- | Same as 'Megaparsec.some', except returns a 'NonEmpty'+some :: MonadPlus f => f a -> f (NonEmpty a)+some p = NonEmpty.fromList <$> Megaparsec.some p -{- UnwrapSchema -}+-- | Same as 'Megaparsec.sepBy1', except returns a 'NonEmpty'+sepBy1 :: MonadPlus f => f a -> f sep -> f (NonEmpty a)+sepBy1 p sep = NonEmpty.fromList <$> Megaparsec.sepBy1 p sep -data UnwrapSchema = UnwrapSchema-  { startSchema :: String-  , getterOps   :: GetterOps-  } deriving (Show)+-- | Same as 'Megaparsec.sepEndBy1', except returns a 'NonEmpty'+sepEndBy1 :: MonadPlus f => f a -> f sep -> f (NonEmpty a)+sepEndBy1 p sep = NonEmpty.fromList <$> Megaparsec.sepEndBy1 p sep -unwrapSchema :: Parser UnwrapSchema-unwrapSchema = do-  space-  startSchema <- namespacedIdentifier upperChar-  getterOps <- some parseGetterOp-  space-  void eof-  return UnwrapSchema{..}+-- | Return a non-empty list containing elements from the given parsers in order.+--+-- i.e. for `someWith [p1, p2, p3]`, elements parsed with `p1` will come before+-- elements parsed with `p2` and `p3`, etc.+--+-- An individual parser in the list may not parse anything, but at least one parser must return+-- something.+someWith :: MonadParsec e s m => [m a] -> m (NonEmpty a)+someWith ps = do+  as <- concatMapM (many . try) ps+  maybe empty return $ NonEmpty.nonEmpty as+  where+    concatMapM f = fmap concat . mapM f
src/Data/Aeson/Schema/TH/Schema.hs view
@@ -7,28 +7,39 @@ The 'schema' quasiquoter. -} {-# LANGUAGE DataKinds #-}-{-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE PolyKinds #-}-{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE TypeOperators #-}  module Data.Aeson.Schema.TH.Schema (schema) where -import Control.Monad (unless, (<=<), (>=>))-import Data.Bifunctor (second)+import Control.Monad (unless, (>=>))+import Data.Function (on)+import Data.Hashable (Hashable) import qualified Data.HashMap.Strict as HashMap-import Data.Maybe (mapMaybe)+import Data.List (nubBy)+import Data.List.NonEmpty (NonEmpty)+import qualified Data.List.NonEmpty as NonEmpty import Language.Haskell.TH import Language.Haskell.TH.Quote (QuasiQuoter(..)) -import Data.Aeson.Schema.Internal (SchemaType(..))-import Data.Aeson.Schema.Key (SchemaKey(..), fromSchemaKey)-import qualified Data.Aeson.Schema.Show as SchemaShow+import Data.Aeson.Schema.Key (SchemaKey'(..), SchemaKeyV, fromSchemaKeyV) import Data.Aeson.Schema.TH.Parse-import Data.Aeson.Schema.TH.Utils-    (parseSchemaType, schemaPairsToTypeQ, typeQListToTypeQ, typeToSchemaPairs)+    (SchemaDef(..), SchemaDefObjItem(..), SchemaDefObjKey(..), parseSchemaDef)+import Data.Aeson.Schema.TH.Utils (reifySchema, schemaVToTypeQ)+import Data.Aeson.Schema.Type+    ( NameLike(..)+    , Schema'(..)+    , SchemaObjectMapV+    , SchemaType'(..)+    , SchemaTypeV+    , fromSchemaV+    , showSchemaTypeV+    , toSchemaObjectV+    )+import Data.Aeson.Schema.Utils.Invariant (unreachable)  -- | Defines a QuasiQuoter for writing schemas. --@@ -64,8 +75,10 @@ -- * @Maybe \<schema\>@ and @List \<schema\>@ correspond to @Maybe@ and @[]@, containing values --   specified by the provided schema (no parentheses needed). ----- * @Try \<schema\>@ correspond to @Maybe@, where the value will be @Just@ if the given schema---   successfully parses the value, or @Nothing@ otherwise.+-- * @Try \<schema\>@ corresponds to @Maybe@, where the value will be @Just@ if the given schema+--   successfully parses the value, or @Nothing@ otherwise. Different from @Maybe \<schema\>@,+--   where parsing @{ "foo": true }@ with @{ foo: Try Int }@ returns @Nothing@, whereas it would+--   be a parse error with @{ foo: Maybe Int }@ (added in v1.2.0) -- -- * Any other uppercase identifier corresponds to the respective type in scope -- requires a --   FromJSON instance.@@ -77,106 +90,119 @@ --   'Data.Aeson.Schema.Utils.Sum.JSONSum' object. (added in v1.1.0) -- -- * @{ [key]: \<schema\> }@ uses the current object to resolve the keys in the given schema. Only---   object schemas are allowed here.+--   object schemas are allowed here. (added in v1.2.0) ----- * @{ key: #Other, ... }@ maps the given key to the @Other@ schema.+-- * @{ key: #Other, ... }@ maps the given key to the @Other@ schema. The @Other@ schema needs to+--   be defined in another module. ----- * @{ #Other, ... }@ extends this schema with the @Other@ schema.+-- * @{ #Other, ... }@ extends this schema with the @Other@ schema. The @Other@ schema needs to+--   be defined in another module. schema :: QuasiQuoter schema = QuasiQuoter   { quoteExp = error "Cannot use `schema` for Exp"   , quoteDec = error "Cannot use `schema` for Dec"-  , quoteType = parse schemaDef >=> \case+  , quoteType = parseSchemaDef >=> \case       SchemaDefObj items -> generateSchemaObject items       _ -> fail "`schema` definition must be an object"   , quotePat = error "Cannot use `schema` for Pat"   }--generateSchemaObject :: [SchemaDefObjItem] -> TypeQ-generateSchemaObject items = [t| 'SchemaObject $(fromItems items) |]   where-    fromItems = schemaPairsToTypeQ <=< resolveParts . concat <=< mapM toParts--generateSchema :: SchemaDef -> TypeQ-generateSchema = \case-  SchemaDefType "Bool"   -> [t| 'SchemaBool |]-  SchemaDefType "Int"    -> [t| 'SchemaInt |]-  SchemaDefType "Double" -> [t| 'SchemaDouble |]-  SchemaDefType "Text"   -> [t| 'SchemaText |]-  SchemaDefType other    -> [t| 'SchemaCustom $(getType other) |]-  SchemaDefMaybe inner   -> [t| 'SchemaMaybe $(generateSchema inner) |]-  SchemaDefTry inner     -> [t| 'SchemaTry $(generateSchema inner) |]-  SchemaDefList inner    -> [t| 'SchemaList $(generateSchema inner) |]-  SchemaDefInclude other -> getType other-  SchemaDefObj items     -> generateSchemaObject items-  SchemaDefUnion schemas -> [t| 'SchemaUnion $(typeQListToTypeQ $ map generateSchema schemas) |]+    generateSchemaObject items = schemaVToTypeQ . Schema =<< generateSchemaObjectV items -{- Helpers -}+data KeySource = Provided | Imported+  deriving (Show, Eq) -getName :: String -> Q Name-getName ty = maybe (fail $ "Unknown type: " ++ ty) return =<< lookupTypeName ty+generateSchemaObjectV :: NonEmpty SchemaDefObjItem -> Q SchemaObjectMapV+generateSchemaObjectV schemaDefObjItems = do+  schemaObjectMapsWithSource <- mapM getSchemaObjectMap schemaDefObjItems -getType :: String -> TypeQ-getType = getName >=> conT+  let schemaObjectMaps :: LookupMap SchemaKeyV (KeySource, SchemaTypeV)+      schemaObjectMaps = concatMap (uncurry distribute) schemaObjectMapsWithSource -data KeySource = Provided | Imported-  deriving (Show,Eq)+  either fail return $ resolveKeys schemaObjectMaps --- | Parse SchemaDefObjItem into a list of tuples, each containing a key to add to the schema,--- the value for the key, and the source of the key.-toParts :: SchemaDefObjItem -> Q [(SchemaKey, TypeQ, KeySource)]-toParts = \case+-- | Get the SchemaObjectMapV for the given SchemaDefObjItem, along with where the SchemaObjectMapV+-- came from.+getSchemaObjectMap :: SchemaDefObjItem -> Q (SchemaObjectMapV, KeySource)+getSchemaObjectMap = \case   SchemaDefObjPair (schemaDefKey, schemaDefType) -> do-    let schemaKey = schemaDefToSchemaKey schemaDefKey-    schemaType <- generateSchema schemaDefType+    let schemaKey = fromSchemaDefKey schemaDefKey+    schemaType <- fromSchemaDefType schemaDefType      case schemaKey of-      PhantomKey _ -> do-        let schemaTypeShow = parseSchemaType schemaType-        unless (isValidPhantomSchema schemaTypeShow) $-          fail $ "Invalid schema for '" ++ fromSchemaKey schemaKey ++ "': " ++ SchemaShow.showSchemaType schemaTypeShow+      PhantomKey _ ->+        unless (isValidPhantomSchema schemaType) $+          fail $ "Invalid schema for '" ++ fromSchemaKeyV schemaKey ++ "': " ++ showSchemaTypeV schemaType       _ -> return () -    pure . tagAs Provided $ [(schemaKey, pure schemaType)]+    return ([(schemaKey, schemaType)], Provided)+   SchemaDefObjExtend other -> do-    name <- getName other-    reify name >>= \case-      TyConI (TySynD _ _ (AppT (PromotedT ty) inner)) | ty == 'SchemaObject ->-        pure . tagAs Imported . map (second pure) $ typeToSchemaPairs inner-      _ -> fail $ "'" ++ show name ++ "' is not a SchemaObject"+    schemaV <- reifySchema other+    return (fromSchemaV schemaV, Imported)   where-    tagAs source = map $ \(k,v) -> (k,v,source)-    schemaDefToSchemaKey = \case-      SchemaDefObjKeyNormal key -> NormalKey key-      SchemaDefObjKeyPhantom key -> PhantomKey key+    -- should return true if it's at all possible to get a valid parse     isValidPhantomSchema = \case-      SchemaShow.SchemaTry _ -> True-      SchemaShow.SchemaObject _ -> True-      SchemaShow.SchemaUnion schemas -> all isValidPhantomSchema schemas+      SchemaMaybe inner -> isValidPhantomSchema inner+      SchemaTry _ -> True -- even if inner is a non-object schema, it'll still parse to be Nothing+      SchemaUnion schemas -> any isValidPhantomSchema schemas+      SchemaObject _ -> True       _ -> False --- | Resolve the parts returned by 'toParts' as such:+-- | Resolve the given keys with the following rules: -- -- 1. Any explicitly provided keys shadow/overwrite imported keys -- 2. Fail if duplicate keys are both explicitly provided -- 3. Fail if duplicate keys are both imported-resolveParts :: [(SchemaKey, TypeQ, KeySource)] -> Q [(SchemaKey, TypeQ)]-resolveParts parts = do-  resolved <- resolveParts' $ HashMap.fromListWith (++) $ map nameAndSource parts-  return $ mapMaybe (alignWithResolved resolved) parts+resolveKeys :: Show a => LookupMap SchemaKeyV (KeySource, a) -> Either String (LookupMap SchemaKeyV a)+resolveKeys = mapM (uncurry resolveKey) . groupByKeyWith fromSchemaKeyV   where-    nameAndSource (name, _, source) = (fromSchemaKey name, [source])-    resolveParts' = HashMap.traverseWithKey $ \name sources -> do-      -- invariant: length sources > 0-      let numOf source = length $ filter (== source) sources-      case (numOf Provided, numOf Imported) of-        (1, _) -> return Provided-        (0, 1) -> return Imported-        (x, _) | x > 1 -> fail $ "Key '" ++ name ++ "' specified multiple times"-        (_, x) | x > 1 -> fail $ "Key '" ++ name ++ "' declared in multiple imported schemas"-        _ -> fail "Broken invariant in resolveParts"-    alignWithResolved resolved (key, ty, source) =-      let resolvedSource = resolved HashMap.! fromSchemaKey key-      in if resolvedSource == source-        then Just (key, ty)-        else Nothing+    resolveKey key sourcesAndVals =+      let filterSource source = map snd $ filter ((== source) . fst) sourcesAndVals+          numProvided = length $ filterSource Provided+          numImported = length $ filterSource Imported+      in if+        | numProvided > 1 -> Left $ "Key '" ++ fromSchemaKeyV key ++ "' specified multiple times"+        | [val] <- filterSource Provided -> Right (key, val)+        | numImported > 1 -> Left $ "Key '" ++ fromSchemaKeyV key ++ "' declared in multiple imported schemas"+        | [val] <- filterSource Imported -> Right (key, val)+        | otherwise -> unreachable $ "resolveKey received: " ++ show (key, sourcesAndVals)++{- SchemaDef conversions -}++fromSchemaDefKey :: SchemaDefObjKey -> SchemaKeyV+fromSchemaDefKey = \case+  SchemaDefObjKeyNormal key -> NormalKey key+  SchemaDefObjKeyPhantom key -> PhantomKey key++fromSchemaDefType :: SchemaDef -> Q SchemaTypeV+fromSchemaDefType = \case+  SchemaDefType name     -> return $ SchemaScalar $ NameRef name+  SchemaDefMaybe inner   -> SchemaMaybe <$> fromSchemaDefType inner+  SchemaDefTry inner     -> SchemaTry <$> fromSchemaDefType inner+  SchemaDefList inner    -> SchemaList <$> fromSchemaDefType inner+  SchemaDefInclude other -> toSchemaObjectV <$> reifySchema other+  SchemaDefUnion schemas -> SchemaUnion . NonEmpty.toList <$> mapM fromSchemaDefType schemas+  SchemaDefObj items     -> SchemaObject <$> generateSchemaObjectV items++{- LookupMap utilities -}++type LookupMap k v = [(k, v)]++-- | Distribute the given element across the values in the map.+distribute :: LookupMap k v -> a -> LookupMap k (a, v)+distribute lookupMap a = map (fmap (a,)) lookupMap++-- | Find all values with the same key (according to the given function) and group them.+--+-- Invariants:+-- * [v] has length > 0+-- * If the first occurence of k1 is before the first occurence of k2, k1 is before k2+--   in the result+groupByKeyWith :: (Eq a, Hashable a) => (k -> a) -> LookupMap k v -> LookupMap k [v]+groupByKeyWith f pairs = map (\key -> (key, groups HashMap.! f key)) distinctKeys+  where+    -- don't use sort; keys should stay in the same order+    distinctKeys = nubBy ((==) `on` f) $ map fst pairs++    groups = HashMap.fromListWith (flip (++)) $ map (\(k, v) -> (f k, [v])) pairs
src/Data/Aeson/Schema/TH/Unwrap.hs view
@@ -6,19 +6,37 @@  The 'unwrap' quasiquoter. -}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}  module Data.Aeson.Schema.TH.Unwrap where  import Control.Monad ((>=>))+import Data.Bifunctor (first)+import qualified Data.List.NonEmpty as NonEmpty import Language.Haskell.TH import Language.Haskell.TH.Quote (QuasiQuoter(..)) -import Data.Aeson.Schema.TH.Parse (UnwrapSchema(..), parse, unwrapSchema)-import Data.Aeson.Schema.TH.Utils (reifySchema, unwrapType)+import Data.Aeson.Schema.Internal (Object, SchemaResult)+import Data.Aeson.Schema.Key (fromSchemaKeyV)+import Data.Aeson.Schema.TH.Parse+    (GetterOperation(..), GetterOps, UnwrapSchema(..), parseUnwrapSchema)+import Data.Aeson.Schema.TH.Utils+    (reifySchema, schemaTypeVToTypeQ, schemaVToTypeQ)+import Data.Aeson.Schema.Type+    ( Schema'(..)+    , SchemaType'(..)+    , SchemaTypeV+    , SchemaV+    , showSchemaTypeV+    , toSchemaObjectV+    )  -- | Defines a QuasiQuoter to extract a schema within the given schema. --+-- The base schema needs to be defined in a separate module.+-- -- For example: -- -- > -- | MyFoo ~ Object [schema| { b: Maybe Bool } |]@@ -50,14 +68,100 @@ unwrap = QuasiQuoter   { quoteExp = error "Cannot use `unwrap` for Exp"   , quoteDec = error "Cannot use `unwrap` for Dec"-  , quoteType = parse unwrapSchema >=> generateUnwrapSchema+  , quoteType = parseUnwrapSchema >=> generateUnwrapSchema   , quotePat = error "Cannot use `unwrap` for Pat"   }  generateUnwrapSchema :: UnwrapSchema -> TypeQ-generateUnwrapSchema UnwrapSchema{..} = do-  startSchemaName <- maybe unknownSchema return =<< lookupTypeName startSchema-  startSchemaType <- reifySchema startSchemaName-  unwrapType False getterOps startSchemaType+generateUnwrapSchema UnwrapSchema{..} = reifySchema startSchema >>= unwrapSchema getterOps++-- | Unwrap the given schema by applying the given operations, stripping out functors.+unwrapSchema :: GetterOps -> SchemaV -> TypeQ+unwrapSchema = unwrapSchemaUsing StripFunctors++-- | Unwrap the given schema by applying the given operations, using the given 'FunctorHandler'.+unwrapSchemaUsing :: FunctorHandler -> GetterOps -> SchemaV -> TypeQ+unwrapSchemaUsing functorHandler getterOps = either fail toResultTypeQ . flip go (NonEmpty.toList getterOps) . toSchemaObjectV   where-    unknownSchema = fail $ "Unknown schema: " ++ startSchema+    toResultTypeQ :: UnwrapSchemaResult -> TypeQ+    toResultTypeQ = \case+      -- special case SchemaObject to make it further inspectable+      SchemaResult (SchemaObject pairs) -> [t| Object $(schemaVToTypeQ (Schema pairs)) |]+      SchemaResult schemaType -> [t| SchemaResult $(schemaTypeVToTypeQ schemaType) |]+      SchemaResultList schemaResult -> appT listT (toResultTypeQ schemaResult)+      SchemaResultTuple schemaResults -> foldl appT (tupleT $ length schemaResults) $ map toResultTypeQ schemaResults+      SchemaResultWrapped functorTy schemaResult ->+        let handleFunctor ty =+              case functorHandler of+                ApplyFunctors -> AppT functorTy ty+                StripFunctors -> ty+        in handleFunctor <$> toResultTypeQ schemaResult++    go :: SchemaTypeV -> [GetterOperation] -> Either String UnwrapSchemaResult+    go schemaType [] = pure $ SchemaResult schemaType+    go schemaType (op:ops) =+      let invalid message = Left $ message ++ ": " ++ showSchemaTypeV schemaType+          wrapMaybe = SchemaResultWrapped (ConT ''Maybe)+          wrapList = SchemaResultWrapped ListT+      in case op of+        GetterKey key ->+          case schemaType of+            SchemaObject pairs ->+              case lookup key $ map (first fromSchemaKeyV) pairs of+                Just inner -> go inner ops+                Nothing -> invalid $ "Key '" ++ key ++ "' does not exist in schema"+            _ -> invalid $ "Cannot get key '" ++ key ++ "' in schema"++        GetterBang ->+          case schemaType of+            SchemaMaybe inner -> go inner ops+            SchemaTry inner -> go inner ops+            _ -> invalid "Cannot use `!` operator on schema"++        GetterMapMaybe ->+          case schemaType of+            SchemaMaybe inner -> wrapMaybe <$> go inner ops+            SchemaTry inner -> wrapMaybe <$> go inner ops+            _ -> invalid "Cannot use `?` operator on schema"++        GetterMapList ->+          case schemaType of+            SchemaList inner -> wrapList <$> go inner ops+            _ -> invalid "Cannot use `[]` operator on schema"++        GetterBranch branch ->+          case schemaType of+            SchemaUnion schemas ->+              if branch < length schemas+                then go (schemas !! branch) ops+                else invalid "Branch out of bounds for schema"+            _ -> invalid "Cannot use `@` operator on schema"++        -- suffixes; ops should be empty++        GetterList elemOps ->+          case schemaType of+            SchemaObject _ -> do+              elemSchemas <- traverse (go schemaType . NonEmpty.toList) elemOps+              let elemSchema = NonEmpty.head elemSchemas+              if all (== elemSchema) elemSchemas+                then pure $ SchemaResultList elemSchema+                else invalid "List contains different types in schema"+            _ -> invalid "Cannot get keys in schema"++        GetterTuple elemOps ->+          case schemaType of+            SchemaObject _ -> SchemaResultTuple <$> mapM (go schemaType . NonEmpty.toList) (NonEmpty.toList elemOps)+            _ -> invalid "Cannot get keys in schema"++data UnwrapSchemaResult+  = SchemaResult SchemaTypeV+  | SchemaResultList UnwrapSchemaResult+  | SchemaResultTuple [UnwrapSchemaResult]+  | SchemaResultWrapped Type UnwrapSchemaResult -- ^ Type should be of kind `* -> *`+  deriving (Eq)++-- | A data type that indicates how to handle functors when unwrapping a schema.+data FunctorHandler+  = ApplyFunctors -- ^ handleFunctor Maybe Int ==> Maybe Int+  | StripFunctors -- ^ handleFunctor Maybe Int ==> Int
src/Data/Aeson/Schema/TH/Utils.hs view
@@ -4,191 +4,219 @@ Stability   :  experimental Portability :  portable -}+{-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-}-{-# LANGUAGE DeriveLift #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeOperators #-}+{-# LANGUAGE ViewPatterns #-} -module Data.Aeson.Schema.TH.Utils where+module Data.Aeson.Schema.TH.Utils+  ( reifySchema+  , reifySchemaName+  , schemaVToTypeQ+  , schemaTypeVToTypeQ+  ) where -import Control.Monad ((>=>))-import Data.Bifunctor (bimap, first, second)-import Data.List (intercalate)+import Control.Monad (forM, (>=>))+import Data.Bifunctor (bimap) import Data.Text (Text)-import GHC.Stack (HasCallStack) import Language.Haskell.TH-import Language.Haskell.TH.Syntax (Lift) -import Data.Aeson.Schema.Internal (Object, SchemaResult, SchemaType(..))-import qualified Data.Aeson.Schema.Internal as Internal-import Data.Aeson.Schema.Key (SchemaKey(..), fromSchemaKey)-import qualified Data.Aeson.Schema.Show as SchemaShow+import Data.Aeson.Schema.Internal (Object)+import Data.Aeson.Schema.Key (SchemaKey'(..), SchemaKeyV)+import Data.Aeson.Schema.Type+    ( NameLike(..)+    , Schema'(..)+    , SchemaObjectMapV+    , SchemaType'(..)+    , SchemaTypeV+    , SchemaV+    , fromSchemaV+    ) --- | Show the given schema as a type.-showSchemaType :: HasCallStack => Type -> String-showSchemaType = SchemaShow.showSchemaType . parseSchemaType+reifySchema :: String -> Q SchemaV+reifySchema name = lookupTypeName name >>= maybe unknownSchemaErr reifySchemaName+  where+    unknownSchemaErr = fail $ "Unknown schema: " ++ name -parseSchemaType :: HasCallStack => Type -> SchemaShow.SchemaType-parseSchemaType = \case-  PromotedT name-    | name == 'SchemaBool -> SchemaShow.SchemaBool-    | name == 'SchemaInt -> SchemaShow.SchemaInt-    | name == 'SchemaDouble -> SchemaShow.SchemaDouble-    | name == 'SchemaText -> SchemaShow.SchemaText-  AppT (PromotedT name) (ConT inner)-    | name == 'SchemaCustom -> SchemaShow.SchemaCustom $ nameBase inner-  AppT (PromotedT name) inner-    | name == 'SchemaMaybe -> SchemaShow.SchemaMaybe $ parseSchemaType inner-    | name == 'SchemaTry -> SchemaShow.SchemaTry $ parseSchemaType inner-    | name == 'SchemaList -> SchemaShow.SchemaList $ parseSchemaType inner-    | name == 'SchemaObject -> SchemaShow.SchemaObject $ fromPairs inner-    | name == 'SchemaUnion -> SchemaShow.SchemaUnion $ map parseSchemaType $ typeToList inner-  ty -> error $ "Unknown type: " ++ show ty+reifySchemaName :: Name -> Q SchemaV+reifySchemaName = reifySchemaType >=> parseSchema   where-    fromPairs pairs = map (second parseSchemaType) $ typeToSchemaPairs pairs+    reifySchemaType :: Name -> Q TypeWithoutKinds+    reifySchemaType schemaName = reify schemaName >>= \case+      TyConI (TySynD _ _ (stripKinds -> ty))+        -- `type MySchema = 'Schema '[ ... ]`+        | isPromotedSchema ty+        -> return ty -typeToList :: HasCallStack => Type -> [Type]-typeToList = \case-  PromotedNilT -> []-  AppT (AppT PromotedConsT x) xs -> x : typeToList xs-  SigT ty _ -> typeToList ty-  ty -> error $ "Not a type-level list: " ++ show ty+        -- `type MySchema = Object ('Schema '[ ... ])`+        | Just inner <- unwrapObject ty+        , isPromotedSchema inner+        -> return inner -typeToPair :: HasCallStack => Type -> (Type, Type)-typeToPair = \case-  AppT (AppT (PromotedTupleT 2) a) b -> (a, b)-  SigT ty _ -> typeToPair ty-  ty -> error $ "Not a type-level pair: " ++ show ty+        -- `type MySchema = Object OtherSchema`+        | Just (ConT schemaName') <- unwrapObject ty+        -> reifySchemaType schemaName' -typeToSchemaPairs :: HasCallStack => Type -> [(SchemaKey, Type)]-typeToSchemaPairs = map (bimap parseSchemaKey stripSigs . typeToPair) . typeToList+      _ -> fail $ "'" ++ show schemaName ++ "' is not a Schema" -typeQListToTypeQ :: [TypeQ] -> TypeQ-typeQListToTypeQ = foldr consT promotedNilT-  where-    -- nb. https://stackoverflow.com/a/34457936-    consT x xs = appT (appT promotedConsT x) xs+    -- If the given type is of the format `Object a`, return `a`.+    unwrapObject :: TypeWithoutKinds -> Maybe TypeWithoutKinds+    unwrapObject = \case+      AppT (ConT name) inner | name == ''Object -> Just inner+      _ -> Nothing -schemaPairsToTypeQ :: [(SchemaKey, TypeQ)] -> TypeQ-schemaPairsToTypeQ = typeQListToTypeQ . map pairT+    -- Return True if the given type is of the format: 'Schema '[ ... ]+    isPromotedSchema :: TypeWithoutKinds -> Bool+    isPromotedSchema = \case+      AppT (PromotedT name) _ | name == 'Schema -> True+      _ -> False++    parseSchema :: TypeWithoutKinds -> Q SchemaV+    parseSchema ty = maybe (fail $ "Could not parse schema: " ++ show ty) return $ do+      schemaObjectType <- case ty of+        AppT (PromotedT name) schemaType | name == 'Schema -> Just schemaType+        _ -> Nothing++      Schema <$> parseSchemaObjectType schemaObjectType++    parseSchemaObjectType :: TypeWithoutKinds -> Maybe SchemaObjectMapV+    parseSchemaObjectType schemaObjectType = do+      schemaObjectListOfPairs <- mapM typeToPair =<< typeToList schemaObjectType+      forM schemaObjectListOfPairs $ \(schemaKeyType, schemaTypeType) -> do+        schemaKey <- parseSchemaKey schemaKeyType+        schemaType <- parseSchemaType schemaTypeType+        Just (schemaKey, schemaType)++    parseSchemaKey :: TypeWithoutKinds -> Maybe SchemaKeyV+    parseSchemaKey = \case+      AppT (PromotedT ty) (LitT (StrTyLit key))+        | ty == 'NormalKey -> Just $ NormalKey key+        | ty == 'PhantomKey -> Just $ PhantomKey key+      _ -> Nothing++    parseSchemaType :: TypeWithoutKinds -> Maybe SchemaTypeV+    parseSchemaType = \case+      AppT (PromotedT name) (ConT inner)+        | name == 'SchemaScalar -> Just $ SchemaScalar $ NameTH inner++      AppT (PromotedT name) inner+        | name == 'SchemaMaybe  -> SchemaMaybe <$> parseSchemaType inner++        | name == 'SchemaTry    -> SchemaTry <$> parseSchemaType inner++        | name == 'SchemaList   -> SchemaList <$> parseSchemaType inner++        | name == 'SchemaUnion  -> do+            schemas <- typeToList inner+            SchemaUnion <$> mapM parseSchemaType schemas++        | name == 'SchemaObject -> SchemaObject <$> parseSchemaObjectType inner++      _ -> Nothing++schemaVToTypeQ :: SchemaV -> TypeQ+schemaVToTypeQ = appT [t| 'Schema |] . schemaObjectMapVToTypeQ . fromSchemaV++schemaObjectMapVToTypeQ :: SchemaObjectMapV -> TypeQ+schemaObjectMapVToTypeQ = promotedListT . map schemaObjectPairVToTypeQ   where-    pairT (k, v) =-      let schemaKey = case k of-            NormalKey key -> [t| 'Internal.NormalKey $(litT $ strTyLit key) |]-            PhantomKey key -> [t| 'Internal.PhantomKey $(litT $ strTyLit key) |]-      in [t| '($schemaKey, $v) |]+    schemaObjectPairVToTypeQ :: (SchemaKeyV, SchemaTypeV) -> TypeQ+    schemaObjectPairVToTypeQ = promotedPairT . bimap schemaKeyVToTypeQ schemaTypeVToTypeQ -parseSchemaKey :: HasCallStack => Type -> SchemaKey-parseSchemaKey = \case-  AppT (PromotedT ty) (LitT (StrTyLit key))-    | ty == 'Internal.NormalKey -> NormalKey key-    | ty == 'Internal.PhantomKey -> PhantomKey key-  SigT ty _ -> parseSchemaKey ty-  ty -> error $ "Could not parse a schema key: " ++ show ty+    schemaKeyVToTypeQ :: SchemaKeyV -> TypeQ+    schemaKeyVToTypeQ = \case+      NormalKey key  -> [t| 'NormalKey $(litT $ strTyLit key) |]+      PhantomKey key -> [t| 'PhantomKey $(litT $ strTyLit key) |] --- | Strip all kind signatures from the given type.-stripSigs :: Type -> Type-stripSigs = \case-  ForallT tyVars ctx ty -> ForallT tyVars ctx (stripSigs ty)-  AppT ty1 ty2 -> AppT (stripSigs ty1) (stripSigs ty2)-  SigT ty _ -> stripSigs ty-  InfixT ty1 name ty2 -> InfixT (stripSigs ty1) name (stripSigs ty2)-  UInfixT ty1 name ty2 -> UInfixT (stripSigs ty1) name (stripSigs ty2)-  ParensT ty -> ParensT (stripSigs ty)-  ty -> ty+schemaTypeVToTypeQ :: SchemaTypeV -> TypeQ+schemaTypeVToTypeQ = \case+  SchemaScalar name   -> [t| 'SchemaScalar $(resolveName name >>= conT) |]+  SchemaMaybe inner   -> [t| 'SchemaMaybe $(schemaTypeVToTypeQ inner) |]+  SchemaTry inner     -> [t| 'SchemaTry $(schemaTypeVToTypeQ inner) |]+  SchemaList inner    -> [t| 'SchemaList $(schemaTypeVToTypeQ inner) |]+  SchemaUnion schemas -> [t| 'SchemaUnion $(promotedListT $ map schemaTypeVToTypeQ schemas) |]+  SchemaObject pairs  -> [t| 'SchemaObject $(schemaObjectMapVToTypeQ pairs) |] -reifySchema :: Name -> TypeQ-reifySchema = reify >=> \case-  TyConI (TySynD _ _ ty) -> pure $ stripSigs ty-  info -> fail $ "Unknown reified schema: " ++ show info+resolveName :: NameLike -> Q Name+resolveName = \case+  -- some hardcoded cases+  NameRef "Bool"   -> pure ''Bool+  NameRef "Int"    -> pure ''Int+  NameRef "Double" -> pure ''Double+  NameRef "Text"   -> pure ''Text --- | Unwrap the given type using the given getter operations.+  -- general cases+  NameRef name     -> lookupTypeName name >>= maybe (fail $ "Unknown type: " ++ name) pure+  NameTH name      -> pure name++{- TH utilities -}++-- | Same as 'Type' except without any kind signatures or applications at any depth. ----- Accepts Bool for whether to maintain functor structure (True) or strip away functor applications--- (False).-unwrapType :: Bool -> GetterOps -> Type -> TypeQ-unwrapType _ [] = fromSchemaType-  where-    fromSchemaType schema = case schema of-      AppT (PromotedT ty) inner-        | ty == 'SchemaCustom -> [t| SchemaResult $(pure schema) |]-        | ty == 'SchemaMaybe -> [t| Maybe $(fromSchemaType inner) |]-        | ty == 'SchemaTry -> [t| Maybe $(fromSchemaType inner) |]-        | ty == 'SchemaList -> [t| [$(fromSchemaType inner)] |]-        | ty == 'SchemaObject -> [t| Object $(pure schema) |]-        | ty == 'SchemaUnion -> [t| SchemaResult $(pure schema) |]-      PromotedT ty-        | ty == 'SchemaBool -> [t| Bool |]-        | ty == 'SchemaInt -> [t| Int |]-        | ty == 'SchemaDouble -> [t| Double |]-        | ty == 'SchemaText -> [t| Text |]-      AppT t1 t2 -> appT (fromSchemaType t1) (fromSchemaType t2)-      TupleT _ -> pure schema-      _ -> fail $ "Could not convert schema: " ++ showSchemaType schema-unwrapType keepFunctor (op:ops) = \case-  schema@(AppT (PromotedT ty) inner) ->-    case op of-      GetterKey key | ty == 'SchemaObject ->-        case lookup key (getObjectSchema inner) of-          Just schema' -> unwrapType' ops schema'-          Nothing -> fail $ "Key '" ++ key ++ "' does not exist in schema: " ++ showSchemaType schema-      GetterKey key -> fail $ "Cannot get key '" ++ key ++ "' in schema: " ++ showSchemaType schema-      GetterList elems | ty == 'SchemaObject -> do-        (elem':rest) <- mapM (`unwrapType'` schema) elems-        if all (== elem') rest-          then unwrapType' ops elem'-          else fail $ "List contains different types with schema: " ++ showSchemaType schema-      GetterList _ -> fail $ "Cannot get keys in schema: " ++ showSchemaType schema-      GetterTuple elems | ty == 'SchemaObject ->-        foldl appT (tupleT $ length elems) $ map (`unwrapType'` schema) elems-      GetterTuple _ -> fail $ "Cannot get keys in schema: " ++ showSchemaType schema-      GetterBang | ty == 'SchemaMaybe -> unwrapType' ops inner-      GetterBang | ty == 'SchemaTry -> unwrapType' ops inner-      GetterBang -> fail $ "Cannot use `!` operator on schema: " ++ showSchemaType schema-      GetterMapMaybe | ty == 'SchemaMaybe -> withFunctor [t| Maybe |] $ unwrapType' ops inner-      GetterMapMaybe | ty == 'SchemaTry -> withFunctor [t| Maybe |] $ unwrapType' ops inner-      GetterMapMaybe -> fail $ "Cannot use `?` operator on schema: " ++ showSchemaType schema-      GetterMapList | ty == 'SchemaList -> withFunctor (pure ListT) $ unwrapType' ops inner-      GetterMapList -> fail $ "Cannot use `[]` operator on schema: " ++ showSchemaType schema-      GetterBranch branch | ty == 'SchemaUnion ->-        let subTypes = typeToList inner-        in if branch >= length subTypes-          then fail $ "Branch out of bounds for schema: " ++ showSchemaType schema-          else unwrapType' ops $ subTypes !! branch-      GetterBranch _ -> fail $ "Cannot use `@` operator on schema: " ++ showSchemaType schema-  -- allow starting from (Object schema)-  AppT (ConT ty) inner | ty == ''Object -> unwrapType' (op:ops) inner-  schema -> fail $ unlines ["Cannot get type:", show schema, show op]-  where-    unwrapType' = unwrapType keepFunctor-    getObjectSchema = map (first getSchemaKey . typeToPair) . typeToList-    getSchemaKey = fromSchemaKey . parseSchemaKey-    withFunctor f = if keepFunctor then appT f else id+-- Provides no actual guarantees. The caller is responsible for making sure the value+-- has been run through 'stripKinds' at one point.+type TypeWithoutKinds = Type -{- GetterOps -}+-- | Recursively strip all kind signatures and applications.+stripKinds :: Type -> TypeWithoutKinds+stripKinds ty =+  case ty of+    -- cases that strip + recurse+    SigT ty1 _ -> stripKinds ty1+#if MIN_VERSION_template_haskell(2,15,0)+    AppKindT ty1 _ -> stripKinds ty1+#endif -type GetterOps = [GetterOperation]+    -- cases that recurse+    ForallT tyVars ctx ty1 -> ForallT tyVars ctx (stripKinds ty1)+#if MIN_VERSION_template_haskell(2,16,0)+    ForallVisT tyVars ty1 -> ForallVisT tyVars (stripKinds ty1)+#endif+    AppT ty1 ty2 -> AppT (stripKinds ty1) (stripKinds ty2)+    InfixT ty1 name ty2 -> InfixT (stripKinds ty1) name (stripKinds ty2)+    UInfixT ty1 name ty2 -> UInfixT (stripKinds ty1) name (stripKinds ty2)+    ParensT ty1 -> ParensT (stripKinds ty1)+#if MIN_VERSION_template_haskell(2,15,0)+    ImplicitParamT str ty1 -> ImplicitParamT str (stripKinds ty1)+#endif -data GetterOperation-  = GetterKey String-  | GetterList [GetterOps]-  | GetterTuple [GetterOps]-  | GetterBang-  | GetterMapList-  | GetterMapMaybe-  | GetterBranch Int-  deriving (Show,Lift)+    -- base cases+    VarT _           -> ty+    ConT _           -> ty+    PromotedT _      -> ty+    TupleT _         -> ty+    UnboxedTupleT _  -> ty+    UnboxedSumT _    -> ty+    ArrowT           -> ty+    EqualityT        -> ty+    ListT            -> ty+    PromotedTupleT _ -> ty+    PromotedNilT     -> ty+    PromotedConsT    -> ty+    StarT            -> ty+    ConstraintT      -> ty+    LitT _           -> ty+    WildCardT        -> ty -showGetterOps :: GetterOps -> String-showGetterOps = concatMap showGetterOp+typeToList :: TypeWithoutKinds -> Maybe [TypeWithoutKinds]+typeToList = \case+  PromotedNilT -> Just []+  AppT (AppT PromotedConsT x) xs -> (x:) <$> typeToList xs+  _ -> Nothing++typeToPair :: TypeWithoutKinds -> Maybe (TypeWithoutKinds, TypeWithoutKinds)+typeToPair = \case+  AppT (AppT (PromotedTupleT 2) a) b -> Just (a, b)+  _ -> Nothing++promotedListT :: [TypeQ] -> TypeQ+promotedListT = foldr consT promotedNilT   where-    showGetterOp = \case-      GetterKey key -> '.':key-      GetterList elems -> ".[" ++ intercalate "," (map showGetterOps elems) ++ "]"-      GetterTuple elems -> ".(" ++ intercalate "," (map showGetterOps elems) ++ ")"-      GetterBang -> "!"-      GetterMapList -> "[]"-      GetterMapMaybe -> "?"-      GetterBranch x -> '@' : show x+    -- nb. https://stackoverflow.com/a/34457936+    consT x xs = appT (appT promotedConsT x) xs++promotedPairT :: (TypeQ, TypeQ) -> TypeQ+promotedPairT (a, b) = [t| '( $a, $b ) |]
+ src/Data/Aeson/Schema/Type.hs view
@@ -0,0 +1,159 @@+{-|+Module      :  Data.Aeson.Schema.Type+Maintainer  :  Brandon Chinn <brandon@leapyear.io>+Stability   :  experimental+Portability :  portable++Defines SchemaType, the AST that defines a JSON schema.+-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DeriveLift #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeInType #-}++module Data.Aeson.Schema.Type+  ( Schema'(..)+  , SchemaType'(..)+  , SchemaV+  , SchemaTypeV+  , SchemaObjectMapV+  , NameLike(..)+  , toSchemaObjectV+  , fromSchemaV+  , showSchemaV+  , showSchemaTypeV+  , Schema+  , SchemaType+  , ToSchemaObject+  , FromSchema+  , IsSchemaType(..)+  , IsSchemaObjectMap+  ) where++import Data.Kind (Type)+import Data.List (intercalate)+import Data.Proxy (Proxy(..))+import Data.Typeable (Typeable, tyConName, typeRep, typeRepTyCon)+import GHC.TypeLits (Symbol)+import Language.Haskell.TH.Syntax (Lift, Name, nameBase)++import Data.Aeson.Schema.Key+    (IsSchemaKey(..), SchemaKey, SchemaKey', SchemaKeyV, showSchemaKeyV)+import Data.Aeson.Schema.Utils.All (All(..))++-- | The schema definition for a JSON object.+data Schema' s ty = Schema (SchemaObjectMap' s ty)+  deriving (Show, Eq, Lift)++-- | The AST defining a JSON schema.+data SchemaType' s ty+  = SchemaScalar ty+  | SchemaMaybe (SchemaType' s ty)+  | SchemaTry (SchemaType' s ty) -- ^ @since v1.2.0+  | SchemaList (SchemaType' s ty)+  | SchemaUnion [SchemaType' s ty] -- ^ @since v1.1.0+  | SchemaObject (SchemaObjectMap' s ty)+  deriving (Show, Eq, Lift)++type SchemaObjectMap' s ty = [(SchemaKey' s, SchemaType' s ty)]++data NameLike = NameRef String | NameTH Name++instance Eq NameLike where+  ty1 == ty2 = show ty1 == show ty2++instance Show NameLike where+  show (NameRef ty) = ty+  show (NameTH ty) = nameBase ty++{- Value-level schema types -}++type SchemaV = Schema' String NameLike+type SchemaTypeV = SchemaType' String NameLike+type SchemaObjectMapV = SchemaObjectMap' String NameLike++toSchemaObjectV :: SchemaV -> SchemaTypeV+toSchemaObjectV (Schema schema) = SchemaObject schema++fromSchemaV :: SchemaV -> SchemaObjectMapV+fromSchemaV (Schema schema) = schema++-- | Show the given schema, as "{ key: Schema, ... }"+showSchemaV :: SchemaV -> String+showSchemaV = showSchemaTypeV' . toSchemaObjectV++-- | Pretty show the given SchemaType.+showSchemaTypeV :: SchemaTypeV -> String+showSchemaTypeV schema = case schema of+  SchemaScalar _ -> "SchemaScalar " ++ showSchemaTypeV' schema+  SchemaMaybe inner -> "SchemaMaybe " ++ showSchemaTypeV' inner+  SchemaTry inner -> "SchemaTry " ++ showSchemaTypeV' inner+  SchemaList inner -> "SchemaList " ++ showSchemaTypeV' inner+  SchemaUnion _ -> "SchemaUnion " ++ showSchemaTypeV' schema+  SchemaObject _ -> "SchemaObject " ++ showSchemaTypeV' schema++showSchemaTypeV' :: SchemaTypeV -> String+showSchemaTypeV' = \case+  SchemaScalar ty -> show ty+  SchemaMaybe inner -> "Maybe " ++ showSchemaTypeV' inner+  SchemaTry inner -> "Try " ++ showSchemaTypeV' inner+  SchemaList inner -> "List " ++ showSchemaTypeV' inner+  SchemaUnion schemas -> "( " ++ mapJoin showSchemaTypeV' " | " schemas ++ " )"+  SchemaObject pairs -> "{ " ++ mapJoin showPair ", " pairs ++ " }"+  where+    showPair (key, inner) = showSchemaKeyV key ++ ": " ++ showSchemaTypeV' inner++    mapJoin f delim = intercalate delim . map f++{- Type-level schema types -}++-- | The kind of schemas that may be used with Object; e.g.+--+-- > data Payload (schema :: Schema) = Payload+-- >   { getPayload :: Object schema+-- >   , timestamp  :: UTCTime+-- >   }+type Schema = Schema' Symbol Type++type SchemaType = SchemaType' Symbol Type++type SchemaObjectMap = SchemaObjectMap' Symbol Type++type family ToSchemaObject (schema :: Schema) :: SchemaType where+  ToSchemaObject ('Schema schema) = 'SchemaObject schema++type family FromSchema (schema :: Schema) :: SchemaObjectMap where+  FromSchema ('Schema schema) = schema++class IsSchemaType (schemaType :: SchemaType) where+  toSchemaTypeV :: Proxy schemaType -> SchemaTypeV++instance Typeable inner => IsSchemaType ('SchemaScalar inner) where+  toSchemaTypeV _ = SchemaScalar (NameRef $ tyConName $ typeRepTyCon $ typeRep $ Proxy @inner)++instance IsSchemaType inner => IsSchemaType ('SchemaMaybe inner) where+  toSchemaTypeV _ = SchemaMaybe (toSchemaTypeV $ Proxy @inner)++instance IsSchemaType inner => IsSchemaType ('SchemaTry inner) where+  toSchemaTypeV _ = SchemaTry (toSchemaTypeV $ Proxy @inner)++instance IsSchemaType inner => IsSchemaType ('SchemaList inner) where+  toSchemaTypeV _ = SchemaList (toSchemaTypeV $ Proxy @inner)++instance All IsSchemaType schemas => IsSchemaType ('SchemaUnion schemas) where+  toSchemaTypeV _ = SchemaUnion (mapAll @IsSchemaType @schemas toSchemaTypeV)++instance IsSchemaObjectMap pairs => IsSchemaType ('SchemaObject pairs) where+  toSchemaTypeV _ = SchemaObject (mapAll @IsSchemaObjectPair @pairs toSchemaTypePairV)++type IsSchemaObjectMap (pairs :: SchemaObjectMap) = All IsSchemaObjectPair pairs++class IsSchemaObjectPair (a :: (SchemaKey, SchemaType)) where+  toSchemaTypePairV :: Proxy a -> (SchemaKeyV, SchemaTypeV)++instance (IsSchemaKey key, IsSchemaType inner) => IsSchemaObjectPair '(key, inner) where+  toSchemaTypePairV _ = (toSchemaKeyV $ Proxy @key, toSchemaTypeV $ Proxy @inner)
+ src/Data/Aeson/Schema/Utils/All.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeOperators #-}++module Data.Aeson.Schema.Utils.All+  ( All(..)+  ) where++import Data.Proxy (Proxy(..))++-- | A type family for traversing a type-level list.+class All f xs where+  mapAll :: forall a. (forall x. f x => Proxy x -> a) -> [a]+  mapAll f = foldrAll @f @xs f' []+    where+      f' :: forall x. f x => Proxy x -> [a] -> [a]+      f' proxy acc = f proxy : acc++  foldrAll :: (forall x. f x => Proxy x -> a -> a) -> a -> a++instance All f '[] where+  foldrAll _ acc = acc++instance (f x, All f xs) => All f (x ': xs) where+  foldrAll f acc = f (Proxy @x) (foldrAll @f @xs f acc)
+ src/Data/Aeson/Schema/Utils/Invariant.hs view
@@ -0,0 +1,11 @@+module Data.Aeson.Schema.Utils.Invariant+  ( unreachable+  ) where++-- | An error function to indicate that a branch is unreachable. Provides a useful error message+-- if it ends up happening, pointing users to write a bug report.+unreachable :: String -> a+unreachable msg = error $ unlines+  [ "`aeson-schemas` internal error: " ++ msg+  , "Please file a bug report at https://github.com/LeapYear/aeson-schemas/issues/"+  ]
src/Data/Aeson/Schema/Utils/Sum.hs view
@@ -8,11 +8,10 @@ specified in a type-level list. -} {-# LANGUAGE DataKinds #-}-{-# LANGUAGE ExplicitNamespaces #-}+{-# LANGUAGE EmptyCase #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-}-{-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE RankNTypes #-}@@ -29,7 +28,7 @@   ) where  import Control.Applicative ((<|>))-import Data.Aeson (FromJSON(..))+import Data.Aeson (FromJSON(..), ToJSON(..)) import Data.Kind (Constraint, Type) import Data.Proxy (Proxy(..)) import GHC.TypeLits (type (-), ErrorMessage(..), Nat, TypeError)@@ -63,21 +62,29 @@  deriving instance (Show x, Show (SumType xs)) => Show (SumType (x ': xs)) instance Show (SumType '[]) where-  show = error "impossible"+  show = \case {}  deriving instance (Eq x, Eq (SumType xs)) => Eq (SumType (x ': xs)) instance Eq (SumType '[]) where-  (==) = error "impossible"+  _ == _ = True  deriving instance (Ord x, Ord (SumType xs)) => Ord (SumType (x ': xs)) instance Ord (SumType '[]) where-  compare = error "impossible"+  compare _ _ = EQ  instance (FromJSON x, FromJSON (SumType xs)) => FromJSON (SumType (x ': xs)) where   parseJSON v = (Here <$> parseJSON v) <|> (There <$> parseJSON v)  instance FromJSON (SumType '[]) where   parseJSON _ = fail "Could not parse sum type"++instance (ToJSON x, ToJSON (SumType xs)) => ToJSON (SumType (x ': xs)) where+  toJSON = \case+    Here x -> toJSON x+    There xs -> toJSON xs++instance ToJSON (SumType '[]) where+  toJSON = \case {}  {- Extracting sum type branches -} 
− src/Data/Aeson/Schema/Utils/TypeFamilies.hs
@@ -1,23 +0,0 @@-{-|-Module      :  Data.Aeson.Schema.Utils.TypeFamilies-Maintainer  :  Brandon Chinn <brandon@leapyear.io>-Stability   :  experimental-Portability :  portable--Utilities for working with type families.--}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE KindSignatures #-}-{-# LANGUAGE PolyKinds #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE TypeOperators #-}--module Data.Aeson.Schema.Utils.TypeFamilies-  ( All-  ) where--import Data.Kind (Constraint)--type family All (f :: a -> Constraint) (xs :: [a]) :: Constraint where-  All _ '[] = ()-  All f (x ': xs) = (f x, All f xs)
− test/AllTypes.hs
@@ -1,83 +0,0 @@-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE TypeApplications #-}--module AllTypes where--import Data.Aeson (FromJSON(..), withText)-import Data.Aeson.Schema-import Data.Aeson.Schema.TH (mkEnum)-import qualified Data.Text as Text--import Util (getMockedResult)--{- Greeting enum -}--mkEnum "Greeting" ["HELLO", "GOODBYE"]--{- Coordinate scalar -}--newtype Coordinate = Coordinate (Int, Int)-  deriving (Show)--instance FromJSON Coordinate where-  parseJSON = withText "Coordinate" $ \s ->-    case map (read . Text.unpack) $ Text.splitOn "," s of-      [x, y] -> return $ Coordinate (x, y)-      _ -> fail $ "Bad Coordinate: " ++ Text.unpack s--{- AllTypes result -}--type Schema = [schema|-  {-    bool: Bool,-    int: Int,-    int2: Int,-    double: Double,-    text: Text,-    scalar: Coordinate,-    enum: Greeting,-    maybeObject: Maybe {-      text: Text,-    },-    maybeObjectNull: Maybe {-      text: Text,-    },-    tryObject: Try {-      a: Int,-    },-    tryObjectNull: Try {-      a: Int,-    },-    maybeList: Maybe List {-      text: Text,-    },-    maybeListNull: Maybe List {-      text: Text,-    },-    // this is a comment-    list: List {-      type: Text,-      maybeBool: Maybe Bool,-      maybeInt: Maybe Int,-      maybeNull: Maybe Bool,-    },-    nonexistent: Maybe Text,-    // future_key: Int,-    union: List (-        { a: Int } | List Bool | Text-    ),-    [phantom]: {-      keyForPhantom: Int,-    },-  }-|]--result :: Object Schema-result = $(getMockedResult "test/all_types.json")--{- AllTypes getters -}--mkGetter "ListItem" "getList" ''AllTypes.Schema ".list[]"
− test/Enums.hs
@@ -1,13 +0,0 @@-{-# LANGUAGE TemplateHaskell #-}--module Enums (State(..), Color(..)) where--import Data.Aeson.Schema.TH (genFromJSONEnum, genToJSONEnum, mkEnum)--mkEnum "State" ["OPEN", "CLOSED"]--data Color = Red | LightBlue | Yellow | DarkGreen | Black | JustABitOffWhite-  deriving (Show, Eq, Enum)--genFromJSONEnum ''Color-genToJSONEnum ''Color
test/Main.hs view
@@ -1,301 +1,20 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeFamilies #-}--import Data.Aeson (ToJSON, decode, eitherDecode, encode)-import Data.Aeson.Schema (Object, get, unwrap)-import Data.Aeson.Schema.Utils.Sum (SumType(..), fromSumType)-import qualified Data.ByteString.Lazy.Char8 as ByteString-import Data.Char (toLower, toUpper)-import Data.Maybe (fromMaybe)-import Data.Proxy (Proxy(..))-import qualified Data.Text as Text-import Language.Haskell.TH.TestUtils (tryQErr')-import Test.Tasty (TestTree, defaultMain, testGroup)-import Test.Tasty.Golden (goldenVsString)-import Test.Tasty.HUnit (testCase, (@?=))-import Test.Tasty.QuickCheck-    (arbitrary, elements, infiniteListOf, oneof, testProperty, (===))-import Text.RawString.QQ (r)--import qualified AllTypes-import Enums-import qualified Nested-import Schema-import SumType-import Util+import Test.Tasty (defaultMain, testGroup) -allTypes :: Object AllTypes.Schema-allTypes = AllTypes.result+import qualified Tests.EnumTH+import qualified Tests.GetQQ+import qualified Tests.MkGetter+import qualified Tests.Object+import qualified Tests.SchemaQQ+import qualified Tests.SumType+import qualified Tests.UnwrapQQ  main :: IO () main = defaultMain $ testGroup "aeson-schemas"-  [ testGetterExp-  , testFromObjectAllTypes-  , testFromObjectNested-  , testFromObjectNamespaced-  , testUnwrapSchema-  , testSchemaDef-  , testMkGetter-  , testEnumTH-  , testSumType-  ]--goldens' :: String -> String -> TestTree-goldens' name = goldenVsString name fp . pure . ByteString.pack-  where-    fp = "test/goldens/" ++ name ++ ".golden"--goldens :: Show s => String -> s -> TestTree-goldens name = goldens' name . show--testGetterExp :: TestTree-testGetterExp = testGroup "Test getter expressions"-  [ goldens "bool"                     [get| allTypes.bool                  |]-  , goldens "lambda_bool"             ([get| .bool |] allTypes)-  , goldens "int"                      [get| allTypes.int                   |]-  , goldens "int_int2"                 [get| allTypes.[int,int2]            |]-  , goldens "double"                   [get| allTypes.double                |]-  , goldens "bool_int_double"          [get| allTypes.(bool,int,double)     |]-  , goldens "text"                     [get| allTypes.text                  |]-  , goldens "scalar"                   [get| allTypes.scalar                |]-  , goldens "enum"                     [get| allTypes.enum                  |]-  , goldens "maybeObj"                 [get| allTypes.maybeObject           |]-  , goldens "maybeObj_bang"            [get| allTypes.maybeObject!          |]-  , goldens "maybeObj_text"            [get| allTypes.maybeObject?.text     |]-  , goldens "maybeObj_bang_text"       [get| allTypes.maybeObject!.text     |]-  , goldens "maybeObjNull"             [get| allTypes.maybeObjectNull       |]-  , goldens "maybeObjNull_text"        [get| allTypes.maybeObjectNull?.text |]-  , goldens "tryObj"                   [get| allTypes.tryObject             |]-  , goldens "tryObjNull"               [get| allTypes.tryObjectNull         |]-  , goldens "tryObj_bang"              [get| allTypes.tryObject!            |]-  , goldens "tryObj_a"                 [get| allTypes.tryObject?.a          |]-  , goldens "tryObj_bang_a"            [get| allTypes.tryObject!.a          |]-  , goldens "tryObjNull_a"             [get| allTypes.tryObjectNull?.a      |]-  , goldens "maybeList"                [get| allTypes.maybeList             |]-  , goldens "maybeList_bang"           [get| allTypes.maybeList!            |]-  , goldens "maybeList_bang_list"      [get| allTypes.maybeList![]          |]-  , goldens "maybeList_bang_list_text" [get| allTypes.maybeList![].text     |]-  , goldens "maybeList_list"           [get| allTypes.maybeList?[]          |]-  , goldens "maybeList_list_text"      [get| allTypes.maybeList?[].text     |]-  , goldens "maybeListNull"            [get| allTypes.maybeListNull         |]-  , goldens "maybeListNull_list"       [get| allTypes.maybeListNull?[]      |]-  , goldens "maybeListNull_list_text"  [get| allTypes.maybeListNull?[].text |]-  , goldens "list"                     [get| allTypes.list                  |]-  , goldens "list_explicit"            [get| allTypes.list[]                |]-  , goldens "list_type"                [get| allTypes.list[].type           |]-  , goldens "list_maybeBool"           [get| allTypes.list[].maybeBool      |]-  , goldens "list_maybeInt"            [get| allTypes.list[].maybeInt       |]-  , goldens "nonexistent"              [get| allTypes.nonexistent           |]-  , goldens "union"                    [get| allTypes.union                 |]-  , goldens "union_0"                  [get| allTypes.union[]@0             |]-  , goldens "union_0_a"                [get| allTypes.union[]@0?.a          |]-  , goldens "union_1"                  [get| allTypes.union[]@1             |]-  , goldens "union_2"                  [get| allTypes.union[]@2             |]-  , goldens "phantom"                  [get| allTypes.phantom.keyForPhantom |]-  -- bad 'get' expressions-  , goldens' "maybeListNull_bang" $(getError [get| (AllTypes.result).maybeListNull! |])-#if MIN_VERSION_megaparsec(7,0,0)-  , goldens' "get_empty" $(tryQErr' $ showGet "")-  , goldens' "get_just_start" $(tryQErr' $ showGet "allTypes")-#endif-  , goldens' "get_ops_after_tuple" $(tryQErr' $ showGet "allTypes.(bool,int).foo")-  , goldens' "get_ops_after_list" $(tryQErr' $ showGet "allTypes.[int,int2].foo")-  ]--testFromObjectAllTypes :: TestTree-testFromObjectAllTypes =-  goldens "from_object_all_types" $ map fromObj [get| allTypes.list |]-  where-    fromObj o = case Text.unpack [get| o.type |] of-      "bool" -> show [get| o.maybeBool! |]-      "int"  -> show [get| o.maybeInt!  |]-      "null" -> show [get| o.maybeNull  |]-      _ -> error "unreachable"--testFromObjectNested :: TestTree-testFromObjectNested = goldens "from_object_nested" $ map fromObj [get| (Nested.result).list |]-  where-    fromObj obj = case [get| obj.a |] of-      Just field -> [get| field.b |]-      Nothing    -> [get| obj.b |]--testFromObjectNamespaced :: TestTree-testFromObjectNamespaced = goldens "from_object_namespaced" $-  map fromAllTypes [get| allTypes.list |]-  ++ map fromNested [get| (Nested.result).list |]-  where-    fromAllTypes o = Text.unpack [get| o.type |]-    fromNested o = show [get| o.b |]--type NestedObject = [unwrap| (Nested.Schema).list[] |]--parseNestedObject :: NestedObject -> Int-parseNestedObject obj = fromMaybe [get| obj.b |] [get| obj.a?.b |]--nestedList :: [NestedObject]-nestedList = [get| (Nested.result).list[] |]--testUnwrapSchema :: TestTree-testUnwrapSchema = testGroup "Test unwrapping schemas"-  [ goldens' "unwrap_schema" $(showUnwrap "(Nested.Schema).list[]")-  , goldens "unwrap_schema_nested_list" nestedList-  , goldens "unwrap_schema_nested_object" $ map parseNestedObject nestedList-  -- bad unwrap types-  , goldens' "unwrap_schema_bad_bang" $(tryQErr' $ showUnwrap "(AllTypes.Schema).list[]!")-  , goldens' "unwrap_schema_bad_question" $(tryQErr' $ showUnwrap "(AllTypes.Schema).list[]?")-  , goldens' "unwrap_schema_bad_list" $(tryQErr' $ showUnwrap "(AllTypes.Schema).list[][]")-  , goldens' "unwrap_schema_bad_key" $(tryQErr' $ showUnwrap "(AllTypes.Schema).list.a")-  , goldens' "unwrap_schema_bad_branch" $(tryQErr' $ showUnwrap "(AllTypes.Schema).list@0")-  , goldens' "unwrap_schema_branch_out_of_bounds" $(tryQErr' $ showUnwrap "(AllTypes.Schema).union[]@10")-  ]--testSchemaDef :: TestTree-testSchemaDef = testGroup "Test generating schema definitions"-  [ goldens' "schema_def_bool" $(showSchema [r| { a: Bool } |])-  , goldens' "schema_def_int" $(showSchema [r| { a: Int } |])-  , goldens' "schema_def_double" $(showSchema [r| { foo123: Double } |])-  , goldens' "schema_def_text" $(showSchema [r| { some_text: Text } |])-  , goldens' "schema_def_custom" $(showSchema [r| { status: Status } |])-  , goldens' "schema_def_maybe" $(showSchema [r| { a: Maybe Int } |])-  , goldens' "schema_def_list" $(showSchema [r| { a: List Int } |])-  , goldens' "schema_def_obj" $(showSchema [r| { a: { b: Int } } |])-  , goldens' "schema_def_maybe_obj" $(showSchema [r| { a: Maybe { b: Int } } |])-  , goldens' "schema_def_list_obj" $(showSchema [r| { a: List { b: Int } } |])-  , goldens' "schema_def_import_user" $(showSchema [r| { user: #UserSchema } |])-  , goldens' "schema_def_extend" $(showSchema [r| { a: Int, #(Schema.MySchema) } |])-  , goldens' "schema_def_shadow" $(showSchema [r| { extra: Bool, #(Schema.MySchema) } |])-  , goldens' "schema_def_union" $(showSchema [r| { a: List Int | Text } |])-  , goldens' "schema_def_union_grouped" $(showSchema [r| { a: List (Int | Text) } |])-  -- bad schema definitions-  , goldens' "schema_def_duplicate" $(tryQErr' $ showSchema [r| { a: Int, a: Bool } |])-  , goldens' "schema_def_duplicate_phantom" $(tryQErr' $ showSchema [r| { a: Int, [a]: { b: Bool } } |])-  , goldens' "schema_def_duplicate_extend" $(tryQErr' $ showSchema [r| { #MySchema, #MySchema2 } |])-  , goldens' "schema_def_not_object" $(tryQErr' $ showSchema [r| List { a: Int } |])-  , goldens' "schema_def_unknown_type" $(tryQErr' $ showSchema [r| HelloWorld |])-  , goldens' "schema_def_invalid_extend" $(tryQErr' $ showSchema [r| { #Int } |])-  , goldens' "schema_def_nonobject_phantom" $(tryQErr' $ showSchema [r| { [a]: Int } |])-  ]--testMkGetter :: TestTree-testMkGetter = testGroup "Test the mkGetter helper"-  [ goldens "getter_all_types_list" list-  , goldens "getter_all_types_list_item" $ map getType list-  ]-  where-    list :: [AllTypes.ListItem]-    list = AllTypes.getList AllTypes.result--    getType :: AllTypes.ListItem -> Text.Text-    getType = [get| .type |]--testEnumTH :: TestTree-testEnumTH = testGroup "Test the Enum TH helpers"-  -- State-  [ testProperty "mkEnum decode is case insensitive" $ do-      (val, enumVal) <- elements-        [ ("OPEN", OPEN)-        , ("CLOSED", CLOSED)-        ]-      casedVal <- randomlyCased val-      return $ decode (ByteString.pack $ show casedVal) === Just enumVal-  , testCase "mkEnum encode keeps case of constructor" $ do-      encode OPEN @?= "\"OPEN\""-      encode CLOSED @?= "\"CLOSED\""-  , testProperty "mkEnum: (fromJust . decode . encode) === id" $ do-      enumVal <- elements [OPEN, CLOSED]-      return $ (decode . encode) enumVal === Just enumVal--  -- Color-  , testProperty "genFromJSONEnum decode is case insensitive" $ do-      (val, enumVal) <- elements-        [ ("Red", Red)-        , ("LightBlue", LightBlue)-        , ("Yellow", Yellow)-        , ("DarkGreen", DarkGreen)-        , ("Black", Black)-        , ("JustABitOffWhite", JustABitOffWhite)-        ]-      casedVal <- randomlyCased val-      return $ decode (ByteString.pack $ show casedVal) === Just enumVal-  , testCase "genToJSONEnum encode keeps case of constructor" $ do-      encode Red @?= "\"Red\""-      encode LightBlue @?= "\"LightBlue\""-      encode Yellow @?= "\"Yellow\""-      encode DarkGreen @?= "\"DarkGreen\""-      encode Black @?= "\"Black\""-      encode JustABitOffWhite @?= "\"JustABitOffWhite\""-  , testProperty "genFromJSONEnum + genToJSONEnum: (fromJust . decode . encode) === id" $ do-      enumVal <- elements-        [ Red-        , LightBlue-        , Yellow-        , DarkGreen-        , Black-        , JustABitOffWhite-        ]-      return $ (decode . encode) enumVal === Just enumVal-  ]-  where-    randomlyCased s = do-      caseFuncs <- infiniteListOf $ elements [toLower, toUpper]-      return $ zipWith ($) caseFuncs s--testSumType :: TestTree-testSumType = testGroup "Test the SumType helper"-  [ testCase "Sanity checks" $-      let values =-            [ Here True-            , Here False-            , There (Here 1)-            , There (Here 10)-            , There (There (Here []))-            , There (There (Here ["a"]))-            ] :: [SpecialJSON]-      in values @?= values-  , testGroup "Decode SumType"-    [ testProperty "branch 1" $ \(b :: Bool) ->-        toSpecialJSON b === Right (Here b)-    , testProperty "branch 2" $ \(x :: Int) ->-        toSpecialJSON x === Right (There (Here x))-    , testProperty "branch 3" $ \(l :: [String]) ->-        toSpecialJSON l === Right (There (There (Here l)))-    , testCase "invalid SumType" $-        toSpecialJSON [True] @?= Left "Error in $: Could not parse sum type"-    ]-  , testGroup "fromSumType"-    [ testProperty "branch 0 valid" $ \b ->-        fromSumType (Proxy @0) (Here b :: SpecialJSON) === Just b-    , testProperty "branch 0 invalid" $ do-        value <- oneof-          [ There . Here <$> arbitrary-          , There . There . Here <$> arbitrary-          ]-        return $ fromSumType (Proxy @0) (value :: SpecialJSON) === Nothing-    , testProperty "branch 1 valid" $ \x ->-        fromSumType (Proxy @1) (There (Here x) :: SpecialJSON) === Just x-    , testProperty "branch 1 invalid" $ do-        value <- oneof-          [ Here <$> arbitrary-          , There . There . Here <$> arbitrary-          ]-        return $ fromSumType (Proxy @1) (value :: SpecialJSON) === Nothing-    , testProperty "branch 2 valid" $ \l ->-        fromSumType (Proxy @2) (There (There (Here l)) :: SpecialJSON) === Just l-    , testProperty "branch 2 invalid" $ do-        value <- oneof-          [ Here <$> arbitrary-          , There . Here <$> arbitrary-          ]-        return $ fromSumType (Proxy @2) (value :: SpecialJSON) === Nothing-    ]+  [ Tests.Object.test+  , Tests.GetQQ.test+  , Tests.UnwrapQQ.test+  , Tests.SchemaQQ.test+  , Tests.MkGetter.test+  , Tests.EnumTH.test+  , Tests.SumType.test   ]-  where-    toSpecialJSON :: ToJSON a => a -> Either String SpecialJSON-    toSpecialJSON = eitherDecode . encode
− test/Nested.hs
@@ -1,23 +0,0 @@-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE TemplateHaskell #-}--module Nested where--import Data.Aeson.Schema--import Util (getMockedResult)--type Schema = [schema|-  {-    list: List {-      a: Maybe {-        b: Int,-      },-      b: Int,-    },-  }-|]--result :: Object Schema-result = $(getMockedResult "test/nested.json")
− test/Schema.hs
@@ -1,15 +0,0 @@-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE QuasiQuotes #-}--module Schema where--import Data.Aeson (FromJSON)-import Data.Aeson.Schema (schema)--newtype Status = Status Int-  deriving (Show,FromJSON)--type UserSchema = [schema| { name: Text } |]-type MySchema = [schema| { extra: Text } |]-type MySchema2 = [schema| { extra: Maybe Text } |]
− test/SumType.hs
@@ -1,7 +0,0 @@-{-# LANGUAGE DataKinds #-}--module SumType where--import Data.Aeson.Schema.Utils.Sum (SumType(..))--type SpecialJSON = SumType '[Bool, Int, [String]]
+ test/TestUtils.hs view
@@ -0,0 +1,127 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}++module TestUtils+  ( ShowSchemaResult(..)+  , json+  , parseValue+  , parseObject+  , parseProxy+  , mkExpQQ+  , testGolden+  , testGoldenIO+  , testParseError+  ) where++import Data.Aeson (FromJSON(..), Value, eitherDecode)+import Data.Aeson.Types (parseEither)+#if !MIN_VERSION_megaparsec(7,0,0)+import Data.Char (isDigit, isSpace)+#endif+import Data.Proxy (Proxy(..))+import Data.String (fromString)+import qualified Data.Text as Text+import qualified Data.Text.IO as Text+import qualified Data.Text.Lazy as TextL+import qualified Data.Text.Lazy.Encoding as TextL+import Data.Typeable (Typeable, typeRep)+import Language.Haskell.TH (ExpQ)+import Language.Haskell.TH.Quote (QuasiQuoter(..))+import Test.Tasty (TestTree)+import Test.Tasty.Golden (goldenVsString)+import Test.Tasty.Golden.Advanced (goldenTest)++import Data.Aeson.Schema (IsSchema, Object, schema)+import qualified Data.Aeson.Schema.Internal as Internal++{- ShowSchemaResult -}++class ShowSchemaResult a where+  showSchemaResult :: String++instance IsSchema schema => ShowSchemaResult (Object schema) where+  showSchemaResult = "Object (" ++ Internal.showSchema @schema ++ ")"++instance ShowSchemaResult a => ShowSchemaResult [a] where+  showSchemaResult = "[" ++ showSchemaResult @a ++ "]"++instance {-# OVERLAPPABLE #-} Typeable a => ShowSchemaResult a where+  showSchemaResult = show $ typeRep (Proxy @a)++{- Loading JSON data -}++json :: QuasiQuoter+json = mkExpQQ $ \s -> [| (either error id . eitherDecode . fromString) s |]++parseValue :: FromJSON a => Value -> a+parseValue = either error id . parseEither parseJSON++parseProxy :: FromJSON a => Proxy a -> Value -> Either String a+parseProxy _ = parseEither parseJSON++parseObject :: String -> ExpQ+parseObject schemaString = [| parseValue :: Value -> Object $schemaType |]+  where+    schemaType = quoteType schema schemaString++{- QuasiQuotation -}++mkExpQQ :: (String -> ExpQ) -> QuasiQuoter+mkExpQQ f = QuasiQuoter+  { quoteExp = f+  , quotePat = error "Cannot use this QuasiQuoter for patterns"+  , quoteType = error "Cannot use this QuasiQuoter for types"+  , quoteDec = error "Cannot use this QuasiQuoter for declarations"+  }++{- Tasty test trees -}++testGolden :: String -> FilePath -> String -> TestTree+testGolden name fp = testGoldenIO name fp . return++testGoldenIO :: String -> FilePath -> IO String -> TestTree+testGoldenIO name fp = goldenVsString name ("test/goldens/" ++ fp) . fmap toByteString+  where+    toByteString = TextL.encodeUtf8 . TextL.fromStrict . Text.pack++-- | A golden test for testing parse errors.+testParseError :: String -> FilePath -> String -> TestTree+testParseError name fp s = goldenTest name getExpected getActual cmp update+  where+    goldenFile = "test/goldens/" ++ fp+    getExpected = sanitize <$> Text.readFile goldenFile+    getActual = return $ Text.pack s+    cmp expected actual = return $ if expected == actual+      then Nothing+      else Just $ "Test output was different from '" ++ goldenFile ++ "'. It was:\n" ++ Text.unpack actual+    update = Text.writeFile goldenFile++#if MIN_VERSION_megaparsec(7,0,0)+    sanitize = id+#else+    -- megaparsec before version 7.0.0 doesn't display the offending lines, so we need to strip+    -- out those lines from the golden before comparing them.+    --+    -- e.g.+    --+    --    { "a:b": Int } :1:6:+    -- >   |+    -- > 1 |  { "a:b": Int }+    -- >   |      ^+    --   unexpected ':'+    --   expecting '"' or '\'+    sanitize = Text.unlines . filter (not . isOffendingLine) . Text.lines++    isOffendingLine line = case Text.splitOn "|" line of+      [prefix, _] -> Text.all (\c -> isDigit c || isSpace c) prefix+      _ -> False+#endif
+ test/TestUtils/Arbitrary.hs view
@@ -0,0 +1,341 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveLift #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeInType #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}+{-# OPTIONS_GHC -Wno-orphans #-}++module TestUtils.Arbitrary+  ( ArbitraryObject(..)+  , forAllArbitraryObjects+  ) where++import Control.Monad (forM)+import Data.Aeson (ToJSON(..), Value(..), encode)+import qualified Data.Aeson as Aeson+import qualified Data.HashMap.Strict as HashMap+import Data.List (nub)+import Data.Proxy (Proxy(..))+import Data.Text (Text)+import qualified Data.Text as Text+import Data.Typeable (Typeable)+import GHC.Exts (fromList)+import GHC.TypeLits (KnownSymbol)+import Language.Haskell.TH (ExpQ, listE, runIO)+import Language.Haskell.TH.Instances ()+import Language.Haskell.TH.Quote (QuasiQuoter(quoteType))+import Language.Haskell.TH.Syntax (Lift)+import Test.QuickCheck++import Data.Aeson.Schema (IsSchema, Object, schema)+import Data.Aeson.Schema.Key+    (IsSchemaKey(..), SchemaKey, SchemaKey'(..), SchemaKeyV, toContext)+import Data.Aeson.Schema.Type+    ( NameLike(..)+    , Schema'(..)+    , SchemaObjectMapV+    , SchemaType+    , SchemaType'(..)+    , SchemaV+    , showSchemaV+    , toSchemaObjectV+    )+import Data.Aeson.Schema.Utils.All (All(..))++data ArbitraryObject where+  ArbitraryObject+    :: IsSchema schema+    => Proxy (Object schema)+    -> Value+    -> SchemaV+    -> ArbitraryObject++-- Show the value and schema as something that could be copied/pasted into GHCi.+instance Show ArbitraryObject where+  show (ArbitraryObject _ v schemaV) = unlines+    [ "ArbitraryObject:"+    , "  " ++ show (encode v)+    , "  [schema| " ++ showSchemaV schemaV ++ " |]"+    ]++-- | A Template Haskell function to generate a splice for QuickCheck tests to generate arbitrary+-- objects with arbitrary schemas.+--+-- Note that for repeated runs of the test suite, the schemas will be the same, with the actual+-- JSON values generated randomly. You need to recompile in order to generate different schemas.+arbitraryObject :: ExpQ+arbitraryObject = do+  arbitrarySchemas <- runIO $ genSchemaTypes 20++  [| oneof $(listE $ map mkSchemaGen arbitrarySchemas) |]+  where+    mkSchemaGen schemaV =+      let schemaType = quoteType schema $ showSchemaV schemaV+      in [| genSchema' (Proxy :: Proxy (Object $schemaType)) schemaV |]++-- | Splices to a 'forAll' with 'arbitraryObject', outputting information about the object+-- generated, to ensure we get good generation.+--+-- $(forAllArbitraryObjects) :: Testable prop => ArbitraryObject -> prop+forAllArbitraryObjects :: ExpQ+forAllArbitraryObjects = [| forAllArbitraryObjects' $arbitraryObject |]++forAllArbitraryObjects' :: Gen ArbitraryObject -> (ArbitraryObject -> Property) -> Property+forAllArbitraryObjects' genArbitraryObject runTest =+  forAll @_ @Property genArbitraryObject $ \o@(ArbitraryObject _ _ schemaType) ->+    tabulate' "Key types" (map getKeyType $ getKeys schemaType) $+    tabulate' "Schema types" (getSchemaTypes schemaType) $+    tabulate' "Object sizes" (map show $ getObjectSizes schemaType) $+    tabulate' "Object depth" [show $ getObjectDepth schemaType] $+    runTest o++{- Run time helpers -}++deriving instance Lift NameLike++genSchema' :: forall schema.+  ( ArbitrarySchema ('SchemaObject schema)+  , IsSchema ('Schema schema)+  )+  => Proxy (Object ('Schema schema)) -> SchemaV -> Gen ArbitraryObject+genSchema' proxy schemaV = do+  v <- genSchema @('SchemaObject schema)+  return $ ArbitraryObject proxy v schemaV++getKeyType :: SchemaKeyV -> String+getKeyType = \case+  NormalKey _ -> "Normal"+  PhantomKey _ -> "Phantom"++getKeys :: SchemaV -> [SchemaKeyV]+getKeys = getKeys' . toSchemaObjectV+  where+    getKeys' = \case+      SchemaMaybe inner -> getKeys' inner+      SchemaTry inner -> getKeys' inner+      SchemaList inner -> getKeys' inner+      SchemaUnion schemas -> concatMap getKeys' schemas+      SchemaObject pairs -> concatMap (\(key, inner) -> key : getKeys' inner) pairs+      _ -> []++getSchemaTypes :: SchemaV -> [String]+getSchemaTypes = getSchemaTypes' . toSchemaObjectV+  where+    getSchemaTypes' = \case+      SchemaScalar name -> [show name]+      SchemaMaybe inner -> "SchemaMaybe" : getSchemaTypes' inner+      SchemaTry inner -> "SchemaTry" : getSchemaTypes' inner+      SchemaList inner -> "SchemaList" : getSchemaTypes' inner+      SchemaUnion schemas -> "SchemaUnion" : concatMap getSchemaTypes' schemas+      SchemaObject pairs -> "SchemaObject" : concatMap (getSchemaTypes' . snd) pairs++getObjectSizes :: SchemaV -> [Int]+getObjectSizes = getObjectSizes' . toSchemaObjectV+  where+    getObjectSizes' = \case+      SchemaScalar _ -> []+      SchemaMaybe inner -> getObjectSizes' inner+      SchemaTry inner -> getObjectSizes' inner+      SchemaList inner -> getObjectSizes' inner+      SchemaUnion schemas -> concatMap getObjectSizes' schemas+      SchemaObject pairs -> length pairs : concatMap (getObjectSizes' . snd) pairs++getObjectDepth :: SchemaV -> Int+getObjectDepth = getObjectDepth' . toSchemaObjectV+  where+    getObjectDepth' = \case+      SchemaScalar _ -> 0+      SchemaMaybe inner -> getObjectDepth' inner+      SchemaTry inner -> getObjectDepth' inner+      SchemaList inner -> getObjectDepth' inner+      SchemaUnion schemas -> maximum $ map getObjectDepth' schemas+      SchemaObject pairs -> 1 + maximum (map (getObjectDepth' . snd) pairs)++tabulate' :: String -> [String] -> Property -> Property+#if MIN_VERSION_QuickCheck(2,12,0)+tabulate' = tabulate+#else+tabulate' _ _ = id+#endif++{- Generating schemas -}++class ArbitrarySchema (schema :: SchemaType) where+  genSchema :: Gen Value++instance {-# OVERLAPS #-} ArbitrarySchema ('SchemaScalar Text) where+  genSchema = toJSON <$> arbitrary @String++instance (Arbitrary inner, ToJSON inner, Typeable inner) => ArbitrarySchema ('SchemaScalar inner) where+  genSchema = toJSON <$> arbitrary @inner++instance ArbitrarySchema inner => ArbitrarySchema ('SchemaMaybe inner) where+  genSchema = frequency+    [ (3, genSchema @inner)+    , (1, pure Null)+    ]++instance ArbitrarySchema inner => ArbitrarySchema ('SchemaTry inner) where+  genSchema = frequency+    [ (3, genSchema @inner)+    , (1, genValue)+    ]+    where+      genValue = oneof+        [ pure Null+        , Number . realToFrac <$> arbitrary @Double+        , Bool <$> arbitrary+        , String . Text.pack <$> arbitrary+        ]++instance ArbitrarySchema inner => ArbitrarySchema ('SchemaList inner) where+  genSchema = Array . fromList <$> listOf (genSchema @inner)++instance All ArbitrarySchema schemas => ArbitrarySchema ('SchemaUnion schemas) where+  genSchema = oneof $ mapAll @ArbitrarySchema @schemas genSchemaElem+    where+      genSchemaElem :: forall schema. ArbitrarySchema schema => Proxy schema -> Gen Value+      genSchemaElem _ = genSchema @schema++instance All ArbitraryObjectPair pairs => ArbitrarySchema ('SchemaObject (pairs :: [(SchemaKey, SchemaType)])) where+  genSchema = Object . HashMap.unions <$> genSchemaPairs+    where+      genSchemaPairs :: Gen [Aeson.Object]+      genSchemaPairs = sequence $ mapAll @ArbitraryObjectPair @pairs genSchemaPair++class IsSchemaKey (Fst pair) => ArbitraryObjectPair (pair :: (SchemaKey, SchemaType)) where+  genSchemaPair :: Proxy pair -> Gen Aeson.Object+  genSchemaPair _ = toContext schemaKey <$> genInnerSchema @pair+    where+      schemaKey = toSchemaKeyV $ Proxy @(Fst pair)++  genInnerSchema :: Gen Value++instance (IsSchemaKey key, ArbitrarySchema schema) => ArbitraryObjectPair '(key, schema) where+  genInnerSchema = genSchema @schema++-- For phantom keys, Maybe is only valid for Objects. Since phantom keys parse the schema with+-- the current object as the context, we should guarantee that this only generates objects, and+-- not Null.+instance {-# OVERLAPS #-} (KnownSymbol key, inner ~ 'SchemaObject a, ArbitrarySchema inner)+  => ArbitraryObjectPair '( 'PhantomKey key, 'SchemaMaybe inner ) where+  genInnerSchema = genSchema @inner++-- For phantom keys, Try can be used on any schema, but for all non-object schemas, need to ensure+-- we generate 'Null', because Try on a non-object schema will always be an invalid parse.+instance {-# OVERLAPS #-} (KnownSymbol key, ArbitrarySchema ('SchemaTry inner))+  => ArbitraryObjectPair '( 'PhantomKey key, 'SchemaTry inner ) where+  genInnerSchema = castNull <$> genSchema @('SchemaTry inner)+    where+      castNull inner =+        case inner of+          Object _ -> inner+          _ -> Null++-- For phantom keys, Union can be used on any schemas, as long as at least one is an object schema.+instance {-# OVERLAPS #-} (KnownSymbol key, FilterObjectSchemas schemas ~ objectSchemas, ArbitrarySchema ('SchemaUnion objectSchemas))+  => ArbitraryObjectPair '( 'PhantomKey key, 'SchemaUnion schemas ) where+  genInnerSchema = genSchema @('SchemaUnion objectSchemas)++{- Generating schema definitions -}++genSchemaTypes :: Int -> IO [SchemaV]+genSchemaTypes numSchemasToGenerate =+  generate $ sequence $ take numSchemasToGenerate+    [ resize n arbitrary | n <- [0,2..] ]++instance Arbitrary SchemaV where+  arbitrary = Schema <$> sized genSchemaObject++-- | Generate an arbitrary schema.+--+-- SchemaType is a recursive definition, so we want to make sure that generating a schema will+-- terminate, and also not take too long. The ways we account for that are:+--  * Providing an upper bound on the depth of any object schemas in the current object (n / 2)+--  * Providing an upper bound on the number of keys in the current object (n / 3)+--  * Providing an upper bound on the number of schemas in a union (n / 5)+genSchemaObject :: Int -> Gen SchemaObjectMapV+genSchemaObject n = do+  keys <- genUniqList1 (n `div` 3) genKey+  forM keys $ \key -> frequency+    [ (10, genSchemaObjectPairNormal key)+    , (1, genSchemaObjectPairPhantom key)+    ]+  where+    genSchemaObject' = do+      n' <- choose (0, n `div` 2)+      SchemaObject <$> genSchemaObject n'++    genSchemaObjectPairNormal key = do+      schemaType <- frequency $ if n == 0+        then scalarSchemaTypes+        else allSchemaTypes+      return (NormalKey key, schemaType)++    genSchemaObjectPairPhantom key = do+      schemaType <- frequency+        [ (2, SchemaMaybe <$> genSchemaObject')+        , (2, SchemaTry <$> frequency nonNullableSchemaTypes)+        , (4, genSchemaObject')+        , (1, genSchemaUnion genSchemaObject')+        ]+      return (PhantomKey key, schemaType)++    scalarSchemaTypes =+      [ (4, pure $ SchemaScalar $ NameRef "Bool")+      , (4, pure $ SchemaScalar $ NameRef "Int")+      , (4, pure $ SchemaScalar $ NameRef "Double")+      , (4, pure $ SchemaScalar $ NameRef "Text")+      ]++    nonNullableSchemaTypes =+      scalarSchemaTypes +++      [ (2, SchemaList <$> frequency allSchemaTypes)+      , (1, genSchemaUnion $ frequency allSchemaTypes)+      , (2, genSchemaObject')+      ]++    allSchemaTypes =+      nonNullableSchemaTypes +++      [ (2, SchemaMaybe <$> frequency nonNullableSchemaTypes)+      , (2, SchemaTry <$> frequency nonNullableSchemaTypes)+      ]++    -- avoid generating big unions by scaling list length+    genSchemaUnion gen = SchemaUnion <$> genUniqList1 (n `div` 5) gen+++-- | Generate a valid JSON key+-- See Data.Aeson.Schema.TH.Parse.jsonKey'+genKey :: Gen String+genKey = listOf1 $ arbitraryPrintableChar `suchThat` (`notElem` " \"\\!?[](),.@:{}#")++-- | Generate a non-empty and unique list of the given generator.+--+-- Takes in the max size of the list.+genUniqList1 :: Eq a => Int -> Gen a -> Gen [a]+genUniqList1 n gen = do+  k <- choose (1, max 1 n)+  take k . nub <$> infiniteListOf gen++{- Helper type families -}++type family Fst x where+  Fst '(a, _) = a++type family FilterObjectSchemas schemas where+  FilterObjectSchemas '[] = '[]+  FilterObjectSchemas ('SchemaObject inner ': xs) = 'SchemaObject inner : FilterObjectSchemas xs+  FilterObjectSchemas (_ ': xs) = FilterObjectSchemas xs
+ test/TestUtils/DeepSeq.hs view
@@ -0,0 +1,64 @@+{-# LANGUAGE CPP #-}+{-# OPTIONS_GHC -Wno-orphans #-}++module TestUtils.DeepSeq () where++import Control.DeepSeq (NFData(..))+#if MIN_VERSION_template_haskell(2,16,0)+import Control.DeepSeq (rwhnf)+import GHC.ForeignPtr (ForeignPtr)+#endif+import Language.Haskell.TH.Syntax++instance NFData AnnTarget+instance NFData Bang+instance NFData Body+instance NFData Callconv+instance NFData Clause+instance NFData Con+instance NFData Dec+instance NFData DerivClause+instance NFData DerivStrategy+instance NFData Exp+instance NFData FamilyResultSig+instance NFData Fixity+instance NFData FixityDirection+instance NFData Foreign+instance NFData FunDep+instance NFData Guard+instance NFData InjectivityAnn+instance NFData Inline+instance NFData Lit+instance NFData Match+instance NFData ModName+instance NFData Name+instance NFData NameFlavour+instance NFData NameSpace+instance NFData OccName+instance NFData Overlap+instance NFData Pat+instance NFData PatSynArgs+instance NFData PatSynDir+instance NFData Phases+instance NFData PkgName+instance NFData Pragma+instance NFData Range+instance NFData Role+instance NFData RuleBndr+instance NFData RuleMatch+instance NFData Safety+instance NFData SourceStrictness+instance NFData SourceUnpackedness+instance NFData Stmt+instance NFData Type+instance NFData TypeFamilyHead+instance NFData TyLit+instance NFData TySynEqn+instance NFData TyVarBndr++#if MIN_VERSION_template_haskell(2,16,0)+instance NFData Bytes++instance NFData (ForeignPtr a) where+  rnf = rwhnf+#endif
+ test/Tests/EnumTH.hs view
@@ -0,0 +1,82 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}++module Tests.EnumTH where++import Data.Aeson (decode, encode)+import Data.Char (toLower, toUpper)+import Test.Tasty+import Test.Tasty.HUnit+import Test.Tasty.QuickCheck++import Data.Aeson.Schema.TH (genFromJSONEnum, genToJSONEnum, mkEnum)++mkEnum "State" ["OPEN", "CLOSED"]++data Color = Red | LightBlue | Yellow | DarkGreen | Black | JustABitOffWhite+  deriving (Show, Eq, Enum)++genFromJSONEnum ''Color+genToJSONEnum ''Color++{- Tests -}++test :: TestTree+test = testGroup "Enum TH helpers"+  [ testMkEnum+  , testGenJSONEnum+  ]++testMkEnum :: TestTree+testMkEnum = testGroup "mkEnum"+  [ testProperty "mkEnum decode is case insensitive" $ do+      (val, enumVal) <- elements+        [ ("OPEN", OPEN)+        , ("CLOSED", CLOSED)+        ]+      casedVal <- randomlyCased val+      return $ decode (encode casedVal) === Just enumVal+  , testCase "mkEnum encode keeps case of constructor" $ do+      encode OPEN @?= "\"OPEN\""+      encode CLOSED @?= "\"CLOSED\""+  , testProperty "mkEnum: (fromJust . decode . encode) === id" $ do+      enumVal <- elements [OPEN, CLOSED]+      return $ (decode . encode) enumVal === Just enumVal+  ]++testGenJSONEnum :: TestTree+testGenJSONEnum = testGroup "gen{To,From}JSONEnum"+  [ testProperty "genFromJSONEnum decode is case insensitive" $ do+      (val, enumVal) <- elements+        [ ("Red", Red)+        , ("LightBlue", LightBlue)+        , ("Yellow", Yellow)+        , ("DarkGreen", DarkGreen)+        , ("Black", Black)+        , ("JustABitOffWhite", JustABitOffWhite)+        ]+      casedVal <- randomlyCased val+      return $ decode (encode casedVal) === Just enumVal+  , testCase "genToJSONEnum encode keeps case of constructor" $ do+      encode Red @?= "\"Red\""+      encode LightBlue @?= "\"LightBlue\""+      encode Yellow @?= "\"Yellow\""+      encode DarkGreen @?= "\"DarkGreen\""+      encode Black @?= "\"Black\""+      encode JustABitOffWhite @?= "\"JustABitOffWhite\""+  , testProperty "genFromJSONEnum + genToJSONEnum: (fromJust . decode . encode) === id" $ do+      enumVal <- elements+        [ Red+        , LightBlue+        , Yellow+        , DarkGreen+        , Black+        , JustABitOffWhite+        ]+      return $ (decode . encode) enumVal === Just enumVal+  ]++randomlyCased :: String -> Gen String+randomlyCased s = do+  caseFuncs <- infiniteListOf $ elements [toLower, toUpper]+  return $ zipWith ($) caseFuncs s
+ test/Tests/GetQQ.hs view
@@ -0,0 +1,405 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeApplications #-}++module Tests.GetQQ where++import Control.DeepSeq (deepseq)+import Control.Exception (SomeException, try)+import Data.Aeson (FromJSON(..), ToJSON(..), withText)+import Data.Aeson.QQ (aesonQQ)+import Data.Text (Text)+import qualified Data.Text as Text+import qualified Language.Haskell.Interpreter as Hint+import Test.Tasty+import Test.Tasty.HUnit+import Test.Tasty.QuickCheck++import Data.Aeson.Schema (Object, schema)+import Data.Aeson.Schema.TH (mkEnum)+import Data.Aeson.Schema.Utils.Sum (SumType(..))+import Tests.GetQQ.TH+import TestUtils (parseObject, testGoldenIO, testParseError)++mkEnum "Greeting" ["HELLO", "GOODBYE"]++newtype Coordinate = Coordinate (Int, Int)+  deriving (Show,Eq)++instance ToJSON Coordinate where+  toJSON (Coordinate (x, y)) = toJSON $ show x ++ "," ++ show y++instance FromJSON Coordinate where+  parseJSON = withText "Coordinate" $ \s ->+    case map (read . Text.unpack) $ Text.splitOn "," s of+      [x, y] -> return $ Coordinate (x, y)+      _ -> fail $ "Bad Coordinate: " ++ Text.unpack s++{- Tests -}++test :: TestTree+test = testGroup "`get` quasiquoter"+  [ testValidExpressions+  , testInvalidExpressions+  , testCompileTimeErrors+  ]++testValidExpressions :: TestTree+testValidExpressions = testGroup "Valid get expressions"+  [ testScalarExpressions+  , testBasicExpressions+  , testNullableExpressions+  , testListExpressions+  , testUnionExpressions+  , testPhantomExpressions+  , testNestedExpressions+  ]++testScalarExpressions :: TestTree+testScalarExpressions = testGroup "Scalar expressions"+  [ testProperty "Get Bool key from object" $ \b ->+      let o = $(parseObject "{ foo: Bool }") [aesonQQ| { "foo": #{b} } |]+      in [runGet| o.foo |] === b++  , testProperty "Get Int key from object" $ \x ->+      let o = $(parseObject "{ foo: Int }") [aesonQQ| { "foo": #{x} } |]+      in [runGet| o.foo |] === x++  , testProperty "Get Double key from object" $ \x ->+      let o = $(parseObject "{ foo: Double }") [aesonQQ| { "foo": #{x} } |]+      in [runGet| o.foo |] === x++  , testProperty "Get Text key from object" $ \(UnicodeText s) ->+      let o = $(parseObject "{ foo: Text }") [aesonQQ| { "foo": #{s} } |]+      in [runGet| o.foo |] === s++  , testProperty "Get Custom key from object" $ \coordinate ->+      let o = $(parseObject "{ foo: Coordinate }") [aesonQQ| { "foo": #{coordinate} } |]+      in [runGet| o.foo |] === coordinate++  , testProperty "Get Enum key from object" $ \greeting ->+      let o = $(parseObject "{ foo: Greeting }") [aesonQQ| { "foo": #{greeting} } |]+      in [runGet| o.foo |] === greeting+  ]++testBasicExpressions :: TestTree+testBasicExpressions = testGroup "Basic expressions"+  [ testCase "Can query fields on namespaced object" $+      [runGet| (Tests.GetQQ.TH.testData).foo |] @?= [runGet| testData.foo |]++  , testProperty "Can query quoted keys" $ \x ->+      let o = $(parseObject "{ foo: Int }") [aesonQQ| { "foo": #{x :: Int} } |]+      in [runGet| o."foo" |] === [runGet| o.foo |]++  , testProperty "Can query nested fields" $ \x ->+      let o = $(parseObject "{ foo: { bar: Int } }") [aesonQQ| { "foo": { "bar": #{x} } } |]+      in [runGet| o.foo.bar |] === x++  , testProperty "Can generate a lambda expression" $ \x ->+      let o = $(parseObject "{ foo: Int }") [aesonQQ| { "foo": #{x} } |]+      in [runGet| .foo |] o === x++  , testProperty "Can extract a list of elements" $ \x y bar ->+      let o = $(parseObject "{ x: Int, y: Int, foo: { bar: Int } }") [aesonQQ|+            {+              "x": #{x},+              "y": #{y},+              "foo": { "bar": #{bar} }+            }+          |]+      in [runGet| o.[x, y, x, foo.bar] |] === [x, y, x, bar]++  , testProperty "Can extract a tuple of elements" $ \x b bar ->+      let o = $(parseObject "{ x: Int, b: Bool, foo: { bar: Int } }") [aesonQQ|+            {+              "x": #{x},+              "b": #{b},+              "foo": { "bar": #{bar} }+            }+          |]+      in [runGet| o.(x,b,x,foo.bar) |] === (x, b, x, bar)+  ]++testNullableExpressions :: TestTree+testNullableExpressions = testGroup "Nullable expressions"+  [ testCase "Get Maybe key from object with value" $ do+      let o = $(parseObject "{ foo: Maybe Bool }") [aesonQQ| { "foo": true } |]+      [runGet| o.foo |] @?= Just True+      [runGet| o.foo! |] @?= True++  , testCase "Get Maybe key from object with null value" $+      let o = $(parseObject "{ foo: Maybe Bool }") [aesonQQ| { "foo": null } |]+      in [runGet| o.foo |] @?= Nothing++  , testCase "Get Maybe key from object without value" $+      let o = $(parseObject "{ foo: Maybe Bool }") [aesonQQ| {} |]+      in [runGet| o.foo |] @?= Nothing++  , testCase "Can run operations within existing Maybe value" $+      let o = $(parseObject "{ foo: Maybe List { bar: Bool } }") [aesonQQ|+            {+              "foo": [+                { "bar": true },+                { "bar": true },+                { "bar": false }+              ]+            }+          |]+      in [runGet| o.foo?[].bar |] @?= Just [True, True, False]++  , testCase "Can run operations within nonexisting Maybe value" $+      let o = $(parseObject "{ foo: Maybe List { bar: Bool } }") [aesonQQ| { "foo": null } |]+      in [runGet| o.foo?[].bar |] @?= Nothing++  , testCase "Can run operations after unwrapping Maybe value" $+      let o = $(parseObject "{ foo: Maybe List { bar: Bool } }") [aesonQQ|+            {+              "foo": [+                { "bar": true },+                { "bar": true },+                { "bar": false }+              ]+            }+          |]+      in [runGet| o.foo![].bar |] @?= [True, True, False]++  , testCase "Get Try key from object with parsed value" $ do+      let o = $(parseObject "{ foo: Try Bool }") [aesonQQ| { "foo": true } |]+      [runGet| o.foo |] @?= Just True+      [runGet| o.foo! |] @?= True++  , testCase "Get Try key from object with invalid value" $+      let o = $(parseObject "{ foo: Try Bool }") [aesonQQ| { "foo": 1 } |]+      in [runGet| o.foo |] @?= Nothing++  , testCase "Can run operations within parsed Try value" $+      let o = $(parseObject "{ foo: Try List { bar: Bool } }") [aesonQQ|+            {+              "foo": [+                { "bar": true },+                { "bar": true },+                { "bar": false }+              ]+            }+          |]+      in [runGet| o.foo?[].bar |] @?= Just [True, True, False]++  , testCase "Can run operations within invalid Try value" $+      let o = $(parseObject "{ foo: Try List { bar: Bool } }") [aesonQQ| { "foo": [{ "baz": 1 }] } |]+      in [runGet| o.foo?[].bar |] @?= Nothing++  , testCase "Can run operations after unwrapping Try value" $+      let o = $(parseObject "{ foo: Try List { bar: Bool } }") [aesonQQ|+            {+              "foo": [+                { "bar": true },+                { "bar": true },+                { "bar": false }+              ]+            }+          |]+      in [runGet| o.foo![].bar |] @?= [True, True, False]++  , testFromJustErrors+  ]++-- test error message for bang operator, to get some coverage on startDisplay + showGetterOps+testFromJustErrors :: TestTree+testFromJustErrors = testGroup "fromJust errors"+  [ testCase "Plain fromJust" $+      assertError "Called 'fromJust' on null expression" $ [runGet| ! |] (Nothing :: Maybe Int)++  , testCase "With start" $ do+      let o = $(parseObject "{ foo: Maybe Bool }") [aesonQQ| { "foo": null } |]+      assertError "Called 'fromJust' on null expression: o.foo"+        [runGet| o.foo! |]++  , testCase "With qualified start" $+      assertError "Called 'fromJust' on null expression: (Tests.GetQQ.TH.testData).foo"+        [runGet| (Tests.GetQQ.TH.testData).foo! |]++  , testCase "With lambda" $ do+      let o = $(parseObject "{ foo: Maybe Bool }") [aesonQQ| { "foo": null } |]+      assertError "Called 'fromJust' on null expression: .foo" $ [runGet| .foo! |] o++  , testCase "Within nested list of keys" $ do+      let o = $(parseObject "{ foo: { a: Bool, b: Maybe Bool } }") [aesonQQ| { "foo": { "a": true, "b": null } } |]+      assertError "Called 'fromJust' on null expression: o.foo.b" [runGet| o.foo.[a, b!] |]++  , testCase "Within list of keys" $ do+      let o = $(parseObject "{ foo: Bool, bar: Maybe Bool }") [aesonQQ| { "foo": true, "bar": null } |]+      assertError "Called 'fromJust' on null expression: o.bar" [runGet| o.[foo, bar!] |]++  , testCase "Within nested tuple of keys" $ do+      let o = $(parseObject "{ foo: { a: Int, b: Maybe Bool } }") [aesonQQ| { "foo": { "a": 1, "b": null } } |]+      assertError "Called 'fromJust' on null expression: o.foo.b" [runGet| o.foo.(a, b!) |]++  , testCase "Within tuple of keys" $ do+      let o = $(parseObject "{ foo: Int, bar: Maybe Bool }") [aesonQQ| { "foo": 1, "bar": null } |]+      assertError "Called 'fromJust' on null expression: o.bar" [runGet| o.(foo, bar!) |]++  , testCase "Within list" $ do+      let o = $(parseObject "{ foo: List Maybe Bool }") [aesonQQ| { "foo": [null] } |]+      assertError "Called 'fromJust' on null expression: o.foo[]" [runGet| o.foo[]! |]++  , testCase "On incorrect branch selector" $ do+      let o = $(parseObject "{ foo: Bool | Int }") [aesonQQ| { "foo": 1 } |]+      assertError "Called 'fromJust' on null expression: o.foo@0" [runGet| o.foo@0! |]+  ]+  where+    assertError msg x = try @SomeException (x `deepseq` pure ()) >>= \case+      Right _ -> error "Unexpectedly succeeded"+      Left e -> (head . lines . show) e @?= msg++testListExpressions :: TestTree+testListExpressions = testGroup "List expressions"+  [ testCase "Get List key from object" $+      let o = $(parseObject "{ foo: List Int }") [aesonQQ| { "foo": [1,2,3] } |]+      in [runGet| o.foo |] @?= [1,2,3]++  , testProperty "Ending with a `[]` operator is a noop" $ \(xs :: [Int]) ->+      let o = $(parseObject "{ foo: List Int }") [aesonQQ| { "foo": #{xs} } |]+      in [runGet| o.foo |] === [runGet| o.foo[] |]++  , testCase "Can run operations within list" $+      let o = $(parseObject "{ foo: List Maybe { bar: Int } }") [aesonQQ|+            {+              "foo": [+                { "bar": 1 },+                { "bar": 2 },+                null,+                { "bar": 3 }+              ]+            }+          |]+      in [runGet| o.foo[]?.bar |] @?= [Just 1, Just 2, Nothing, Just 3]+  ]++testUnionExpressions :: TestTree+testUnionExpressions = testGroup "Union expressions"+  [ testCase "Get Union key from object" $ do+      let o = $(parseObject "{ foo: Bool | Int }") [aesonQQ| { "foo": 1 } |]+      [runGet| o.foo |] @?= There (Here 1)+      [runGet| o.foo@0 |] @?= Nothing+      [runGet| o.foo@1 |] @?= Just 1++  , testCase "Can run operations after extracting branch" $ do+      let o = $(parseObject "{ foo: { bar: Bool } | { baz: Int } }") [aesonQQ| { "foo": { "bar": true } } |]+      [runGet| o.foo@0?.bar |] @?= Just True+      [runGet| o.foo@0!.bar |] @?= True+      [runGet| o.foo@1?.baz |] @?= Nothing+  ]++testPhantomExpressions :: TestTree+testPhantomExpressions = testGroup "Phantom expressions"+  [ testProperty "Get Phantom object key from object" $ \x ->+      let o = $(parseObject "{ [foo]: { bar: Int } }") [aesonQQ| { "bar": #{x} } |]+      in [runGet| o.foo.bar |] === x++  , testProperty "Get Phantom object Try key from object" $ \x ->+      let o = $(parseObject "{ [foo]: Try { bar: Int } }") [aesonQQ| { "bar": #{x} } |]+      in [runGet| o.foo?.bar |] === Just x++  , testProperty "Get Phantom non-object Try key from object" $ \(x :: Int) ->+      let o = $(parseObject "{ [foo]: Try Int }") [aesonQQ| { "foo": #{x} } |]+      in [runGet| o.foo |] === Nothing+  ]++testNestedExpressions :: TestTree+testNestedExpressions = testGroup "Nested expressions"+  [ testProperty "Extracted object from Object can be queried further" $ \x ->+      let o = $(parseObject "{ foo: { bar: Int } }") [aesonQQ| { "foo": { "bar": #{x} } } |]+          foo = [runGet| o.foo |]+      in [runGet| foo.bar |] === x++  , testProperty "Extracted object from Maybe can be queried further" $ \x ->+      let o = $(parseObject "{ foo: Maybe { bar: Int } }") [aesonQQ| { "foo": { "bar": #{x} } } |]+          foo = [runGet| o.foo! |]+      in [runGet| foo.bar |] === x++  , testProperty "Extracted object from Try can be queried further" $ \x ->+      let o = $(parseObject "{ foo: Try { bar: Int } }") [aesonQQ| { "foo": { "bar": #{x} } } |]+          foo = [runGet| o.foo! |]+      in [runGet| foo.bar |] === x++  , testProperty "Extracted object from List can be queried further" $ \xs ->+      classify (length xs > 1) "non-trivial" $+        let bars = map (\x -> [aesonQQ| { "bar": #{x} } |]) xs+            o = $(parseObject "{ foo: List { bar: Int } }") [aesonQQ| { "foo": #{bars} } |]+            getBar :: Object [schema| { bar: Int } |] -> Int+            getBar foo = [runGet| foo.bar |]+        in map getBar [runGet| o.foo |] === xs++  , testProperty "Extracted objects from list of keys can be queried further" $ \fooId barId ->+      let o = $(parseObject "{ foo: { id: Int }, bar: { id: Int } }") [aesonQQ|+            {+              "foo": { "id": #{fooId} },+              "bar": { "id": #{barId} }+            }+          |]+          getId :: Object [schema| { id: Int } |] -> Int+          getId inner = [runGet| inner.id |]+      in map getId [runGet| o.[foo, bar] |] === [fooId, barId]++  , testProperty "Extracted objects from tuple of keys can be queried further" $ \a b ->+      let o = $(parseObject "{ foo: { a: Int }, bar: { b: Bool } }") [aesonQQ|+            {+              "foo": { "a": #{a} },+              "bar": { "b": #{b} }+            }+          |]+          (foo, bar) = [runGet| o.(foo, bar) |]+      in [runGet| foo.a |] === a .&&. [runGet| bar.b |] === b++  , testProperty "Extracted objects from union can be queried further" $ \x ->+      let o = $(parseObject "{ foo: { x: Bool } | { x: Int } }") [aesonQQ| { "foo": { "x": #{x} } } |]+      in case [runGet| o.foo |] of+        There (Here foo) -> [runGet| foo.x |] === x+        foo -> error $ "Unexpected failure: o.foo = " ++ show foo ++ ", x = " ++ show x+  ]++testInvalidExpressions :: TestTree+testInvalidExpressions = testGroup "Invalid expressions"+  [ testParseError "Empty expression" "getqq_empty_expression.golden"+      [getErr| |]+  , testParseError "No operators" "getqq_no_operators.golden"+      [getErr| o |]+  , testParseError "Operators after tuple of keys" "getqq_ops_after_tuple.golden"+      [getErr| o.(a,b).foo |]+  , testParseError "Operators after list of keys" "getqq_ops_after_list.golden"+      [getErr| o.[a,b].foo |]+  ]++testCompileTimeErrors :: TestTree+testCompileTimeErrors = testGroup "Compile-time errors"+  [ testGoldenIO "Key not in schema" "getqq_missing_key.golden" $+      getCompileError "GetMissingKey"+  ]+  where+    getCompileError name = do+      let fp = "test/wont-compile/" ++ name ++ ".hs"+      Hint.runInterpreter (Hint.loadModules [fp]) >>= \case+        Left (Hint.WontCompile errors) -> return $ unlines $ map Hint.errMsg errors+        Left e -> fail $ show e+        Right _ -> fail "Compilation unexpectedly succeeded"++{- Helpers -}++instance Arbitrary Greeting where+  arbitrary = elements [HELLO, GOODBYE]++instance Arbitrary Coordinate where+  arbitrary = Coordinate <$> arbitrary++newtype UnicodeText = UnicodeText Text+  deriving (Show)++instance Arbitrary UnicodeText where+  arbitrary = UnicodeText . Text.pack . getUnicodeString <$> arbitrary
+ test/Tests/GetQQ/TH.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}++module Tests.GetQQ.TH where++import Control.DeepSeq (deepseq)+import Data.Aeson.QQ (aesonQQ)+import Language.Haskell.TH.Quote (QuasiQuoter(..))+import Language.Haskell.TH.TestUtils+    (MockedMode(..), QMode(..), QState(..), runTestQ, runTestQErr)++import Data.Aeson.Schema (Object, get, schema)+import TestUtils (mkExpQQ, parseValue)+import TestUtils.DeepSeq ()++-- For testing namespaced object+testData :: Object [schema| { foo: Maybe Int } |]+testData = parseValue [aesonQQ| { "foo": null } |]++qState :: QState 'FullyMocked+qState = QState+  { mode = MockQ+  , knownNames = []+  , reifyInfo = []+  }++-- | Run the `get` quasiquoter at both runtime and compile-time, to get coverage.+--+-- The `get` Quasiquoter doesn't reify anything, so this should work.+runGet :: QuasiQuoter+runGet = mkExpQQ $ \s -> [| runTestQ qState (quoteExp get s) `deepseq` $(quoteExp get s) |]++getErr :: QuasiQuoter+getErr = mkExpQQ $ \s -> [| runTestQErr qState (quoteExp get s) |]
+ test/Tests/MkGetter.hs view
@@ -0,0 +1,64 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}++module Tests.MkGetter where++import Control.DeepSeq (deepseq)+import Data.Text (Text)+import Language.Haskell.TH.TestUtils+    (QMode(..), QState(..), loadNames, runTestQ, runTestQErr)+import Test.Tasty+import Test.Tasty.HUnit+import Text.RawString.QQ (r)++import Data.Aeson.Schema (Object, get, mkGetter, schema)+import TestUtils (json, showSchemaResult)+import TestUtils.DeepSeq ()++type MySchema = [schema| { users: List { name: Text } } |]++mkGetter "User" "getUsers" ''MySchema ".users[]"++test :: TestTree+test = runMkGetterQ `deepseq` testGroup "`mkGetter` helper"+  [ testCase "Type synonym is generated" $+      showSchemaResult @User @?= [r|Object (SchemaObject { "name": Text })|]++  , testCase "Getter function is generated" $+      let users :: [User]+          users = getUsers testData++          getName :: User -> Text+          getName = [get| .name |]++      in map getName users @?= ["Alice", "Bob", "Claire"]++  , testCase "mkGetter expression should be a lambda expression" $+      let msg = runTestQErr qState $ mkGetter "User" "getUsers" ''MySchema "foo.users[]"+      in msg @?= "Getter expression should start with '.': foo.users[]"+  ]+  where+    qState = QState+      { mode = MockQ+      , knownNames = []+      , reifyInfo = $(loadNames [''MySchema])+      }++    -- run same mkGetter expression that was spliced, for coverage+    runMkGetterQ = runTestQ qState $ mkGetter "User" "getUsers" ''MySchema ".users[]"++testData :: Object MySchema+testData = [json|+  {+    "users": [+      { "name": "Alice" },+      { "name": "Bob" },+      { "name": "Claire" }+    ]+  }+|]
+ test/Tests/Object.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}++module Tests.Object where++import Data.Aeson (Value(..))+import Data.Aeson.QQ (aesonQQ)+import qualified Data.HashMap.Lazy as HashMap+import Test.Tasty+import Test.Tasty.HUnit++import Data.Aeson.Schema (Object, schema, toMap)+import qualified Tests.Object.Eq+import qualified Tests.Object.FromJSON+import qualified Tests.Object.Show+import qualified Tests.Object.ToJSON+import TestUtils (parseValue)++test :: TestTree+test = testGroup "Object"+  [ Tests.Object.Show.test+  , Tests.Object.Eq.test+  , Tests.Object.FromJSON.test+  , Tests.Object.ToJSON.test++  , testCase "toMap smoketest" $+      let o :: Object [schema| { a: Bool } |]+          o = parseValue [aesonQQ| { "a": true } |]+      in toMap o @?= HashMap.fromList+        [ ("a", Bool True)+        ]+  ]
+ test/Tests/Object/Eq.hs view
@@ -0,0 +1,18 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TemplateHaskell #-}++module Tests.Object.Eq where++import Test.Tasty+import Test.Tasty.QuickCheck++import TestUtils (parseProxy)+import TestUtils.Arbitrary (ArbitraryObject(..), forAllArbitraryObjects)++test :: TestTree+test = testGroup "Eq instance"+  [ testProperty "o === o" $+      $(forAllArbitraryObjects) $ \(ArbitraryObject proxy v _) ->+        let o = either error id $ parseProxy proxy v+        in o === o+  ]
+ test/Tests/Object/FromJSON.hs view
@@ -0,0 +1,155 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}++module Tests.Object.FromJSON where++import Data.Aeson (FromJSON(..), Value)+import Data.Aeson.QQ (aesonQQ)+import Data.Aeson.Types (parseEither)+import Data.Proxy (Proxy)+import Test.Tasty+import Test.Tasty.QuickCheck++import Data.Aeson.Schema (Object)+import Tests.Object.FromJSON.TH+import TestUtils (parseProxy, testGolden)+import TestUtils.Arbitrary (ArbitraryObject(..), forAllArbitraryObjects)++test :: TestTree+test = testGroup "FromJSON instance" $+  map runTestCase testCases +++  [ testProperty "QuickCheck arbitrary Schema" $+      $(forAllArbitraryObjects) $ \(ArbitraryObject proxy v _) ->+        case parseProxy proxy v of+          Right _ -> property ()+          Left e -> error $ "Could not parse: " ++ e+  ]++testCases :: [FromJSONTestCase]+testCases =+ [ CheckValid "Scalar valid"+      [schemaProxy| { foo: Text } |]+      $ \(s :: String) -> [aesonQQ| { "foo": #{s} } |]+  , CheckError "Scalar invalid" "fromjson_scalar_invalid.golden"+      [schemaProxy| { foo: Text } |]+      [aesonQQ| { "foo": 1 } |]++  , CheckValid "Maybe valid"+      [schemaProxy| { foo: Maybe Int } |]+      $ \(x :: Maybe Int) -> [aesonQQ| { "foo": #{x} } |]+  , CheckError "Maybe invalid" "fromjson_maybe_invalid.golden"+      [schemaProxy| { foo: Maybe Int } |]+      [aesonQQ| { "foo": true } |]++  , CheckValid "Try valid with valid parse"+      [schemaProxy| { foo: Try Bool } |]+      $ \(x :: Bool) -> [aesonQQ| { "foo": #{x} } |]+  , CheckValid "Try valid with invalid parse"+      [schemaProxy| { foo: Try Bool } |]+      $ \(s :: String) -> [aesonQQ| { "foo": #{s} } |]++  , CheckValid "List valid"+      [schemaProxy| { foo: List Double } |]+      $ \(xs :: [Double]) -> [aesonQQ| { "foo": #{xs} } |]+  , CheckError "List invalid" "fromjson_list_invalid.golden"+      [schemaProxy| { foo: List Double } |]+      [aesonQQ| { "foo": true } |]+  , CheckError "List invalid inner" "fromjson_list_inner_invalid.golden"+      [schemaProxy| { foo: List Double } |]+      [aesonQQ| { "foo": [true] } |]++  , CheckError "Object invalid" "fromjson_object_invalid.golden"+      [schemaProxy| { foo: Int } |]+      [aesonQQ| 1 |]+  , CheckError "Object invalid in later keys" "fromjson_object_later_keys_invalid.golden"+      [schemaProxy| { foo: Int, bar: Int } |]+      [aesonQQ| { "foo": 1, "bar": true } |]++  , CheckValid "Nested object valid"+      [schemaProxy| { foo: { bar: Int } } |]+      $ \(x :: Int) -> [aesonQQ| { "foo": { "bar": #{x} } } |]+  , CheckError "Nested object invalid" "fromjson_nested_invalid.golden"+      [schemaProxy| { foo: { bar: Int } } |]+      [aesonQQ| { "foo": true } |]+  , CheckError "Nested object invalid inner" "fromjson_nested_inner_invalid.golden"+      [schemaProxy| { foo: { bar: Int } } |]+      [aesonQQ| { "foo": { "bar": true } } |]++  , CheckValid "Union object valid"+      [schemaProxy| { foo: Int | Text } |]+      $ \(x :: Int) -> [aesonQQ| { "foo": #{x} } |]+  , CheckError "Union object invalid" "fromjson_union_invalid.golden"+      [schemaProxy| { foo: Int | Text } |]+      [aesonQQ| { "foo": true } |]++  , CheckValid "Phantom key valid object"+      [schemaProxy| { [foo]: { bar: Int } } |]+      $ \(x :: Int) -> [aesonQQ| { "bar": #{x} } |]+  , CheckValid "Phantom key valid non-object try"+      [schemaProxy| { [foo]: Try Bool } |]+      $ \(b :: Bool) -> [aesonQQ| { "bar": #{b} } |]+  , CheckError "Phantom key invalid" "fromjson_phantom_invalid.golden"+      [schemaProxy| { [foo]: { bar: Int } } |]+      [aesonQQ| 1 |]+  , CheckError "Phantom key missing inner" "fromjson_phantom_inner_missing.golden"+      [schemaProxy| { [foo]: { bar: Int } } |]+      [aesonQQ| { "foo": true } |]+  , CheckError "Phantom key invalid inner" "fromjson_phantom_inner_invalid.golden"+      [schemaProxy| { [foo]: { bar: Int } } |]+      [aesonQQ| { "bar": true } |]++  , CheckError "Decode failure messages are truncated" "fromjson_error_messages_truncate.golden"+      [schemaProxy| { foo: Int } |]+      [aesonQQ|+        {+          "foo": [+            { "bar": 1, "baz": "a" },+            { "bar": 2, "baz": "b" },+            { "bar": 3, "baz": "c" },+            { "bar": 4, "baz": "d" }+          ]+        }+      |]+  ]++{- Helpers -}++data FromJSONTestCase where+  CheckValid+    :: (Arbitrary a, Show a, FromJSON (Object schema))+    => TestName              -- ^ Name of test case+    -> Proxy (Object schema) -- ^ The schema to parse with+    -> (a -> Value)          -- ^ A function that builds a Value that should satisfy the schema+    -> FromJSONTestCase++  CheckError+    :: (FromJSON (Object schema), Show (Object schema))+    => TestName              -- ^ Name of test case+    -> String                -- ^ Name of golden file+    -> Proxy (Object schema) -- ^ The schema to parse with+    -> Value                 -- ^ The value that should fail parsing the given schema+    -> FromJSONTestCase++runTestCase :: FromJSONTestCase -> TestTree+runTestCase = \case+  CheckValid name schema valueGen ->+    testProperty name $ \a ->+      case parse schema (valueGen a) of+        Right _ -> ()+        Left e -> error $ "Unexpected failure: " ++ e++  CheckError name fp schema value ->+    testGolden name fp $+      case parse schema value of+        Right o -> error $ "Unexpectedly parsed: " ++ show o+        Left e -> e++parse :: FromJSON a => Proxy a -> Value -> Either String a+parse _ = parseEither parseJSON
+ test/Tests/Object/FromJSON/TH.hs view
@@ -0,0 +1,14 @@+{-# LANGUAGE TemplateHaskell #-}++module Tests.Object.FromJSON.TH where++import Data.Proxy (Proxy(..))+import Language.Haskell.TH.Quote (QuasiQuoter(quoteType))++import Data.Aeson.Schema (Object, schema)+import TestUtils (mkExpQQ)++schemaProxy :: QuasiQuoter+schemaProxy = mkExpQQ $ \s ->+  let schemaType = [t| Object $(quoteType schema s) |]+  in [| Proxy :: Proxy $schemaType |]
+ test/Tests/Object/Show.hs view
@@ -0,0 +1,70 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}++module Tests.Object.Show where++import Data.Aeson.QQ (aesonQQ)+import Data.String.Interpolate (i)+import Test.Tasty+import Test.Tasty.HUnit+import Test.Tasty.QuickCheck++import Tests.Object.Show.TH+import TestUtils (parseObject)++test :: TestTree+test = testGroup "Show instance"+  [ testProperty "Scalar key" $ \(s :: String) ->+      let o = $(parseObject "{ foo: Text }") [aesonQQ| { "foo": #{s} } |]+      in show o === [i|{ "foo": #{show s} }|]++  , testProperty "Object with multiple keys" $ \(b :: Double, x :: Int) ->+      let o = $(parseObject "{ foo: Double, bar: Int }") [aesonQQ| { "foo": #{b}, "bar": #{x} } |]+      in show o === [i|{ "foo": #{show b}, "bar": #{show x} }|]++  , testProperty "Nested object" $ \(x :: Int) ->+      let o = $(parseObject "{ foo: { bar: Int } }") [aesonQQ| { "foo": { "bar": #{x} } } |]+      in show o === [i|{ "foo": { "bar": #{show x} } }|]++  , testProperty "Object with existing Maybe key" $ \(x :: Bool) ->+      let o = $(parseObject "{ foo: Maybe Bool }") [aesonQQ| { "foo": #{x} } |]+      in show o === [i|{ "foo": Just #{show x} }|]++  , testCase "Object with non-existing Maybe key" $+      let o = $(parseObject "{ foo: Maybe Double }") [aesonQQ| { "foo": null } |]+      in show o @?= [i|{ "foo": Nothing }|]++  , testProperty "Object with valid Try key" $ \(b :: Bool) ->+      let o = $(parseObject "{ foo: Try Bool }") [aesonQQ| { "foo": #{b} } |]+      in show o === [i|{ "foo": Just #{show b} }|]++  , testProperty "Object with invalid Try key" $ \(x :: Int) ->+      let o = $(parseObject "{ foo: Try Bool }") [aesonQQ| { "foo": #{x} } |]+      in show o === [i|{ "foo": Nothing }|]++  , testProperty "Object with List key" $ \(x :: [Int]) ->+      let o = $(parseObject "{ foo: List Int }") [aesonQQ| { "foo": #{x} } |]+      in show o === [i|{ "foo": #{show x} }|]++  , testProperty "Object with Union key branch 0" $ \(Positive (x :: Int)) ->+      let o = $(parseObject "{ foo: Int | Text }") [aesonQQ| { "foo": #{x} } |]+      in show o === [i|{ "foo": Here #{show x} }|]++  , testProperty "Object with Union key branch 1" $ \(s :: String) ->+      let o = $(parseObject "{ foo: Int | Text }") [aesonQQ| { "foo": #{s} } |]+      in show o === [i|{ "foo": There (Here #{show s}) }|]++  , testProperty "Object with referenced Object" $ \(name :: String) ->+      let o = $(parseObject "{ user: #UserSchema }") [aesonQQ| { "user": { "name": #{name} } } |]+      in show o === [i|{ "user": { "name": #{show name} } }|]++  , testProperty "Object with extended Object" $ \(name :: String, age :: Int) ->+      let o = $(parseObject "{ #UserSchema, age: Int }") [aesonQQ| { "name": #{name}, "age": #{age} } |]+      in show o === [i|{ "name": #{show name}, "age": #{show age} }|]++  , testProperty "Object with Phantom key" $ \(x :: Int) ->+      let o = $(parseObject "{ [foo]: { bar: Int } }") [aesonQQ| { "bar": #{x} } |]+      in show o === [i|{ [foo]: { "bar": #{show x} } }|]+  ]
+ test/Tests/Object/Show/TH.hs view
@@ -0,0 +1,8 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE QuasiQuotes #-}++module Tests.Object.Show.TH where++import Data.Aeson.Schema (schema)++type UserSchema = [schema| { name: Text } |]
+ test/Tests/Object/ToJSON.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE TemplateHaskell #-}+{-# OPTIONS_GHC -Wno-orphans #-}++module Tests.Object.ToJSON where++import Data.Aeson (FromJSON(..), ToJSON(..))+import qualified Data.Aeson.Types as Aeson+import Test.Tasty+import Test.Tasty.QuickCheck++import TestUtils (parseProxy)+import TestUtils.Arbitrary (ArbitraryObject(..), forAllArbitraryObjects)++test :: TestTree+test = testGroup "ToJSON instance"+  [ testProperty "parseJSON . toJSON === pure" $+      $(forAllArbitraryObjects) $ \(ArbitraryObject proxy v _) ->+        let o = either error id $ parseProxy proxy v+        in (parseJSON . toJSON) o === pure o+  ]++{- Realizing Aeson.Parser -}++-- We're defining two Parsers to be equivalent if they evaluate to the same result.+instance Eq a => Eq (Aeson.Parser a) where+  a == b = runParser a == runParser b++instance Show a => Show (Aeson.Parser a) where+  show = show . runParser++runParser :: Aeson.Parser a -> Aeson.Result a+runParser = Aeson.parse id
+ test/Tests/SchemaQQ.hs view
@@ -0,0 +1,221 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TypeApplications #-}++module Tests.SchemaQQ where++import qualified Data.Text as Text+import Test.Tasty+import Test.Tasty.HUnit+import Text.RawString.QQ (r)++import Tests.SchemaQQ.TH+import TestUtils (testParseError)++test :: TestTree+test = testGroup "`schema` quasiquoter"+  [ testValidSchemas+  , testInvalidSchemas+  , testKeys+  ]++testValidSchemas :: TestTree+testValidSchemas = testGroup "Valid schemas"+  [ testCase "Object with Bool field" $+      assertMatches+        [schemaRep| { a: Bool } |]+        [r| SchemaObject { "a": Bool } |]++  , testCase "Object with Int field" $+      assertMatches+        [schemaRep| { a: Int } |]+        [r| SchemaObject { "a": Int } |]++  , testCase "Object with Double field" $+      assertMatches+        [schemaRep| { foo123: Double } |]+        [r| SchemaObject { "foo123": Double } |]++  , testCase "Object with Text field" $+      assertMatches+        [schemaRep| { some_text: Text } |]+        [r| SchemaObject { "some_text": Text } |]++  , testCase "Object with a field with a custom type" $+      assertMatches+        [schemaRep| { status: Status } |]+        [r| SchemaObject { "status": Status } |]++  , testCase "Object with a field with a Maybe type" $+      assertMatches+        [schemaRep| { a: Maybe Bool } |]+        [r| SchemaObject { "a": Maybe Bool } |]++  , testCase "Object with a field with a Try type" $+      assertMatches+        [schemaRep| { a: Try Bool } |]+        [r| SchemaObject { "a": Try Bool } |]++  , testCase "Object with a nested object" $+      assertMatches+        [schemaRep| { a: { b: Int } } |]+        [r| SchemaObject { "a": { "b": Int } } |]++  , testCase "Object with a nullable nested object" $+      assertMatches+        [schemaRep| { a: Maybe { b: Int } } |]+        [r| SchemaObject { "a": Maybe { "b": Int } } |]++  , testCase "Object with a list of nested objects" $+      assertMatches+        [schemaRep| { a: List { b: Int } } |]+        [r| SchemaObject { "a": List { "b": Int } } |]++  , testCase "Object with an imported schema" $+      assertMatches+        [schemaRep| { user: #UserSchema } |]+        [r| SchemaObject { "user": { "name": Text } } |]++  , testCase "Object with a qualified imported schema" $+      assertMatches+        [schemaRep| { user: #(Tests.SchemaQQ.TH.UserSchema) } |]+        [r| SchemaObject { "user": { "name": Text } } |]++  , testCase "Object with an imported schema that uses a non-imported type" $+      assertMatches+        [schemaRep| { a: #SchemaWithHiddenImport } |]+        [r| SchemaObject { "a": { "a": CBool } } |]++  , testCase "Object with an extended schema" $+      assertMatches+        [schemaRep| { a: Int, #ExtraSchema } |]+        [r| SchemaObject { "a": Int, "extra": Text } |]++  , testCase "Object with a qualified extended schema" $+      assertMatches+        [schemaRep| { a: Int, #(Tests.SchemaQQ.TH.ExtraSchema) } |]+        [r| SchemaObject { "a": Int, "extra": Text } |]++  , testCase "Object with an extended schema that uses a non-imported type" $+      assertMatches+        [schemaRep| { #SchemaWithHiddenImport } |]+        [r| SchemaObject { "a": CBool } |]++  , testCase "Object with an extended schema with a shadowed key" $+      assertMatches+        [schemaRep| { extra: Bool, #ExtraSchema } |]+        [r| SchemaObject { "extra": Bool } |]++  , testCase "Object with a qualified extended schema with a shadowed key" $+      assertMatches+        [schemaRep| { extra: Bool, #(Tests.SchemaQQ.TH.ExtraSchema) } |]+        [r| SchemaObject { "extra": Bool } |]++  , testCase "Object with a union field" $+      assertMatches+        [schemaRep| { a: List Int | Text } |]+        [r| SchemaObject { "a": ( List Int | Text ) } |]++  , testCase "Object with a union field with a group" $+      assertMatches+        [schemaRep| { a: List (Int | Text) } |]+        [r| SchemaObject { "a": List ( Int | Text ) } |]++  , testCase "Object with a phantom key for an object" $+      assertMatches+        [schemaRep| { [a]: { b: Int } } |]+        [r| SchemaObject { [a]: { "b": Int } } |]++  , testCase "Object with a phantom key for a Maybe" $+      assertMatches+        [schemaRep| { [a]: Maybe { b: Int } } |]+        [r| SchemaObject { [a]: Maybe { "b": Int } } |]++  , testCase "Object with a phantom key for a Try" $+      assertMatches+        [schemaRep| { [a]: Try { b: Int } } |]+        [r| SchemaObject { [a]: Try { "b": Int } } |]++  , testCase "Object with a phantom key for a non-object Try" $+      assertMatches+        [schemaRep| { [a]: Try Bool } |]+        [r| SchemaObject { [a]: Try Bool } |]++  , testCase "Object with a phantom key for a union of valid schemas" $+      assertMatches+        [schemaRep| { [a]: { b: Int } | Int } |]+        [r| SchemaObject { [a]: ( { "b": Int } | Int ) } |]+  ]++testInvalidSchemas :: TestTree+testInvalidSchemas = testGroup "Invalid schemas"+  [ testCase "Object with a duplicate key" $+      [schemaErr| { a: Int, a: Bool } |] @?= "Key 'a' specified multiple times"++  , testCase "Object with a duplicate phantom key" $+      [schemaErr| { a: Int, [a]: { b: Bool } } |] @?= "Key 'a' specified multiple times"++  , testCase "Object with a duplicate key from extending" $+      [schemaErr| { #ExtraSchema, #ExtraSchema2 } |] @?= "Key 'extra' declared in multiple imported schemas"++  , testCase "Quasiquoter defining a non-object" $+      [schemaErr| List { a: Int } |] @?= "`schema` definition must be an object"++  , testCase "Object with a field with an unknown type" $+      [schemaErr| { a: HelloWorld } |] @?= "Unknown type: HelloWorld"++  , testCase "Object extending a non-schema" $+      [schemaErr| { #Int } |] @?= "'GHC.Types.Int' is not a Schema"++  , testCase "Object importing an unknown schema" $+      [schemaErr| { foo: #FooSchema } |] @?= "Unknown schema: FooSchema"++  , testCase "Object extending an unknown schema" $+      [schemaErr| { #FooSchema } |] @?= "Unknown schema: FooSchema"++  , testCase "Object with a phantom key for a scalar" $+      [schemaErr| { [a]: Int } |] @?= "Invalid schema for 'a': SchemaScalar Int"++  , testCase "Object with a phantom key for a list" $+      [schemaErr| { [a]: List Int } |] @?= "Invalid schema for 'a': SchemaList Int"++  , testCase "Object with a phantom key for a non-object Maybe" $+      [schemaErr| { [a]: Maybe Int } |] @?= "Invalid schema for 'a': SchemaMaybe Int"++  , testCase "Object with a phantom key for an invalid union" $+      [schemaErr| { [a]: Bool | Int } |] @?= "Invalid schema for 'a': SchemaUnion ( Bool | Int )"+  ]++testKeys :: TestTree+testKeys = testGroup "Keys in schemas"+  [ testCase "Quoted key same as plain key" $+      [schemaRep| { a: Int } |] @?= [schemaRep| { "a": Int } |]++  , testParseError "Key with invalid character" "schemaqq_key_with_invalid_character.golden"+      [schemaErr| { "a:b": Int } |]++  , testCase "Key with escaped invalid character" $+      assertMatches+        [schemaRep| { "a\:b": Int } |]+        [r| SchemaObject { "a:b": Int } |]++  , testParseError "Key with trailing escape" "schemaqq_key_with_trailing_escape.golden"+      [schemaErr| { "a\": Int } |]++  , testCase "Quoted key that starts with '//'" $+      assertMatches+        [schemaRep| { "//a": { b: Int } } |]+        [r| SchemaObject { "//a": { "b": Int } } |]++  , testCase "Phantom key that starts with '//'" $+      assertMatches+        [schemaRep| { [//a]: { b: Int } } |]+        [r| SchemaObject { [//a]: { "b": Int } } |]+  ]++{- Helpers -}++assertMatches :: String -> String -> Assertion+assertMatches a b = strip a @?= strip b+  where+    strip = Text.unpack . Text.strip . Text.pack
+ test/Tests/SchemaQQ/TH.hs view
@@ -0,0 +1,71 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeApplications #-}+{-# OPTIONS_GHC -Wno-orphans #-}++module Tests.SchemaQQ.TH where++import Control.DeepSeq (deepseq)+import Data.Aeson (FromJSON, ToJSON)+import Foreign.C (CBool(..))+import Language.Haskell.TH.Quote (QuasiQuoter(..))+import Language.Haskell.TH.TestUtils+    (MockedMode(..), QMode(..), QState(..), loadNames, runTestQ, runTestQErr)++import Data.Aeson.Schema (schema)+import Data.Aeson.Schema.Internal (showSchemaType)+import Data.Aeson.Schema.Type (ToSchemaObject)+import TestUtils (mkExpQQ)+import TestUtils.DeepSeq ()++type UserSchema = [schema| { name: Text } |]+type ExtraSchema = [schema| { extra: Text } |]+type ExtraSchema2 = [schema| { extra: Maybe Text } |]++newtype Status = Status Int+  deriving (Show,FromJSON,ToJSON)++-- | The type referenced here should not be imported in SchemaQQ.hs nor included in 'knownNames'.+type SchemaWithHiddenImport = [schema| { a: CBool } |]+deriving instance ToJSON CBool+deriving instance FromJSON CBool++-- Compile above types before reifying+$(return [])++qState :: QState 'FullyMocked+qState = QState+  { mode = MockQ+  , knownNames =+      [ ("Status", ''Status)+      , ("UserSchema", ''UserSchema)+      , ("ExtraSchema", ''ExtraSchema)+      , ("ExtraSchema2", ''ExtraSchema2)+      , ("Tests.SchemaQQ.TH.UserSchema", ''UserSchema)+      , ("Tests.SchemaQQ.TH.ExtraSchema", ''ExtraSchema)+      , ("SchemaWithHiddenImport", ''SchemaWithHiddenImport)+      , ("Int", ''Int)+      ]+  , reifyInfo = $(loadNames+      [ ''UserSchema+      , ''ExtraSchema+      , ''ExtraSchema2+      , ''SchemaWithHiddenImport+      , ''Int+      ]+    )+  }++-- | A quasiquoter for generating the string representation of a schema.+--+-- Also runs the `schema` quasiquoter at runtime, to get coverage information.+schemaRep :: QuasiQuoter+schemaRep = mkExpQQ $ \s ->+  let schemaType = quoteType schema s+  in [| runTestQ qState (quoteType schema s) `deepseq` showSchemaType @(ToSchemaObject $schemaType) |]++schemaErr :: QuasiQuoter+schemaErr = mkExpQQ $ \s -> [| runTestQErr qState (quoteType schema s) |]
+ test/Tests/SumType.hs view
@@ -0,0 +1,89 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE TypeApplications #-}++module Tests.SumType where++import Data.Aeson (ToJSON, eitherDecode, encode)+import Data.Proxy (Proxy(..))+import Data.String (fromString)+import Test.Tasty+import Test.Tasty.Golden+import Test.Tasty.HUnit+import Test.Tasty.QuickCheck++import Data.Aeson.Schema.Utils.Sum (SumType(..), fromSumType)++type SpecialJSON = SumType '[Bool, Int, [String]]++toSpecialJSON :: ToJSON a => a -> SpecialJSON+toSpecialJSON = either (error . ("Invalid SpecialJSON: " ++) . show) id . toSpecialJSON'++toSpecialJSON' :: ToJSON a => a -> Either String SpecialJSON+toSpecialJSON' = eitherDecode . encode++{- Tests -}++test :: TestTree+test = testGroup "SumType"+  [ testCase "Sanity checks" $+      -- this should compile+      let values =+            [ Here True+            , Here False+            , There (Here 1)+            , There (Here 10)+            , There (There (Here []))+            , There (There (Here ["a"]))+            ] :: [SpecialJSON]+      in values @?= values+  , testDecode+  , testFromSumType+  ]++testDecode :: TestTree+testDecode = testGroup "Decode SumType"+  [ testProperty "branch 1" $ \(b :: Bool) ->+      toSpecialJSON' b === Right (Here b)+  , testProperty "branch 2" $ \(x :: Int) ->+      toSpecialJSON' x === Right (There (Here x))+  , testProperty "branch 3" $ \(l :: [String]) ->+      toSpecialJSON' l === Right (There (There (Here l)))+  , goldenVsString "invalid SumType" "test/goldens/sumtype_decode_invalid.golden" $+      case toSpecialJSON' [True] of+        Right v -> error $ "Unexpectedly decoded value: " ++ show v+        Left e -> pure $ fromString e+  ]++testFromSumType :: TestTree+testFromSumType = testGroup "fromSumType"+  [ testProperty "branch 0 valid" $ \b ->+      fromSumType (Proxy @0) (toSpecialJSON b) === Just b+  , testProperty "branch 0 invalid" $+      forAll (specialJSONExcept 0) $ \(branch, value) ->+        labelBranch branch $ fromSumType (Proxy @0) value === Nothing++  , testProperty "branch 1 valid" $ \x ->+      fromSumType (Proxy @1) (toSpecialJSON x) === Just x+  , testProperty "branch 1 invalid" $+      forAll (specialJSONExcept 1) $ \(branch, value) ->+        labelBranch branch $ fromSumType (Proxy @1) value === Nothing++  , testProperty "branch 2 valid" $ \l ->+      fromSumType (Proxy @2) (toSpecialJSON l) === Just l+  , testProperty "branch 2 invalid" $+      forAll (specialJSONExcept 2) $ \(branch, value) ->+        labelBranch branch $ fromSumType (Proxy @2) value === Nothing+  ]+  where+    specialJSONExcept :: Int -> Gen (Int, SpecialJSON)+    specialJSONExcept validBranch =+      let fmapFst (a, gen) = (a,) <$> gen+      in oneof $ map fmapFst $ filter ((/= validBranch) . fst)+        [ (0, toSpecialJSON <$> arbitrary @Bool)+        , (1, toSpecialJSON <$> arbitrary @Int)+        , (2, toSpecialJSON <$> arbitrary @[String])+        ]++    labelBranch branch = label $ "branch " ++ show branch
+ test/Tests/UnwrapQQ.hs view
@@ -0,0 +1,111 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TypeApplications #-}++module Tests.UnwrapQQ where++import qualified Data.Text as Text+import Test.Tasty+import Test.Tasty.HUnit+import Text.RawString.QQ (r)++import Data.Aeson.Schema (Object, get)+import Tests.UnwrapQQ.TH+import TestUtils (json, testParseError)++test :: TestTree+test = testGroup "`unwrap` quasiquoter"+  [ testValidUnwrapDefs+  , testInvalidUnwrapDefs+  ]++testValidUnwrapDefs :: TestTree+testValidUnwrapDefs = testGroup "Valid unwrap definitions"+  [ testCase "Can unwrap a list" $ do+      [unwrapRep| ListSchema.ids |] @?= "[Int]"+      [unwrapRep| ListSchema.ids[] |] @?= "Int"++  , testCase "Can unwrap a list of keys" $+      [unwrapRep| ABCSchema.[a, b] |] @?= "[Bool]"++  , testCase "Can unwrap a tuple of keys" $+      [unwrapRep| ABCSchema.(a, b, c) |] @?= "(Bool,Bool,Double)"++  , testCase "Can unwrap a maybe" $ do+      [unwrapRep| MaybeSchema.class |] @?= "Maybe Text"+      [unwrapRep| MaybeSchema.class! |] @?= "Text"+      [unwrapRep| MaybeSchema.class? |] @?= "Text"++  , testCase "Can unwrap a sum type" $ do+      [unwrapRep| SumSchema.verbosity@0 |] @?= "Int"+      [unwrapRep| SumSchema.verbosity@1 |] @?= "Bool"++  , testCase "Can unwrap an included schema" $+      [unwrapRep| ListSchema2.list.ids |] @?= "[Int]"++  , testCase "Can unwrap an Object twice" $+      [unwrapRep| UnwrappedNestedSchema.b |] @?= "Object (SchemaObject { \"c\": Bool })"++  , testCase "Can use unwrapped type" $ do+      let result :: Object MySchema+          result = [json|+            {+              "users": [+                { "name": "Alice" },+                { "name": "Bob" },+                { "name": "Claire" }+              ]+            }+          |]++          users :: [User]+          users = [get| result.users |]++          getName :: User -> String+          getName = Text.unpack . [get| .name |]++      map getName users @?= ["Alice", "Bob", "Claire"]+  ]++testInvalidUnwrapDefs :: TestTree+testInvalidUnwrapDefs = testGroup "Invalid unwrap definitions"+  [ testCase "Unwrap unknown schema" $+      [unwrapErr| FooSchema.asdf |] @?= "Unknown schema: FooSchema"++  , testCase "Unwrap non-schema" $+      [unwrapErr| NotASchema.foo |] @?= "'Tests.UnwrapQQ.TH.NotASchema' is not a Schema"++  , testCase "Unwrap key on non-object" $+      [unwrapErr| ListSchema.ids.foo |] @?= "Cannot get key 'foo' in schema: SchemaList Int"++  , testCase "Unwrap maybe on non-maybe" $ do+      [unwrapErr| ListSchema.ids! |] @?= "Cannot use `!` operator on schema: SchemaList Int"+      [unwrapErr| ListSchema.ids? |] @?= "Cannot use `?` operator on schema: SchemaList Int"++  , testCase "Unwrap list on non-list" $+      [unwrapErr| MaybeSchema.class[] |] @?= "Cannot use `[]` operator on schema: SchemaMaybe Text"++  , testCase "Unwrap nonexistent key" $+      [unwrapErr| ListSchema.foo |] @?= [r|Key 'foo' does not exist in schema: SchemaObject { "ids": List Int }|]++  , testCase "Unwrap list of keys with different types" $+      [unwrapErr| ABCSchema.[a,b,c] |] @?= [r|List contains different types in schema: SchemaObject { "a": Bool, "b": Bool, "c": Double }|]++  , testCase "Unwrap list of keys on non-object schema" $+      [unwrapErr| ListSchema.ids.[a,b] |] @?= "Cannot get keys in schema: SchemaList Int"++  , testParseError "Unwrap beyond list of keys" "unwrapqq_unwrap_past_list.golden"+      [unwrapErr| ABCSchema.[a,b].foo |]++  , testCase "Unwrap tuple of keys on non-object schema" $+      [unwrapErr| ListSchema.ids.(a,b) |] @?= "Cannot get keys in schema: SchemaList Int"++  , testParseError "Unwrap beyond tuple of keys" "unwrapqq_unwrap_past_tuple.golden"+      [unwrapErr| ABCSchema.(a,b).foo |]++  , testCase "Unwrap branch on non-branch" $+      [unwrapErr| MaybeSchema.class@0 |] @?= "Cannot use `@` operator on schema: SchemaMaybe Text"++  , testCase "Unwrap out of bounds branch" $+      [unwrapErr| SumSchema.verbosity@10 |] @?= "Branch out of bounds for schema: SchemaUnion ( Int | Bool )"+  ]
+ test/Tests/UnwrapQQ/TH.hs view
@@ -0,0 +1,93 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}++module Tests.UnwrapQQ.TH where++import Control.DeepSeq (deepseq)+import Language.Haskell.TH (appTypeE)+import Language.Haskell.TH.Quote (QuasiQuoter(..))+import Language.Haskell.TH.TestUtils+    (MockedMode(..), QMode(..), QState(..), loadNames, runTestQ, runTestQErr)++import Data.Aeson.Schema (schema, unwrap)+import TestUtils (ShowSchemaResult(..), mkExpQQ)+import TestUtils.DeepSeq ()++type ListSchema = [schema| { ids: List Int } |]+type MaybeSchema = [schema| { class: Maybe Text } |]+type SumSchema = [schema| { verbosity: Int | Bool } |]+type ABCSchema = [schema|+  {+    a: Bool,+    b: Bool,+    c: Double,+  }+|]++type NestedSchema = [schema|+  {+    a: {+      b: {+        c: Bool,+      },+    },+  }+|]++type MySchema = [schema|+  {+    users: List {+      name: Text,+    },+  }+|]++-- Compile above schemas before these schemas+$(return [])++type ListSchema2 = [schema| { list: #ListSchema } |]+type User = [unwrap| MySchema.users[] |]+type UnwrappedNestedSchema = [unwrap| NestedSchema.a |]++type NotASchema = Int++-- Compile above types before reifying+$(return [])++qState :: QState 'FullyMocked+qState = QState+  { mode = MockQ+  , knownNames =+      [ ("ListSchema", ''ListSchema)+      , ("ListSchema2", ''ListSchema2)+      , ("MaybeSchema", ''MaybeSchema)+      , ("SumSchema", ''SumSchema)+      , ("ABCSchema", ''ABCSchema)+      , ("NotASchema", ''NotASchema)+      , ("UnwrappedNestedSchema", ''UnwrappedNestedSchema)+      ]+  , reifyInfo = $(+      loadNames+        [ ''ListSchema+        , ''ListSchema2+        , ''MaybeSchema+        , ''SumSchema+        , ''ABCSchema+        , ''NotASchema+        , ''MySchema+        , ''UnwrappedNestedSchema+        ]+    )+  }++-- | A quasiquoter for generating the string representation of an unwrapped schema.+--+-- Also runs the `unwrap` quasiquoter at runtime, to get coverage information.+unwrapRep :: QuasiQuoter+unwrapRep = mkExpQQ $ \s ->+  let showSchemaResultQ = appTypeE [| showSchemaResult |] (quoteType unwrap s)+  in [| runTestQ qState (quoteType unwrap s) `deepseq` $showSchemaResultQ |]++unwrapErr :: QuasiQuoter+unwrapErr = mkExpQQ $ \s -> [| runTestQErr qState (quoteType unwrap s) |]
− test/Util.hs
@@ -1,54 +0,0 @@-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE TypeApplications #-}--module Util where--import Control.Exception (SomeException, try)-import Control.Monad ((<=<))-import Data.Aeson (eitherDecode)-import qualified Data.ByteString.Lazy as ByteStringL-import qualified Data.ByteString.Lazy.Char8 as Char8L-import Data.List (isPrefixOf)-import Language.Haskell.TH-import Language.Haskell.TH.Quote (QuasiQuoter(..))-import Language.Haskell.TH.Syntax (lift)--import Data.Aeson.Schema (Object, get, schema, unwrap)-import qualified Data.Aeson.Schema.Internal as Internal--getMockedResult :: FilePath -> ExpQ-getMockedResult fp = do-  contents <- runIO $ ByteStringL.readFile fp-  [| either error id $ eitherDecode $ Char8L.pack $(lift $ Char8L.unpack contents) |]---- | Show the expression generated by passing the given string to the 'get' quasiquoter.-showGet :: String -> ExpQ-showGet = lift . pprint <=< quoteExp get---- | Show the type generated by passing the given string to the 'unwrap' quasiquoter.-showUnwrap :: String -> ExpQ-showUnwrap = showType <=< quoteType unwrap---- | Show the type generated by passing the given string to the 'schema' quasiquoter.-showSchema :: String -> ExpQ-showSchema = showSchemaType <=< quoteType schema--showType :: Type -> ExpQ-showType = \case-  AppT (ConT name) schema' | name == ''Object -> [| "Object (" ++ $(showSchemaType schema') ++ ")" |]-  ty -> lift $ pprint ty--showSchemaType :: Type -> ExpQ-showSchemaType = appTypeE [| Internal.showSchema |] . pure---- | Return the 'error' message thrown when evaluating the given expresssion.-getError :: a -> ExpQ-getError x = runIO (try $ x `seq` pure ()) >>= \case-  Right _ -> fail "'getError' expression unexpectedly succeeded"-  Left e -> lift . unlines . stripCallStack . lines . show $ (e :: SomeException)-  where-    stripCallStack [] = []-    stripCallStack (l:ls) = if "CallStack" `isPrefixOf` l-      then []-      else l : stripCallStack ls
− test/all_types.json
@@ -1,47 +0,0 @@-{-    "bool": true,-    "int": 1,-    "int2": 2,-    "double": 2.5,-    "text": "asdf",-    "scalar": "12,34",-    "enum": "HELLO",-    "maybeObject": {-        "text": "foo"-    },-    "maybeObjectNull": null,-    "maybeList": [-        {-            "text": "foo"-        },-        {-            "text": "bar"-        }-    ],-    "maybeListNull": null,-    "tryObject": {-        "a": 1-    },-    "tryObjectNull": true,-    "list": [-        {-            "type": "bool",-            "maybeBool": true-        },-        {-            "type": "int",-            "maybeInt": 2-        },-        {-            "type": "null",-            "maybeNull": null-        }-    ],-    "union": [-        "Hello",-        { "a": 1 },-        [true, false],-        "World!"-    ],-    "keyForPhantom": 1-}
test/goldens/README_Quickstart.golden view
@@ -2,7 +2,7 @@ Details for user #1: * Name: Alice * Age: 30-* Groups: [{"id": 1, "name": "admin"}]+* Groups: [{ "id": 1, "name": "admin" }] Details for user #2: * Name: Bob * Age: N/A@@ -14,4 +14,4 @@ Details for user #4: * Name: Darlene * Age: 40-* Groups: [{"id": 2, "name": "groupA"},{"id": 3, "name": "groupB"}]+* Groups: [{ "id": 2, "name": "groupA" },{ "id": 3, "name": "groupB" }]
− test/goldens/bool.golden
@@ -1,1 +0,0 @@-True
− test/goldens/bool_int_double.golden
@@ -1,1 +0,0 @@-(True,1,2.5)
− test/goldens/double.golden
@@ -1,1 +0,0 @@-2.5
− test/goldens/enum.golden
@@ -1,1 +0,0 @@-HELLO
− test/goldens/from_object_all_types.golden
@@ -1,1 +0,0 @@-["True","2","Nothing"]
− test/goldens/from_object_namespaced.golden
@@ -1,1 +0,0 @@-["bool","int","null","1","3"]
− test/goldens/from_object_nested.golden
@@ -1,1 +0,0 @@-[1,2]
+ test/goldens/fromjson_error_messages_truncate.golden view
@@ -0,0 +1,1 @@+Error in $: Could not parse path 'foo' with schema `SchemaScalar Int`: Array [Object (fromList [("baz",String "a"),("bar",Number 1.0)]),Object (fromList [("baz",String "b"),("bar",Number 2.0)]),Object (fromList [("baz",String "c"),("bar",Number 3.0)]),Object (fromList [(...
+ test/goldens/fromjson_list_inner_invalid.golden view
@@ -0,0 +1,1 @@+Error in $: Could not parse path 'foo' with schema `SchemaScalar Double`: Bool True
+ test/goldens/fromjson_list_invalid.golden view
@@ -0,0 +1,1 @@+Error in $: Could not parse path 'foo' with schema `SchemaList Double`: Bool True
+ test/goldens/fromjson_maybe_invalid.golden view
@@ -0,0 +1,1 @@+Error in $: Could not parse path 'foo' with schema `SchemaScalar Int`: Bool True
+ test/goldens/fromjson_nested_inner_invalid.golden view
@@ -0,0 +1,1 @@+Error in $: Could not parse path 'foo.bar' with schema `SchemaScalar Int`: Bool True
+ test/goldens/fromjson_nested_invalid.golden view
@@ -0,0 +1,1 @@+Error in $: Could not parse path 'foo' with schema `SchemaObject { "bar": Int }`: Bool True
+ test/goldens/fromjson_object_invalid.golden view
@@ -0,0 +1,1 @@+Error in $: Could not parse schema `SchemaObject { "foo": Int }`: Number 1.0
+ test/goldens/fromjson_object_later_keys_invalid.golden view
@@ -0,0 +1,1 @@+Error in $: Could not parse path 'bar' with schema `SchemaScalar Int`: Bool True
+ test/goldens/fromjson_phantom_inner_invalid.golden view
@@ -0,0 +1,1 @@+Error in $: Could not parse path 'foo.bar' with schema `SchemaScalar Int`: Bool True
+ test/goldens/fromjson_phantom_inner_missing.golden view
@@ -0,0 +1,1 @@+Error in $: Could not parse path 'foo.bar' with schema `SchemaScalar Int`: Null
+ test/goldens/fromjson_phantom_invalid.golden view
@@ -0,0 +1,1 @@+Error in $: Could not parse schema `SchemaObject { [foo]: { "bar": Int } }`: Number 1.0
+ test/goldens/fromjson_scalar_invalid.golden view
@@ -0,0 +1,1 @@+Error in $: Could not parse path 'foo' with schema `SchemaScalar Text`: Number 1.0
+ test/goldens/fromjson_union_invalid.golden view
@@ -0,0 +1,1 @@+Error in $: Could not parse path 'foo' with schema `SchemaUnion ( Int | Text )`: Bool True
− test/goldens/get_empty.golden
@@ -1,6 +0,0 @@-1:1:-  |-1 | <empty line>-  | ^-unexpected end of input-expecting "[]", '!', '(', '.', '?', '@', '[', lowercase letter, or white space
− test/goldens/get_just_start.golden
@@ -1,6 +0,0 @@-allTypes:1:9:-  |-1 | allTypes-  |         ^-unexpected end of input-expecting "[]", '!', ''', '(', '.', '?', '@', '[', or alphanumeric character
− test/goldens/get_ops_after_list.golden
@@ -1,1 +0,0 @@-.[*] operation MUST be last.
− test/goldens/get_ops_after_tuple.golden
@@ -1,1 +0,0 @@-.(*) operation MUST be last.
+ test/goldens/getqq_empty_expression.golden view
@@ -0,0 +1,5 @@+ :1:2:+  |+1 |  +  |  ^+expecting "[]", '!', '"', '(', '.', '?', '@', '[', '\', lowercase letter, or white space
+ test/goldens/getqq_missing_key.golden view
@@ -0,0 +1,12 @@+test/wont-compile/GetMissingKey.hs:16:1: error:+    • Key 'missing' does not exist in the following schema:+      '[ '( 'Data.Aeson.Schema.Key.NormalKey "foo",+            'Data.Aeson.Schema.Type.SchemaScalar Bool)]+    • When checking the inferred type+        result :: Data.Aeson.Schema.Internal.SchemaResult (TypeError ...)+test/wont-compile/GetMissingKey.hs:16:1: error:+    • Key 'missing' does not exist in the following schema:+      '[ '( 'Data.Aeson.Schema.Key.NormalKey "foo",+            'Data.Aeson.Schema.Type.SchemaScalar Bool)]+    • When checking the inferred type+        result :: Data.Aeson.Schema.Internal.SchemaResult (TypeError ...)
+ test/goldens/getqq_no_operators.golden view
@@ -0,0 +1,5 @@+ o :1:3:+  |+1 |  o +  |   ^+expecting "[]", '!', '"', ''', '(', '.', '?', '@', '[', '\', or alphanumeric character
+ test/goldens/getqq_ops_after_list.golden view
@@ -0,0 +1,6 @@+ o.[a,b].foo :1:9:+  |+1 |  o.[a,b].foo +  |         ^+unexpected '.'+expecting end of input or white space
+ test/goldens/getqq_ops_after_tuple.golden view
@@ -0,0 +1,6 @@+ o.(a,b).foo :1:9:+  |+1 |  o.(a,b).foo +  |         ^+unexpected '.'+expecting end of input or white space
− test/goldens/getter_all_types_list.golden
@@ -1,1 +0,0 @@-[{"type": "bool", "maybeBool": Just True, "maybeInt": Nothing, "maybeNull": Nothing},{"type": "int", "maybeBool": Nothing, "maybeInt": Just 2, "maybeNull": Nothing},{"type": "null", "maybeBool": Nothing, "maybeInt": Nothing, "maybeNull": Nothing}]
− test/goldens/getter_all_types_list_item.golden
@@ -1,1 +0,0 @@-["bool","int","null"]
− test/goldens/int.golden
@@ -1,1 +0,0 @@-1
− test/goldens/int_int2.golden
@@ -1,1 +0,0 @@-[1,2]
− test/goldens/lambda_bool.golden
@@ -1,1 +0,0 @@-True
− test/goldens/list.golden
@@ -1,1 +0,0 @@-[{"type": "bool", "maybeBool": Just True, "maybeInt": Nothing, "maybeNull": Nothing},{"type": "int", "maybeBool": Nothing, "maybeInt": Just 2, "maybeNull": Nothing},{"type": "null", "maybeBool": Nothing, "maybeInt": Nothing, "maybeNull": Nothing}]
− test/goldens/list_explicit.golden
@@ -1,1 +0,0 @@-[{"type": "bool", "maybeBool": Just True, "maybeInt": Nothing, "maybeNull": Nothing},{"type": "int", "maybeBool": Nothing, "maybeInt": Just 2, "maybeNull": Nothing},{"type": "null", "maybeBool": Nothing, "maybeInt": Nothing, "maybeNull": Nothing}]
− test/goldens/list_maybeBool.golden
@@ -1,1 +0,0 @@-[Just True,Nothing,Nothing]
− test/goldens/list_maybeInt.golden
@@ -1,1 +0,0 @@-[Nothing,Just 2,Nothing]
− test/goldens/list_type.golden
@@ -1,1 +0,0 @@-["bool","int","null"]
− test/goldens/maybeList.golden
@@ -1,1 +0,0 @@-Just [{"text": "foo"},{"text": "bar"}]
− test/goldens/maybeListNull.golden
@@ -1,1 +0,0 @@-Nothing
− test/goldens/maybeListNull_bang.golden
@@ -1,1 +0,0 @@-Called 'fromJust' on null expression: (AllTypes.result).maybeListNull
− test/goldens/maybeListNull_list.golden
@@ -1,1 +0,0 @@-Nothing
− test/goldens/maybeListNull_list_text.golden
@@ -1,1 +0,0 @@-Nothing
− test/goldens/maybeList_bang.golden
@@ -1,1 +0,0 @@-[{"text": "foo"},{"text": "bar"}]
− test/goldens/maybeList_bang_list.golden
@@ -1,1 +0,0 @@-[{"text": "foo"},{"text": "bar"}]
− test/goldens/maybeList_bang_list_text.golden
@@ -1,1 +0,0 @@-["foo","bar"]
− test/goldens/maybeList_list.golden
@@ -1,1 +0,0 @@-Just [{"text": "foo"},{"text": "bar"}]
− test/goldens/maybeList_list_text.golden
@@ -1,1 +0,0 @@-Just ["foo","bar"]
− test/goldens/maybeObj.golden
@@ -1,1 +0,0 @@-Just {"text": "foo"}
− test/goldens/maybeObjNull.golden
@@ -1,1 +0,0 @@-Nothing
− test/goldens/maybeObjNull_text.golden
@@ -1,1 +0,0 @@-Nothing
− test/goldens/maybeObj_bang.golden
@@ -1,1 +0,0 @@-{"text": "foo"}
− test/goldens/maybeObj_bang_text.golden
@@ -1,1 +0,0 @@-"foo"
− test/goldens/maybeObj_text.golden
@@ -1,1 +0,0 @@-Just "foo"
− test/goldens/nonexistent.golden
@@ -1,1 +0,0 @@-Nothing
− test/goldens/phantom.golden
@@ -1,1 +0,0 @@-1
− test/goldens/scalar.golden
@@ -1,1 +0,0 @@-Coordinate (12,34)
− test/goldens/schema_def_bool.golden
@@ -1,1 +0,0 @@-SchemaObject {"a": Bool}
− test/goldens/schema_def_custom.golden
@@ -1,1 +0,0 @@-SchemaObject {"status": Status}
− test/goldens/schema_def_double.golden
@@ -1,1 +0,0 @@-SchemaObject {"foo123": Double}
− test/goldens/schema_def_duplicate.golden
@@ -1,1 +0,0 @@-Key 'a' specified multiple times
− test/goldens/schema_def_duplicate_extend.golden
@@ -1,1 +0,0 @@-Key 'extra' declared in multiple imported schemas
− test/goldens/schema_def_duplicate_phantom.golden
@@ -1,1 +0,0 @@-Key 'a' specified multiple times
− test/goldens/schema_def_extend.golden
@@ -1,1 +0,0 @@-SchemaObject {"a": Int, "extra": Text}
− test/goldens/schema_def_import_user.golden
@@ -1,1 +0,0 @@-SchemaObject {"user": {"name": Text}}
− test/goldens/schema_def_int.golden
@@ -1,1 +0,0 @@-SchemaObject {"a": Int}
− test/goldens/schema_def_invalid_extend.golden
@@ -1,1 +0,0 @@-'GHC.Types.Int' is not a SchemaObject
− test/goldens/schema_def_list.golden
@@ -1,1 +0,0 @@-SchemaObject {"a": List Int}
− test/goldens/schema_def_list_obj.golden
@@ -1,1 +0,0 @@-SchemaObject {"a": List {"b": Int}}
− test/goldens/schema_def_maybe.golden
@@ -1,1 +0,0 @@-SchemaObject {"a": Maybe Int}
− test/goldens/schema_def_maybe_obj.golden
@@ -1,1 +0,0 @@-SchemaObject {"a": Maybe {"b": Int}}
− test/goldens/schema_def_nonobject_phantom.golden
@@ -1,1 +0,0 @@-Invalid schema for 'a': SchemaInt
− test/goldens/schema_def_not_object.golden
@@ -1,1 +0,0 @@-`schema` definition must be an object
− test/goldens/schema_def_obj.golden
@@ -1,1 +0,0 @@-SchemaObject {"a": {"b": Int}}
− test/goldens/schema_def_shadow.golden
@@ -1,1 +0,0 @@-SchemaObject {"extra": Bool}
− test/goldens/schema_def_text.golden
@@ -1,1 +0,0 @@-SchemaObject {"some_text": Text}
− test/goldens/schema_def_union.golden
@@ -1,1 +0,0 @@-SchemaObject {"a": ( List Int | Text )}
− test/goldens/schema_def_union_grouped.golden
@@ -1,1 +0,0 @@-SchemaObject {"a": List ( Int | Text )}
− test/goldens/schema_def_unknown_type.golden
@@ -1,1 +0,0 @@-`schema` definition must be an object
+ test/goldens/schemaqq_key_with_invalid_character.golden view
@@ -0,0 +1,6 @@+ { "a:b": Int } :1:6:+  |+1 |  { "a:b": Int } +  |      ^+unexpected ':'+expecting '"' or '\'
+ test/goldens/schemaqq_key_with_trailing_escape.golden view
@@ -0,0 +1,6 @@+ { "a\": Int } :1:8:+  |+1 |  { "a\": Int } +  |        ^+unexpected ':'+expecting '"' or '\'
+ test/goldens/sumtype_decode_invalid.golden view
@@ -0,0 +1,1 @@+Error in $: Could not parse sum type
− test/goldens/text.golden
@@ -1,1 +0,0 @@-"asdf"
− test/goldens/tryObj.golden
@@ -1,1 +0,0 @@-Just {"a": 1}
− test/goldens/tryObjNull.golden
@@ -1,1 +0,0 @@-Nothing
− test/goldens/tryObjNull_a.golden
@@ -1,1 +0,0 @@-Nothing
− test/goldens/tryObj_a.golden
@@ -1,1 +0,0 @@-Just 1
− test/goldens/tryObj_bang.golden
@@ -1,1 +0,0 @@-{"a": 1}
− test/goldens/tryObj_bang_a.golden
@@ -1,1 +0,0 @@-1
− test/goldens/union.golden
@@ -1,1 +0,0 @@-[There (There (Here "Hello")),Here {"a": 1},There (Here [True,False]),There (There (Here "World!"))]
− test/goldens/union_0.golden
@@ -1,1 +0,0 @@-[Nothing,Just {"a": 1},Nothing,Nothing]
− test/goldens/union_0_a.golden
@@ -1,1 +0,0 @@-[Nothing,Just 1,Nothing,Nothing]
− test/goldens/union_1.golden
@@ -1,1 +0,0 @@-[Nothing,Nothing,Just [True,False],Nothing]
− test/goldens/union_2.golden
@@ -1,1 +0,0 @@-[Just "Hello",Nothing,Nothing,Just "World!"]
− test/goldens/unwrap_schema.golden
@@ -1,1 +0,0 @@-Object (SchemaObject {"a": Maybe {"b": Int}, "b": Int})
− test/goldens/unwrap_schema_bad_bang.golden
@@ -1,1 +0,0 @@-Cannot use `!` operator on schema: SchemaObject {"type": Text, "maybeBool": Maybe Bool, "maybeInt": Maybe Int, "maybeNull": Maybe Bool}
− test/goldens/unwrap_schema_bad_branch.golden
@@ -1,1 +0,0 @@-Cannot use `@` operator on schema: SchemaList {"type": Text, "maybeBool": Maybe Bool, "maybeInt": Maybe Int, "maybeNull": Maybe Bool}
− test/goldens/unwrap_schema_bad_key.golden
@@ -1,1 +0,0 @@-Cannot get key 'a' in schema: SchemaList {"type": Text, "maybeBool": Maybe Bool, "maybeInt": Maybe Int, "maybeNull": Maybe Bool}
− test/goldens/unwrap_schema_bad_list.golden
@@ -1,1 +0,0 @@-Cannot use `[]` operator on schema: SchemaObject {"type": Text, "maybeBool": Maybe Bool, "maybeInt": Maybe Int, "maybeNull": Maybe Bool}
− test/goldens/unwrap_schema_bad_question.golden
@@ -1,1 +0,0 @@-Cannot use `?` operator on schema: SchemaObject {"type": Text, "maybeBool": Maybe Bool, "maybeInt": Maybe Int, "maybeNull": Maybe Bool}
− test/goldens/unwrap_schema_branch_out_of_bounds.golden
@@ -1,1 +0,0 @@-Branch out of bounds for schema: SchemaUnion ( {"a": Int} | List Bool | Text )
− test/goldens/unwrap_schema_nested_list.golden
@@ -1,1 +0,0 @@-[{"a": Nothing, "b": 1},{"a": Just {"b": 2}, "b": 3}]
− test/goldens/unwrap_schema_nested_object.golden
@@ -1,1 +0,0 @@-[1,2]
+ test/goldens/unwrapqq_unwrap_past_list.golden view
@@ -0,0 +1,6 @@+ ABCSchema.[a,b].foo :1:17:+  |+1 |  ABCSchema.[a,b].foo +  |                 ^+unexpected '.'+expecting end of input or white space
+ test/goldens/unwrapqq_unwrap_past_tuple.golden view
@@ -0,0 +1,6 @@+ ABCSchema.(a,b).foo :1:17:+  |+1 |  ABCSchema.(a,b).foo +  |                 ^+unexpected '.'+expecting end of input or white space
− test/nested.json
@@ -1,14 +0,0 @@-{-    "list": [-        {-            "a": null,-            "b": 1-        },-        {-            "a": {-                "b": 2-            },-            "b": 3-        }-    ]-}