curl: update test for latest changes
[nit.git] / lib / serialization / README.md
1 # Abstract serialization services
2
3 The serialization services are based on the `serialize` and the `noserialize` annotations,
4 the `Serializable` interface and the implementations of `Serializer` and `Deserializer`.
5
6 ## The `serialize` annotation
7
8 A class annotated with `serialize` identifies it as a subclass of Serializable and
9 triggers the generation of customized serialization and deserialization services.
10
11 ~~~
12 import serialization
13
14 # Simple serializable class identifying a human
15 class Person
16         serialize
17
18         # First and last name
19         var name: String
20
21         # Year of birth (`null` if unknown)
22         var birth: nullable Int
23
24         redef fun ==(o) do return o isa SELF and name == o.name and birth == o.birth
25         redef fun hash do return name.hash
26 end
27 ~~~
28
29 The `Person` class also defines `==` and `hash`, this is optional but we will use it to make an important point.
30 By definition of a serializable class, an instance can be serialized to a stream, then deserialized.
31 The deserialized instance will not be the same instance, but they should be equal.
32 So, in this case, we can compare both instances with `==` to test their equality.
33
34 Some conditions applies to the classes that can be annotated as `serialize`.
35 All attributes of the class must be serializable, runtime errors will be
36 raised when trying to serialize non-serializable attributes.
37
38 In the class `Person`, all attributes are typed with classes the standards library.
39 These common types are defined defined as serializable by this project.
40 The attributes could also be typed with user-defined `serialize`
41 classes or any other subclass of `Serializable`.
42
43 ~~~
44 # This `serialize` class is composed of two `serialize` attributes
45 class Partnership
46         serialize
47
48         var partner_a: Person
49         var partner_b: Person
50
51         redef fun ==(o) do return o isa SELF and partner_a == o.partner_a and partner_b == o.partner_b
52         redef fun hash do return partner_a.hash + 1024*partner_b.hash
53 end
54 ~~~
55
56 ### Scope of the `serialize` annotation
57
58 `serialize` can annotate class definitions, modules and attributes:
59
60 * The annotation on a class applies only to the class definition,
61   only attributes declared locally will be serialized.
62   However, each definition of a class (a refinement or specialization) can be annotated with `serialize`.
63
64 * A module declaration annotated with `serialize` states that all its class definitions
65   and locally declared attributes are serializable.
66
67   ~~~
68   module shared_between_clients is serialize
69   ~~~
70
71 * Attribute annotated with `serialize` states that it is to be serialized, when the rest of the class does not.
72   The class will become subclass to `Serializable` but its attributes are not to be serialized by default.
73   Only the attributes with the `serialize` annotation will be serialized.
74
75   ~~~
76   # Only serialize the `name`
77   class UserCredentials
78       var name: String is serialize
79       var avatar_path: String = "/somepath/"+name is lazy
80   end
81   ~~~
82
83 ## The `noserialize` annotation
84
85 The `noserialize` annotation mark an exception in a `serialize` module or class definition.
86
87 * By default a module is `noserialize`. There is no need to declare it as such.
88
89 * A class definition annotated with `noserialize` within a `serialize` module will not be made serializable.
90
91 * A `noserialize` attribute within a class or module annotated with `serialize` will not serialize this attribute.
92   The class will still be made subclass of `Serializable` and it won't affect the other attributes.
93   The `noserialize` attribute will not be set at deserialization.
94   Usually, it will also be annotated with `lazy` to get its value by another mean after the object has been deserialized.
95
96   ~~~
97   # Once again, only serialize the `name`
98   class UserCredentials
99       serialize
100
101       var name: String
102       var avatar_path: String = "/somepath/"+name is noserialize, lazy
103   end
104   ~~~
105
106 ## The `serialize_as` annotation
107
108 By default, an attribute is identified in the serialization format by its Nit name.
109 The `serialize_as` attribute changes this behavior and sets the name of an attribute in the serialization format.
110
111 This annotation can be useful to change the name of an attribute to what is expected by a remote service.
112 Or to use identifiers in the serialization format that are reserved keywords in Nit (like `class` and `type`).
113
114 ~~~
115 class UserCredentials
116         serialize
117
118         # Rename to "username" in JSON for compatibility with remote service
119         var name: String is serialize_as "username"
120
121         # Rename to a shorter "ap" for a smaller JSON file
122         var avatar_path: String = "/somepath/"+name is lazy, serialize_as "ap"
123 end
124 ~~~
125
126 ## Custom serializable classes
127
128 The annotation `serialize` should be enough for most cases,
129 but in some cases you need more control over the serialization process.
130
131 For more control, create a subclass to `Serializable` and redefine `core_serialize_to`.
132 This method should use `Serializer::serialize_attribute` to serialize its components.
133 `serialize_attribute` works as a dictionary and organize attributes with a key.
134
135 You will also need to redefine `Deserializer::deserialize_class` to support this specific class.
136 The method should only act on known class names, and call super otherwise.
137
138 ### Example: the User class
139
140 The following example cannot use the `serialize` annotations
141 because some of the arguments to the `User` class need special treatment:
142
143 * The `name` attribute is perfectly normal, it can be serialized and deserialized
144   directly.
145
146 * The `password` attribute must be encrypted before being serialized,
147   and unencrypted on deserialization.
148
149 * The `avatar` attributes is kept as ASCII art in memory.
150   It could be serialized as such but it is cleaner to only
151   serialize the path to its source on the file system.
152   The data is reloaded on deserialization.
153
154 For this customization, the following code snippet implements
155 two serialization services: `User::core_serialize_to` and
156 `Deserializer::deserialize_class`.
157
158 ~~~
159 module user_credentials
160
161 # User credentials for a website
162 class User
163         super Serializable
164
165         # User name
166         var name: String
167
168         # Clear text password
169         var password: String
170
171         # User's avatar image as data blob
172         var avatar: Image
173
174         redef fun core_serialize_to(serializer: Serializer)
175         do
176                 # This is the normal serialization process
177                 serializer.serialize_attribute("name", name)
178
179                 # Serialized an encrypted version of the password
180                 #
181                 # Obviously, `rot(13)` is not a good encrption
182                 serializer.serialize_attribute("pass", password.rot(13))
183
184                 # Do not serialize the image, only its path
185                 serializer.serialize_attribute("avatar_path", avatar.path)
186         end
187 end
188
189 redef class Deserializer
190         redef fun deserialize_class(name)
191         do
192                 if name == "User" then
193                         # Deserialize normally
194                         var user = deserialize_attribute("name")
195
196                         # Decrypt password
197                         var pass = deserialize_attribute("pass").rot(-13)
198
199                         # Deserialize the path and load the avatar from the file system
200                         var avatar_path = deserialize_attribute("avatar_path")
201                         var avatar = new Image(avatar_path)
202
203                         return new User(user, pass, avatar)
204                 end
205
206                 return super
207         end
208 end
209
210 # An image loaded in memory as ASCII art
211 #
212 # Not really useful for this example, provided for consistency only.
213 class Image
214         # Path on the filesystem for `self`
215         var path: String
216
217         # ASCII art composing this image
218         var ascii_art: String = path.read_all is lazy
219 end
220
221 ~~~
222
223 See the documentation of the module `serialization::serialization` for more
224 information on the services to redefine.
225
226 ## Serialization services
227
228 The `serialize` annotation and the `Serializable` class are used on
229 classes specific to the business domain.
230 To write (and read) instances of these classes to a persistent format
231 you must use implementations of `Serializer` and `Deserializer`.
232
233 The main implementations of these services are `JsonSerializer` and `JsonDeserializer`,
234 from the `json_serialization` module.
235
236 ~~~
237 import json
238 import user_credentials
239
240 # Data to be serialized and deserialized
241 var couple = new Partnership(
242         new Person("Alice", 1985, new Image("alice.png")),
243         new Person("Bob", null, new Image("bob.png")))
244
245 var path = "serialized_data.json"
246 var writer = new FileWriter(path)
247 var serializer = new JsonSerializer(writer)
248 serializer.serialize couple
249 writer.close
250
251 var reader = new FileReader(path)
252 var deserializer = new JsonDeserializer(reader.to_s)
253 var deserialized_couple = deserializer.deserialize
254 reader.close
255
256 assert couple == deserialize_couple
257 ~~~
258
259 ## Limitations and TODO
260
261 The serialization has some limitations:
262
263 * A limitation of the JSON parser prevents deserializing from files
264   with more than one object.
265   This could be improved in the future, but for now you should
266   serialize a single object to each files and use different instances of
267   serializer and deserializer each time.
268
269 * The serialization uses only the short name of a class, not its qualified name.
270   This will cause problem when different classes using the same name.
271   This could be solved partially in the compiler and the library.
272   A special attention must be given to the consistency of the name across
273   the different programs sharing the serialized data.
274
275 * The serialization support in the compiler need some help to
276   deal with generic types. A solution is to use `nitserial`,
277   the next section explores this subject.
278
279 ## Dealing with generic types
280
281 One limitation of the serialization support in the compiler is with generic types.
282 For example, the `Array` class is generic and serializable.
283 However, the runtime types of Array instances are parameterized and are unknown to the compiler.
284 So the compiler won't support serializing instances of `Array[MySerializable]`.
285
286 The tool `nitserial` solves this problem at the level of user modules.
287 It does so by parsing a Nit module, group or project to find all known
288 parameterized types of generic classes.
289 It will then generating a Nit module to handle deserialization of these types.
290
291 Usage steps to serialize parameterized types:
292
293 * Write your program, let's call it `my_prog.nit`,
294   it must use some parameterized serializable types.
295   Let's say that you use `Array[MySerializable]`.
296
297 * Run nitserial using `nitserial my_prog.nit` to
298   generate the file `my_prog_serial.nit`.
299
300 * Compile your program by mixing in the generated module with:
301   `nitc my_prog.nit -m my_prog_serial.nit`
302
303 This was a simple example, in practical cases you may need
304 to use more than one generated file.
305 For example, on a client/server system, an instance can be created
306 server-side, serialized and the used client-side.
307 In this case, two files will be generated by nitserial,
308 one for the server and one for the client.
309 Both the files should be compiled with both the client and the server.