firm 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ # FIRM::Serializer - Ruby core serializer extensions
2
+ # Copyright (c) M.J.N. Corino, The Netherlands
3
+
4
+
5
+ module FIRM
6
+ module Serializable
7
+
8
+ # FIRM::Serializable is not included for the Ruby core classes as the would
9
+ # also extend these classes with the engine specific extension that we do not
10
+ # need nor want here.
11
+ # Instead we define the (slim) mixin module CoreExt to extend the non-POD core classes.
12
+ # POD classes (nil, boolean, integer, float) cannot be serialized separately but only
13
+ # as properties of complex serializables.
14
+ module CoreExt
15
+ def serialize(io = nil, pretty: false, format: FIRM::Serializable.default_format)
16
+ FIRM::Serializable[format].dump(self, io, pretty: pretty)
17
+ end
18
+
19
+ def self.included(base)
20
+ base.class_eval do
21
+ # Deserializes object from source data
22
+ # @param [IO,String] source source data (String or IO(-like object))
23
+ # @param [Symbol, String] format data format of source
24
+ # @return [Object] deserialized object
25
+ def self.deserialize(source, format: Serializable.default_format)
26
+ Serializable.deserialize(source, format: format)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ require 'set'
35
+ require 'ostruct'
36
+
37
+ [::Array, ::Hash, ::Struct, ::Range, ::Rational, ::Complex, ::Regexp, ::Set, ::OpenStruct, ::Time, ::Date, ::DateTime].each do |c|
38
+ c.include FIRM::Serializable::CoreExt
39
+ end
40
+
41
+ if ::Object.const_defined?(:BigDecimal)
42
+ ::BigDecimal.include FIRM::Serializable::CoreExt
43
+ end
@@ -0,0 +1,104 @@
1
+ # FIRM::Serializer - FIRM serializable ID class
2
+ # Copyright (c) M.J.N. Corino, The Netherlands
3
+
4
+
5
+ module FIRM
6
+
7
+ module Serializable
8
+
9
+ class ID
10
+
11
+ include FIRM::Serializable
12
+
13
+ class << self
14
+
15
+ # Deserializes object from source data
16
+ # @param [IO,String] source source data (String or IO(-like object))
17
+ # @param [Symbol, String] format data format of source
18
+ # @return [Object] deserialized object
19
+ def deserialize(source, format: Serializable.default_format)
20
+ Serializable.deserialize(source, format: format)
21
+ end
22
+
23
+ end
24
+
25
+ # Serialize this object
26
+ # @overload serialize(pretty: false, format: Serializable.default_format)
27
+ # @param [Boolean] pretty if true specifies to generate pretty formatted output if possible
28
+ # @param [Symbol,String] format specifies output format
29
+ # @return [String] serialized data
30
+ # @overload serialize(io, pretty: false, format: Serializable.default_format)
31
+ # @param [IO] io output stream to write serialized data to
32
+ # @param [Boolean] pretty if true specifies to generate pretty formatted output if possible
33
+ # @param [Symbol,String] format specifies output format
34
+ # @return [IO]
35
+ def serialize(io = nil, pretty: false, format: Serializable.default_format)
36
+ Serializable[format].dump(self, io, pretty: pretty)
37
+ end
38
+
39
+ # Initializes a newly allocated instance for subsequent deserialization (optionally initializing
40
+ # using the given data hash).
41
+ # The default implementation calls the standard #initialize method without arguments (default constructor)
42
+ # and leaves the property restoration to a subsequent call to the instance method #from_serialized(data).
43
+ # Classes that do not support a default constructor can override this class method and
44
+ # implement a custom initialization scheme.
45
+ # @param [Object] _data hash-like object containing deserialized property data (symbol keys)
46
+ # @return [Object] the initialized object
47
+ def init_from_serialized(_data)
48
+ initialize
49
+ self
50
+ end
51
+ protected :init_from_serialized
52
+
53
+ # Noop for ID instances.
54
+ # @param [Object] hash hash-like property serialization container
55
+ # @param [Set] _excludes ignored
56
+ # @return [Object] property hash-like serialization container
57
+ def for_serialize(hash, _excludes = nil)
58
+ hash
59
+ end
60
+
61
+ protected :for_serialize
62
+
63
+ # Noop for ID instances.
64
+ # @param [Hash] _hash ignored
65
+ # @return [self]
66
+ def from_serialized(_hash)
67
+ # no deserializing necessary
68
+ self
69
+ end
70
+
71
+ protected :from_serialized
72
+
73
+ # Noop for ID instances.
74
+ # @return [self]
75
+ def finalize_from_serialized
76
+ # no finalization necessary
77
+ self
78
+ end
79
+
80
+ protected :finalize_from_serialized
81
+
82
+ # Always returns false for IDs.
83
+ # @return [Boolean]
84
+ def serialize_disabled?
85
+ false
86
+ end
87
+
88
+ def to_s
89
+ "FIRM::Serializable::ID<#{object_id}>"
90
+ end
91
+
92
+ def inspect
93
+ to_s
94
+ end
95
+
96
+ def to_i
97
+ object_id
98
+ end
99
+
100
+ end
101
+
102
+ end
103
+
104
+ end
@@ -0,0 +1,394 @@
1
+ # FIRM::Serializer - shape serializer module
2
+ # Copyright (c) M.J.N. Corino, The Netherlands
3
+
4
+
5
+ require 'json'
6
+ require 'json/add/date'
7
+ require 'json/add/date_time'
8
+ require 'json/add/range'
9
+ require 'json/add/regexp'
10
+ require 'json/add/struct'
11
+ require 'json/add/symbol'
12
+ require 'json/add/time'
13
+ require 'json/add/bigdecimal' if ::Object.const_defined?(:BigDecimal)
14
+ require 'json/add/rational'
15
+ require 'json/add/complex'
16
+ require 'json/add/set'
17
+ require 'json/add/ostruct'
18
+
19
+ module FIRM
20
+
21
+ module Serializable
22
+
23
+ module JSON
24
+
25
+ # Derived Hash class to use for deserialized JSON object data which
26
+ # supports using Symbol keys.
27
+ class ObjectHash < ::Hash
28
+ # Returns the object associated with given key.
29
+ # @param [String,Symbol] key key value
30
+ # @return [Object] associated object
31
+ # @see ::Hash#[]
32
+ def [](key)
33
+ super(key.to_s)
34
+ end
35
+ # Returns true if the given key exists in self otherwise false.
36
+ # @param [String,Symbol] key key value
37
+ # @return [Boolean]
38
+ # @see ::Hash#include?
39
+ def include?(key)
40
+ super(key.to_s)
41
+ end
42
+ alias member? include?
43
+ alias has_key? include?
44
+ alias key? include?
45
+ end
46
+
47
+ # Mixin module to patch singleton_clas of the Hash class to make Hash-es
48
+ # JSON creatable (#json_creatable? returns true).
49
+ module HashClassPatch
50
+ # Create a new Hash instance from deserialized JSON data.
51
+ # @param [Hash] object deserialized JSON object
52
+ # @return [Hash] restored Hash instance
53
+ def json_create(object)
54
+ object['data'].to_h
55
+ end
56
+ end
57
+
58
+ class << self
59
+ def serializables
60
+ set = ::Set.new( [::NilClass, ::TrueClass, ::FalseClass, ::Integer, ::Float, ::String, ::Array, ::Hash,
61
+ ::Date, ::DateTime, ::Range, ::Rational, ::Complex, ::Regexp, ::Struct, ::Symbol, ::Time, ::Set, ::OpenStruct])
62
+ set << ::BigDecimal if ::Object.const_defined?(:BigDecimal)
63
+ set
64
+ end
65
+
66
+ TLS_SAFE_DESERIALIZE_KEY = :firm_json_safe_deserialize.freeze
67
+ private_constant :TLS_SAFE_DESERIALIZE_KEY
68
+
69
+ TLS_PARSE_STACK_KEY = :firm_json_parse_stack.freeze
70
+ private_constant :TLS_PARSE_STACK_KEY
71
+
72
+ def safe_deserialize
73
+ ::Thread.current[TLS_SAFE_DESERIALIZE_KEY] ||= []
74
+ end
75
+ private :safe_deserialize
76
+
77
+ def start_safe_deserialize
78
+ safe_deserialize.push(true)
79
+ end
80
+
81
+ def end_safe_deserialize
82
+ safe_deserialize.pop
83
+ end
84
+
85
+ def parse_stack
86
+ ::Thread.current[TLS_PARSE_STACK_KEY] ||= []
87
+ end
88
+ private :parse_stack
89
+
90
+ def start_parse
91
+ parse_stack.push(safe_deserialize.pop)
92
+ end
93
+
94
+ def end_parse
95
+ unless (val = parse_stack.pop).nil?
96
+ safe_deserialize.push(val)
97
+ end
98
+ end
99
+
100
+ def safe_parsing?
101
+ !!parse_stack.last
102
+ end
103
+ end
104
+
105
+ def self.dump(obj, io=nil, pretty: false)
106
+ # obj.extend(HashInstancePatch) if obj.is_a?(::Hash)
107
+ begin
108
+ # initialize anchor registry
109
+ Serializable::Aliasing.start_anchor_object_registry
110
+ for_json = obj.respond_to?(:as_json) ? obj.as_json : obj
111
+ if pretty
112
+ if io || io.respond_to?(:write)
113
+ io.write(::JSON.pretty_generate(for_json))
114
+ io
115
+ else
116
+ ::JSON.pretty_generate(for_json)
117
+ end
118
+ else
119
+ ::JSON.dump(for_json, io)
120
+ end
121
+ ensure
122
+ # reset anchor registry
123
+ Serializable::Aliasing.clear_anchor_object_registry
124
+ end
125
+ end
126
+
127
+ def self.load(source)
128
+ begin
129
+ # initialize alias anchor restoration map
130
+ Serializable::Aliasing.start_anchor_references
131
+ # enable safe deserializing
132
+ self.start_safe_deserialize
133
+ ::JSON.parse!(source,
134
+ **{create_additions: true,
135
+ object_class: Serializable::JSON::ObjectHash})
136
+ ensure
137
+ # reset safe deserializing
138
+ self.end_safe_deserialize
139
+ # reset alias anchor restoration map
140
+ Serializable::Aliasing.clear_anchor_references
141
+ end
142
+ end
143
+
144
+
145
+ # extend serialization class methods
146
+ module SerializeClassMethods
147
+
148
+ def json_create(object)
149
+ data = object['data']
150
+ # deserializing (anchor) object or alias
151
+ if data.has_key?('*id')
152
+ if Serializable::Aliasing.restored?(self, data['*id'])
153
+ # resolving an already restored anchor for this alias
154
+ Serializable::Aliasing.resolve_anchor(self, data['*id'])
155
+ else
156
+ # in case of cyclic references JSON will restore aliases before the anchors
157
+ # so in this case we allocate an instance here and register it as
158
+ # the anchor; when the anchor is restored it will re-use this instance to initialize & restore
159
+ # the properties
160
+ Serializable::Aliasing.restore_anchor(data['*id'], self.allocate)
161
+ end
162
+ else
163
+ instance = if data.has_key?('&id')
164
+ anchor_id = data.delete('&id') # extract anchor id
165
+ if Serializable::Aliasing.restored?(self, anchor_id)
166
+ # in case of cyclic references an alias will already have restored the anchor instance
167
+ # (default constructed); retrieve that instance here for deserialization of properties
168
+ Serializable::Aliasing.resolve_anchor(self, anchor_id)
169
+ else
170
+ # restore the anchor here with a newly allocated instance
171
+ Serializable::Aliasing.restore_anchor(anchor_id, self.allocate)
172
+ end
173
+ else
174
+ self.allocate
175
+ end
176
+ instance.__send__(:init_from_serialized, data)
177
+ .__send__(:from_serialized, data)
178
+ .__send__(:finalize_from_serialized)
179
+ end
180
+ end
181
+
182
+ end
183
+
184
+ # extend instance serialization methods
185
+ module SerializeInstanceMethods
186
+
187
+ def as_json(*)
188
+ json_data = {
189
+ ::JSON.create_id => self.class.name
190
+ }
191
+ if (anchor = Serializable::Aliasing.get_anchor(self))
192
+ anchor_data = Serializable::Aliasing.get_anchor_data(self)
193
+ # retroactively insert the anchor in the anchored instance's serialization data
194
+ anchor_data['&id'] = anchor unless anchor_data.has_key?('&id')
195
+ json_data["data"] = {
196
+ '*id' => anchor
197
+ }
198
+ else
199
+ # register anchor object **before** serializing properties to properly handle cycling (bidirectional
200
+ # references)
201
+ json_data['data'] = for_serialize(Serializable::Aliasing.register_anchor_object(self, {}))
202
+ json_data['data'].transform_values! { |v| v.respond_to?(:as_json) ? v.as_json : v }
203
+ end
204
+ json_data
205
+ end
206
+
207
+ end
208
+
209
+ end
210
+
211
+ module Aliasing
212
+ class << self
213
+ include Serializable::AliasManagement
214
+ end
215
+ end
216
+
217
+ # extend serialization class methods
218
+ module SerializeClassMethods
219
+
220
+ include JSON::SerializeClassMethods
221
+
222
+ end
223
+
224
+ # extend instance serialization methods
225
+ module SerializeInstanceMethods
226
+
227
+ include JSON::SerializeInstanceMethods
228
+
229
+ end
230
+
231
+ class ID
232
+ include JSON::SerializeInstanceMethods
233
+ class << self
234
+ include JSON::SerializeClassMethods
235
+ end
236
+ end
237
+
238
+ register(:json, JSON)
239
+
240
+ end
241
+
242
+ end
243
+
244
+ module ::JSON
245
+ class << self
246
+
247
+ alias :pre_firm_parse! :parse!
248
+ def parse!(*args, **kwargs)
249
+ begin
250
+ # setup parsing stack for safe or normal deserializing
251
+ # the double bracketing provided from FIRM::Serializable::JSON#load and here
252
+ # makes sure to support both nested Wx::SF deserializing as well as nested
253
+ # hybrid deserializing (Wx::SF -> common JSON -> ...)
254
+ FIRM::Serializable::JSON.start_parse
255
+ pre_firm_parse!(*args, **kwargs)
256
+ ensure
257
+ # reset parsing stack
258
+ FIRM::Serializable::JSON.end_parse
259
+ end
260
+ end
261
+
262
+ end
263
+ end
264
+
265
+ class ::Class
266
+
267
+ # override this to be able to do safe deserializing
268
+ def json_creatable?
269
+ if FIRM::Serializable::JSON.safe_parsing?
270
+ return false unless FIRM::Serializable::JSON.serializables.include?(self) ||
271
+ FIRM::Serializable.serializables.include?(self) ||
272
+ ::Struct > self
273
+ end
274
+ respond_to?(:json_create)
275
+ end
276
+
277
+ end
278
+
279
+ class ::Array
280
+ def as_json(*)
281
+ collect { |e| e.respond_to?(:as_json) ? e.as_json : e }
282
+ end
283
+ end
284
+
285
+ class ::Hash
286
+ class << self
287
+ include FIRM::Serializable::JSON::HashClassPatch
288
+ end
289
+ def as_json(*)
290
+ {
291
+ ::JSON.create_id => self.class.name,
292
+ 'data' => collect { |k,v| [k.respond_to?(:as_json) ? k.as_json : k, v.respond_to?(:as_json) ? v.as_json : v] }
293
+ }
294
+ end
295
+ end
296
+
297
+ class ::Set
298
+ def as_json(*)
299
+ {
300
+ JSON.create_id => self.class.name,
301
+ 'a' => to_a.as_json,
302
+ }
303
+ end
304
+ end
305
+
306
+ class ::Struct
307
+ class << self
308
+ def json_create(object)
309
+ # deserializing (anchor) object or alias
310
+ if object.has_key?('*id')
311
+ if FIRM::Serializable::Aliasing.restored?(self, object['*id'])
312
+ # resolving an already restored anchor for this alias
313
+ FIRM::Serializable::Aliasing.resolve_anchor(self, object['*id'])
314
+ else
315
+ # in case of cyclic references JSON will restore aliases before the anchors
316
+ # so in this case we allocate an instance here and register it as
317
+ # the anchor; when the anchor is restored it will re-use this instance to restore
318
+ # the properties
319
+ FIRM::Serializable::Aliasing.restore_anchor(object['*id'], self.allocate)
320
+ end
321
+ else
322
+ if object.has_key?('&id')
323
+ anchor_id = object['&id'] # extract anchor id
324
+ instance = if FIRM::Serializable::Aliasing.restored?(self, anchor_id)
325
+ # in case of cyclic references an alias will already have restored the anchor instance
326
+ # (default constructed); retrieve that instance here for deserialization of properties
327
+ FIRM::Serializable::Aliasing.resolve_anchor(self, anchor_id)
328
+ else
329
+ # restore the anchor here with a newly instantiated instance
330
+ FIRM::Serializable::Aliasing.restore_anchor(anchor_id, self.allocate)
331
+ end
332
+ instance.__send__(:initialize, *object['v'])
333
+ instance
334
+ else
335
+ self.new(*object['v'])
336
+ end
337
+ end
338
+ end
339
+ end
340
+
341
+ def as_json(*)
342
+ klass = self.class.name
343
+ klass.to_s.empty? and raise JSON::JSONError, "Only named structs are supported!"
344
+ # {
345
+ # JSON.create_id => klass,
346
+ # 'v' => values.as_json,
347
+ # }
348
+ json_data = {
349
+ ::JSON.create_id => klass
350
+ }
351
+ if (anchor = FIRM::Serializable::Aliasing.get_anchor(self))
352
+ anchor_data = FIRM::Serializable::Aliasing.get_anchor_data(self)
353
+ # retroactively insert the anchor in the anchored instance's serialization data
354
+ anchor_data['&id'] = anchor unless anchor_data.has_key?('&id')
355
+ json_data['*id'] = anchor
356
+ else
357
+ # register anchor object **before** serializing properties to properly handle cycling (bidirectional
358
+ # references)
359
+ FIRM::Serializable::Aliasing.register_anchor_object(self, json_data)
360
+ json_data['v'] = values.as_json
361
+ end
362
+ json_data
363
+ end
364
+ end
365
+
366
+ class ::OpenStruct
367
+ def as_json(*)
368
+ klass = self.class.name
369
+ klass.to_s.empty? and raise JSON::JSONError, "Only named structs are supported!"
370
+ {
371
+ JSON.create_id => klass,
372
+ 't' => table.as_json,
373
+ }
374
+ end
375
+ end
376
+
377
+ # fix flawed JSON serializing
378
+ class ::DateTime
379
+
380
+ def as_json(*)
381
+ {
382
+ JSON.create_id => self.class.name,
383
+ 'y' => year,
384
+ 'm' => month,
385
+ 'd' => day,
386
+ 'H' => hour,
387
+ 'M' => min,
388
+ 'S' => sec_fraction.to_f+sec,
389
+ 'of' => offset.to_s,
390
+ 'sg' => start,
391
+ }
392
+ end
393
+
394
+ end