@@ -22,9 +22,35 @@ Be aware that library is experimental and subject to change a lot. The current s
22$ stack install schematic
22$ stack install schematic
25−## Basic Examples
25+## GHC Extensions
27+To use this library without any hassle, you should add a few GHC extension
28+either to a module or a cabal file:
37+`Overloaded`-extensions are being used only by `field` combinator,
38+so it you don't use it - feel free to disable it.
42+I recommend using `{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}`
43+pragma in modules which declare schemas and schema migrations, because `schematic`
44+heavily uses promoted types which should be prefixed with "`" signs and they can
45+be dropped with this option, making it less noisy visually.
46+It's entirely optional and totally opinionated, so feel free to ignore this.
47+Through the documentation, the code is written without any explicit ticks for
49+Notice that they're still needed for type-level lists and tuples in schematic code.
28type SchemaExample
54type SchemaExample
29 = SchemaObject
55 = SchemaObject
30 '[ '("foo", SchemaArray '[AEq 1] (SchemaNumber '[NGt 10]))
56 '[ '("foo", SchemaArray '[AEq 1] (SchemaNumber '[NGt 10]))
@@ -42,21 +68,21 @@ type SchemaExample
43This one is valid for example
69This one is valid for example
46schemaJson :: ByteString
72schemaJson :: ByteString
47schemaJson = "{\"foo\": [13], \"bar\": null}"
73schemaJson = "{\"foo\": [13], \"bar\": null}"
53schemaJson :: ByteString
79schemaJson :: ByteString
54schemaJson = "{\"foo\": [13], \"bar\": \"bar\"}"
80schemaJson = "{\"foo\": [13], \"bar\": \"bar\"}"
57it can be parsed and validated like this:
83it can be parsed and validated like this:
60decodeAndValidateJson schemaJson :: ParseResult (JsonRepr SchemaExample)
86decodeAndValidateJson schemaJson :: ParseResult (JsonRepr SchemaExample)
@@ -66,24 +92,64 @@ decodeAndValidateJson schemaJson :: ParseResult (JsonRepr SchemaExample)
66* some of the runtime validations have failed, but it's structurally correct
92* some of the runtime validations have failed, but it's structurally correct
69−It implies a transport layer representation of that data that can be traversed and transformed to whatever internal types user has. It's called `JsonRepr`. Type parameter is the schema itself.
95+It implies a transport layer representation of that data that can be traversed
96+and transformed to whatever internal types user has.
97+It's called `JsonRepr`. Type parameter is the schema itself.
100+jsonExample :: JsonRepr SchemaExample
101+jsonExample = withRepr @SchemaExample
103+ :& field @"bar" (Just "bar")
107+The most basic and sugar-free way of constructing the same object will look like this:
73−jsonExample :: JsonRepr SchemaExample
110+jsonExample' :: JsonRepr SchemaExample
74−jsonExample = ReprObject $
111+jsonExample' = ReprObject $
75 FieldRepr (ReprArray [ReprNumber 12])
112 FieldRepr (ReprArray [ReprNumber 12])
76 :& FieldRepr (ReprOptional (Just (ReprText "bar")))
113 :& FieldRepr (ReprOptional (Just (ReprText "bar")))
117+## Lens-compatibility
119+It's possible to use `flens` to construct a field lens for a typed object in schematic. Let's suppose you have a schema like this:
122+type ArraySchema = SchemaArray '[AEq 1] (SchemaNumber '[NGt 10])
124+type ArrayField = '("foo", ArraySchema)
127+ '[ ArrayField, '("bar", SchemaOptional (SchemaText '[TEnum '["foo", "bar"]]))]
129+type SchemaExample = 'SchemaObject FieldsSchema
133+There are two ways of working with named fields in the objects:
135+* Using the `fget` and `fset` functions: `fget fooProxy (fput newFooVal objectData) == newFooVal`
136+* Using the lens library: `set (flens (Proxy @"foo")) newFooVal objectData ^. flens (Proxy @"foo") == newFooVal`
80## Migrations
138## Migrations
140+Usually migrations are implicit and hard to deal with, `schematic` deals with it
141+by giving tools to deal with it to a programmer, being as explicit as possible,
142+notifying the developer of a missing transition with a compile-time error.
143+It allows to build versioned HTTP APIs or migrate data from the older versions
144+when reading from the JSON storages.
146+It's possible to represent schema changes as a series of migrations,
82−It's possible to represent schema changes as a series of migrations, which describes a series of json-path/change pairs. Migrations can be applied in succession.
147+which describes a series of json-path/change pairs. Migrations can be applied in
84This piece of code will apply a migration to the schema, decode and validate the latest version.
150This piece of code will apply a migration to the schema, decode and validate the latest version.
87type SchemaExample
153type SchemaExample
88 = SchemaObject
154 = SchemaObject
89 '[ '("foo", SchemaArray '[AEq 1] (SchemaNumber '[NGt 10]))
155 '[ '("foo", SchemaArray '[AEq 1] (SchemaNumber '[NGt 10]))
@@ -91,8 +157,8 @@ type SchemaExample
92type TestMigration =
158type TestMigration =
93 'Migration "test_revision"
159 'Migration "test_revision"
94− '[ 'Diff '[ 'PKey "bar" ] ('Update ('SchemaText '[]))
160+ '[ Diff '[ PKey "bar" ] (Update (SchemaText '[]))
95− , 'Diff '[ 'PKey "foo" ] ('Update ('SchemaNumber '[])) ]
161+ , Diff '[ PKey "foo" ] (Update (SchemaNumber '[])) ]
97type VS = 'Versioned SchemaExample '[ TestMigration ]
163type VS = 'Versioned SchemaExample '[ TestMigration ]
@@ -106,25 +172,84 @@ It's possible to decode the latest version like this:
106decodeAndValidateJson schemaJsonTopVersion :: ParseResult (JsonRepr (TopVersion (AllVersions VS)))
172decodeAndValidateJson schemaJsonTopVersion :: ParseResult (JsonRepr (TopVersion (AllVersions VS)))
109−## Lens-compatibility
175+It's important to differentiate between schema migrations and data migrations.
176+Schema migration is a change of a schema acceptable by a JSON consumer. It transforms one schema to another.
177+Data migrations on the other hand are functions on the data itself. They transform values
178+acceptable by one schema to values acceptable by another.
179+Let's look at the situation when we have to add a field to a JSON Object of a current schema:
111−It's possible to use `flens` to construct a field lens for a typed object in schematic. Let's suppose you have a schema like this:
181+```
182+type SchemaExample = 'SchemaObject
183+ '[ '("foo", 'SchemaArray '[ 'AEq 1] ('SchemaNumber '[ 'NGt 10]))
184+ , '("bar", 'SchemaOptional ('SchemaText '[ 'TEnum '["foo", "bar"]]))]
186+jsonExample :: JsonRepr SchemaExample
187+jsonExample = withRepr @SchemaExample
188+ $ field @"bar" (Just "bar")
189+ :& field @"foo" [12]
193+ 'Migration "add_field_quuz"
194+ '[ 'Diff '[] ('AddKey "quuz" (SchemaNumber '[])) ]
197+ 'Migration "remove_field_quuz"
198+ '[ 'Diff '[] ( 'DeleteKey "quuz") ]
200+type Migrations = '[ AddQuuz
203+type VersionedJson = 'Versioned SchemaExample Migrations
205+migrationList :: MigrationList Identity VersionedJson
207+ = (migrateObject (\r -> Identity $ field @"quuz" 42 :& r))
114−type ArraySchema = 'SchemaArray '[AEq 1] ('SchemaNumber '[NGt 10])
116−type ArrayField = '("foo", ArraySchema)
213+In this instance `Migrations` is a list of schema migrations and
214+`MigrationList Identity VersionedJson` is a list of data migrations corresponding
215+to the list of schema migrations: `migrateObject (\r -> Identity $ field @"quuz" 42 :& r))`
216+will be used at runtime to transform `JsonRepr SchemaExample` to
217+`JsonRepr (SchemaByRevision "add_quuz" VersionedJson). `Identity` in the type of
218+`MigrationList` is a monad we choose to run our migrations in. If there's need to
219+do database queries or something like that, it can be changed to something more
220+appropriate by a user.
221+`migrateObject` takes a function transforming old fields into new ones, `shrinkObject`
222+allows to shrink the JSON object in case migration only removed fields.
118−type FieldsSchema =
224+## How do I construct a value of `JsonRepr schema`?
119− '[ ArrayField, '("bar", 'SchemaOptional ('SchemaText '[TEnum '["foo", "bar"]]))]
121−type SchemaExample = 'SchemaObject FieldsSchema
226+`JsonRepr schema` is a primary representation of a value serializable/deserializable
227+to JSON, but with a twist. It's guaranteed to correspond a type-level schema,
228+it's a type parameter of a same name.
229+It makes constructing it a little bit more involved, but luckily `schematic`
230+provides a DSL for doing so in a more straightforward fashion. An example would
234+type SchemaExample = 'SchemaObject
235+ '[ '("foo", SchemaArray '[ AEq 1] (SchemaNumber '[NGt 10]))
236+ , '("bar", SchemaOptional (SchemaText '[TEnum '["foo", "bar"]]))]
238+jsonExample = withRepr @SchemaExample
239+ $ field @"bar" (Just "bar")
240+ :& field @"foo" [12]
125−There are two ways of working with named fields in the objects:
244+`@foo` syntax is an explicit type application, which is a feature of GHC 8+, it
245+makes type inference possible without relying on a bunch of `Proxy`s, which
246+makes it syntactically more terse.
248+[GHC Type Applications](https://ghc.haskell.org/trac/ghc/wiki/TypeApplication)
250+`schematic` provides instances for `OverloadedLists` and `OverloadedStrings`
251+GHC extensions to make use of string and list literals for values in a previous
127−* Using the `fget` and `fset` functions: `fget fooProxy (fput newFooVal objectData) == newFooVal`
128−* Using the lens library: `set (flens (Proxy @"foo")) newFooVal objectData ^. flens (Proxy @"foo") == newFooVal`
130## Export to json-schema (draft 4)
255## Export to json-schema (draft 4)