universe_compiler 0.3.12 → 0.4.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 70b8ec67002e1f93718039b8b235496d77950b82
4
- data.tar.gz: 683e846c95e2a3f631d7c3a530d6d24c5b7fee67
3
+ metadata.gz: d32cb08d065dc49ae220b4f681c37719f8cad091
4
+ data.tar.gz: 97af0f860e453d852d9a77ff69d52e350dd2ec88
5
5
  SHA512:
6
- metadata.gz: 5c5708b1d764c28ada1c2ef1e9b0b33855ef86ecfdfd61272a316f1562cc8401499fe5b3d2e53a5436151899d34098a7fa6f47d631e013734a624819393cc15b
7
- data.tar.gz: b56bf36861ccbfac2e7f6102d22765b9db7c20243a4ed23a7d2d752c3723deb48e470ce15e602fe02a83b44ab1689b9e98a9cd1b31835e4428e68280edb5f01e
6
+ metadata.gz: 700546bad667dedb998bb527ac6fd19c815cb870ccf82d77fb3c42c8927c7ffe915b122a3ef592bd805ca3358fee054ff3c0bfb4dde4c2a5f4740b6f09b4b977
7
+ data.tar.gz: 9a5fce641199557bb8e9fdfbbaa483554d4e2e28ba38078b2757417977b079ab9ba160f83b0f4d52302f430c0276804c51770e47ad150acdb88d6e33bb49ba62
data/README.md CHANGED
@@ -8,9 +8,13 @@ UniverseCompiler
8
8
  - [Core Concepts](#core-concepts)
9
9
  - [Entities](#entities)
10
10
  - [Overview](#overview-1)
11
- - [Special directives](#special-directives)
12
- - [Constraints and relationships directives](#constraints-and-relationships-directives)
13
- - [Compilation](#compilation)
11
+ - [Special directives](#special-directives)
12
+ - [Constraints directives](#constraints-directives)
13
+ - [Relational directives](#relational-directives)
14
+ - [Basic relations](#basic-relations)
15
+ - [Advanced relations](#advanced-relations)
16
+ - [Validations](#validations)
17
+ - [Compilation](#compilation)
14
18
  - [Inheritance](#inheritance)
15
19
  - [Overrides](#overrides)
16
20
  - [Generate a graph of entities using `Graphviz`](#generate-a-graph-of-entities-using-graphviz)
@@ -135,7 +139,7 @@ a.valid? # => false, not compliant with regexp specified
135
139
  a.bar = 'Yo man'
136
140
  a.valid? # => true
137
141
  ```
138
- #### Special directives
142
+ ### Special directives
139
143
 
140
144
  By default every entity has a `type`. It is available using the `#type` instance method or the
141
145
  `::entity_type` class method. The default value for the entity type is coming
@@ -166,7 +170,7 @@ EntityD.new # => #<EntityD:46943375641700 composite_key=["entity_d", "my_seed_2"
166
170
  ```
167
171
 
168
172
 
169
- #### Constraints and relationships directives
173
+ ### Constraints directives
170
174
 
171
175
  The generic form to declare a field is the `field` statement. Any constraint can be declared using the `field`
172
176
  method. Here is the signature:
@@ -187,11 +191,6 @@ Then some methods taking parameter:
187
191
  * should_match
188
192
  * class_name
189
193
 
190
- You have as well relationship methods:
191
-
192
- * has_one
193
- * has_many
194
-
195
194
  So for each of these methods can be used either as "real" methods or as `field` parameter. For example:
196
195
  ```ruby
197
196
  class MyEntity < UniverseCompiler::Entity::Base
@@ -206,9 +205,159 @@ class MyEntity < UniverseCompiler::Entity::Base
206
205
  end
207
206
  ```
208
207
  Notice the fact that in the latter form `my_field` is "declared" more than once.
208
+
209
+ ### Relational directives
210
+
211
+ #### Basic relations
212
+
213
+ `universe_compiler` provides two relational directives
214
+
215
+ * has_one
216
+ * has_many
217
+
218
+ They specify relations to other entities and work both mainly the same way.
219
+
220
+ In it's simplest form you can define:
221
+
222
+ ```ruby
223
+ class MyEntity < UniverseCompiler::Entity::Base
224
+ has_one :another_entity_type
225
+ not_null :another_entity_type
226
+ has_one AnotherEntityClass
227
+ has_many :bar
228
+ end
229
+ ```
230
+
231
+ :information_source: You can notice that you can specify either an entity type or an entity class.
232
+
233
+ :information_source: You can use `not_null` and `not_empty` with `has_one` directives, **but on a separated declaration**. With `has_many` you can use `not_empty` (you could use `not_null` but it would always be satisfied as by default a `has_many` relation returns an empty array).
234
+
235
+ For `has_one`, the accessors generated are like for `field`. With the previous class, for `has_many` the accessors are _pluralized_ (like in activerecord).
236
+
237
+ ```ruby
238
+ e = MyEntity.new fields: {name: :foo}
239
+ # You can then issue
240
+ e.another_entity_type # =>nil
241
+ e.another_entity_type = ...
242
+ e.another_entity_class # =>nil
243
+ e.bars # =>[]
244
+ ```
245
+
246
+ You can notice the `has_one` accessors defined using a class rather than an entity type, has been _camelized_.
247
+ :warning: Notice the `has_many` directive generated _pluralized_ accessors !
248
+
249
+ This is the default behaviour, but you can override this using the `name` option (for both `has_one` and `has_many`):
250
+
251
+ ```ruby
252
+ class MyEntity < UniverseCompiler::Entity::Base
253
+ has_one :another_entity_type, name: :better_name
254
+ has_many :foo, name: :bars
255
+ end
256
+ ```
257
+ :warning: with the `has_many` directive if you specify a `name`, the accessors name is **not pluralized** (hence there, we specify the name as being `bars` and not `bar`).
258
+
259
+ ```ruby
260
+ # You can then issue
261
+ e.bettername # =>nil
262
+ e.bars # =>[]
263
+ ```
264
+
265
+ :information_source: Of course like any other field, you can still use the internal `fields`:
266
+
267
+ ```ruby
268
+ e.bettername == e.fields[:bettername]
269
+ e.bettername == e.[:bettername]
270
+ e.bars == e[:bars]
271
+ ```
272
+
273
+ #### Advanced relations
274
+
275
+ Sometimes you may want entities _targeted_ by `has_one` or `has_many` relations to _be aware_ of this fact. **You can then implement complex relations without duplicating information**.
276
+
277
+ This is called **reverse methods**.
278
+
279
+ **:warning: This can only work within a universe !**
280
+
281
+ ```ruby
282
+ class EntityA < UniverseCompiler::Entity::Base
283
+ auto_named_entity_type
284
+ entity_type :leaf
285
+ end
286
+
287
+ class EntityB < UniverseCompiler::Entity::Base
288
+ entity_type :root
289
+ end
290
+
291
+ class EntityC < UniverseCompiler::Entity::Base
292
+ entity_type :tree
293
+
294
+ has_one :root, with_reverse_method: :tree, unique: true
295
+ has_many :leaf, name: :leaves, with_reverse_method: :trunk, unique: true
296
+ end
297
+ ```
298
+
299
+ :warning: When you declare a reverse method using the `with_reverse_method` option, **an extra method is created on the target entity class**, not the one containing the has_one/many directive !
300
+
301
+ It allows the following kind of code:
302
+
303
+ ```ruby
304
+ u = UniverseCompiler::Universe::Base.new
305
+ t = EntityC.new fields: {name: :my_tree}
306
+ u << t
307
+ (1..10).each {|_| l = EntityA.new ; t.leaves << l ; u << l }
308
+
309
+ t.leaves
310
+ #=> [#<EntityA:47410094357440 composite_key=[:leaf, #"2aad17a4-096c-4de5-9be0-ee80ef522b2b"], @universe='Unnamed #Universe'>,
311
+ # #<EntityA:47410094356680 composite_key=[:leaf, #"56500c9f-0e64-4a48-9894-0e4e485ab001"], @universe='Unnamed #Universe'>,
312
+ # #<EntityA:47410094355880 composite_key=[:leaf, #"2d5eeabb-e237-4741-8c44-0e2e9636b811"], @universe='Unnamed #Universe'>,
313
+ # #<EntityA:47410094355100 composite_key=[:leaf, #"f17d7aa1-c505-4594-889a-e3d4f8813246"], @universe='Unnamed #Universe'>,
314
+ # #<EntityA:47410094354340 composite_key=[:leaf, #"268d8a0c-cb28-42a0-85ab-2dd6217e6e6f"], @universe='Unnamed #Universe'>,
315
+ # #<EntityA:47410094353560 composite_key=[:leaf, #"8d822cde-30d4-4841-bd0a-a1570583c355"], @universe='Unnamed #Universe'>,
316
+ # #<EntityA:47410094352740 composite_key=[:leaf, #"dfffe2c2-b283-47ec-b799-3ef79c8584b4"], @universe='Unnamed #Universe'>,
317
+ # #<EntityA:47410094351940 composite_key=[:leaf, #"c8c111da-ad70-412e-9f2c-7af11894641c"], @universe='Unnamed #Universe'>,
318
+ # #<EntityA:47410094351140 composite_key=[:leaf, #"e1158988-6659-426e-912c-73f827e7429f"], @universe='Unnamed #Universe'>,
319
+ # #<EntityA:47410094350360 composite_key=[:leaf, #"64b0e4c6-a718-43b9-ae8f-beeea5145c20"], @universe='Unnamed #Universe'>]
320
+ t.leaves.last.trunk
321
+ # => #<EntityC:47410094828200 composite_key=[:tree, :my_tree], @universe='Unnamed Universe'>
322
+ t.leaves.last.fields
323
+ # => {:name=>"c881f93d-7216-49a0-a7d4-b5a2a4a314d4"}
324
+ t.leaves.last.respond_to? :trunk
325
+ # => true
326
+ ```
327
+
328
+ You can then notice that any _leaf_ has a new `trunk` method which returns the entity it is referenced from (in this case the _tree_ entity). **The fields themselves are not modified !**
209
329
 
210
330
 
211
- ### Compilation
331
+ What happens if multiple entities reference the same entity ?
332
+
333
+ ```ruby
334
+ t2 = EntityC.new fields: {name: :oak}
335
+ u << t2
336
+ # And let's insert on entity A already added to t
337
+ t2.leaves << t.leaves.last
338
+ t.leaves.last.trunk
339
+ # UniverseCompiler::Error: 'leaf/18693021-c0a3-4e47-be89-291850d7a0ff#trunk' should return only one 'tree' !
340
+ ```
341
+
342
+ An exception is returned. the `unique` option actually specifies that only one entity should reference it !
343
+ If you don't specify this option, an array is returned instead and this check is not performed.
344
+
345
+ ### Validations
346
+
347
+ Every constraint defined on a field or a relation is enforced when an entity is validated (which is as well true when saving it). Continuing on previous example:
348
+
349
+ ```ruby
350
+ t.leaves.last.valid?
351
+ # => false
352
+ t.leaves.last.valid? raise_error: true
353
+ # UniverseCompiler::Error: Invalid entity '[:leaf, "0ecd1283-e98e-43ec-943b-b92e2e8ffa2b"]' for fields trunk !
354
+
355
+ ```
356
+
357
+ :information_source: Here above is just an example regarding the reverse methods but any constraint added to an entity is enforced at validation time (`not_null`, `is_hash`... all of them).
358
+
359
+
360
+ ## Compilation
212
361
 
213
362
  The compilation mechanism is related to universes.
214
363
  When compiling a universe it actually:
@@ -26,6 +26,7 @@ module UniverseCompiler
26
26
  def initialize(fields: {}, universe: nil)
27
27
  @fields = fields
28
28
  define_known_fields_accessors
29
+ define_reverse_methods
29
30
  self.universe = universe
30
31
  self.fully_resolved = true
31
32
  if universe.nil?
@@ -75,8 +75,6 @@ module UniverseCompiler
75
75
 
76
76
  end
77
77
 
78
- private
79
-
80
78
  def define_constraint(field_name, constraint_name = nil, value = nil)
81
79
  fields_constraints[field_name] ||= {}
82
80
  check_constraints_incompatibilities(field_name, constraint_name)
@@ -85,6 +83,14 @@ module UniverseCompiler
85
83
  end
86
84
  end
87
85
 
86
+ private
87
+
88
+ def define_constraints_on_target_entity_type(target_entity_type, source_field, reverse_method = nil)
89
+ target_class = UniverseCompiler::Entity::TypeManagement.types_classes_mapping[target_entity_type]
90
+ raise 'NOT IMPLEMENTED'
91
+ end
92
+
93
+
88
94
  def check_constraints_incompatibilities(field_name, constraint_name)
89
95
  unless INCOMPATIBLE_CONSTRAINTS[constraint_name].nil?
90
96
  INCOMPATIBLE_CONSTRAINTS[constraint_name].each do |incompatible_constraint|
@@ -3,11 +3,67 @@ module UniverseCompiler
3
3
 
4
4
  module FieldManagement
5
5
 
6
-
7
6
  private
8
7
 
8
+ def define_reverse_methods
9
+ self.class.fields_constraints.each do |method_name, constraints|
10
+ next unless constraints[:reverse_method]
11
+
12
+ define_reverse_method method_name, constraints[:reverse_method]
13
+ end
14
+ end
15
+
16
+ def define_reverse_method(default_method_name, method_definition_constraints)
17
+ metaclass = class << self; self ; end
18
+ method_name = normalize_method_name(default_method_name, method_definition_constraints)
19
+ UniverseCompiler.logger.debug 'Defining reverse method "%s" on class %s (%s)' % [method_name, metaclass, self.type]
20
+ raise UniverseCompiler::Error, "'#{method_name}' already exists on class '#{metaclass}'. Skipped !" if self.respond_to? method_name
21
+ method_definition_constraints[:actual_method] = method_name
22
+
23
+ check_operation = case method_definition_constraints[:relation_type]
24
+ when :has_one
25
+ '=='.to_sym
26
+ when :has_many
27
+ 'include?'.to_sym
28
+ end
29
+
30
+ metaclass.instance_eval do
31
+ define_method method_name do
32
+ raise UniverseCompiler::Error, "Entity '#{as_path}' is not in a universe. Reverse methods can't work !" if universe.nil?
33
+
34
+ res = universe.get_entities(criterion: :by_type, value: method_definition_constraints[:source_entity]).select do |entity|
35
+ entity[method_definition_constraints[:source_field]].send check_operation, self
36
+ end
37
+ return res if res.nil? || res.empty?
38
+
39
+ if method_definition_constraints[:unique_result]
40
+ if res.size == 1
41
+ return res.first
42
+ else
43
+ UniverseCompiler.logger.warn 'Too many results. Must be one or none !'
44
+ UniverseCompiler.logger.debug res.inspect
45
+ raise UniverseCompiler::Error, "'#{self.as_path}##{method_name}' should return maximum one '#{method_definition_constraints[:source_entity]}' !"
46
+ end
47
+ end
48
+
49
+ res
50
+ end
51
+ end
52
+ end
53
+
54
+
55
+
56
+ def normalize_method_name(default_method_name, method_definition_constraints)
57
+ if default_method_name == method_definition_constraints[:source_entity]
58
+ "referenced_from_#{method_definition_constraints[:source_entity]}_entities"
59
+ else
60
+ default_method_name
61
+ end
62
+ end
63
+
9
64
  def define_known_fields_accessors
10
65
  self.class.fields_constraints.each do |field_name, constraints|
66
+ next if constraints[:reverse_method]
11
67
  define_field_accessor field_name
12
68
  if fields[field_name].nil?
13
69
  fields[field_name] = [] if constraints[:has_many] || constraints[:is_array]
@@ -5,40 +5,62 @@ module UniverseCompiler
5
5
 
6
6
  module RelationsManagement
7
7
 
8
- def has_one(entity_type, name: nil)
9
- case entity_type
10
- when Class
11
- name = name.nil? ? entity_type.entity_type : name.to_sym
12
- define_constraint name, :has_one, entity_type.entity_type
13
- when Symbol, String
14
- name = name.nil? ? entity_type.to_sym : name.to_sym
15
- define_constraint name, :has_one, entity_type.to_sym
16
- end
8
+ def has_one(target_entity_type_or_class, name: nil, with_reverse_method: nil, unique: false)
9
+ target_entity_type = normalize_entity_type target_entity_type_or_class
10
+ field_name = relation_field_name name, target_entity_type
11
+ define_constraint field_name, :has_one, target_entity_type
12
+ return unless with_reverse_method
13
+
14
+ define_constraint_for_reverse_method :has_one,
15
+ target_entity_type,
16
+ field_name,
17
+ with_reverse_method,
18
+ unique
17
19
  end
18
20
 
19
- def has_many(entity_type, name: nil)
20
- name = case entity_type
21
- when Class
22
- name.nil? ? entity_type.entity_type : name.to_sym
23
- when Symbol, String
24
- name.nil? ? entity_type.to_sym : name.to_sym
25
- end
26
- field_name = name.to_s.pluralize.to_sym
27
-
28
- case entity_type
29
- when Class
30
- define_constraint field_name, :has_many, entity_type.entity_type
31
- when Symbol, String
32
- define_constraint field_name, :has_many, entity_type.to_sym
33
- end
21
+ def has_many(target_entity_type_or_class, name: nil, with_reverse_method: nil, unique: false)
22
+ target_entity_type = normalize_entity_type target_entity_type_or_class
23
+ field_name = relation_field_name name, target_entity_type
24
+ field_name = field_name.to_s.pluralize.to_sym if name.nil?
25
+ define_constraint field_name, :has_many, target_entity_type
26
+ return unless with_reverse_method
27
+
28
+ define_constraint_for_reverse_method :has_many,
29
+ target_entity_type,
30
+ field_name,
31
+ with_reverse_method,
32
+ unique
34
33
  end
35
34
 
36
- def entity_type_relations
37
- fields_constraints.select do |_, constraints|
38
- constraints.keys.include? :has_one or constraints.keys.include? :has_many
35
+ private
36
+
37
+ def relation_field_name(name, target_entity_type)
38
+ case name
39
+ when NilClass
40
+ target_entity_type
41
+ when String, Symbol
42
+ name.to_sym
39
43
  end
40
44
  end
41
45
 
46
+ def define_constraint_for_reverse_method(relation_type, target_entity_type, source_field_name, with_reverse_method, unique)
47
+ reverse_method_name = case with_reverse_method
48
+ when TrueClass
49
+ self.entity_type
50
+ when String, Symbol
51
+ with_reverse_method.to_sym
52
+ end
53
+ target_class = UniverseCompiler::Entity::TypeManagement.type_class_mapping(target_entity_type)
54
+ method_creation_data = {
55
+ source_entity: self.entity_type,
56
+ source_field: source_field_name,
57
+ relation_type: relation_type,
58
+ unique_result: unique
59
+ }
60
+ target_class.define_constraint reverse_method_name, :reverse_method, method_creation_data
61
+ true
62
+ end
63
+
42
64
  end
43
65
 
44
66
  end
@@ -3,6 +3,8 @@ module UniverseCompiler
3
3
 
4
4
  module TypeManagement
5
5
 
6
+ # **
7
+ # Methods to be added into the class the UniverseCompiler::Entity::TypeManagement is included in.
6
8
  module ClassMethods
7
9
 
8
10
  def entity_type(value = nil)
@@ -16,17 +18,45 @@ module UniverseCompiler
16
18
  def entity_type=(value)
17
19
  raise UniverseCompiler::Error, "You cannot change an entity type for class '#{self.name}'" unless @entity_type.nil?
18
20
  raise UniverseCompiler::Error, 'Only Symbol is supported for entity_type !' unless value.is_a? Symbol
21
+ mapping = UniverseCompiler::Entity::TypeManagement.types_classes_mapping
22
+ if mapping.keys.include? value
23
+ raise UniverseCompiler::Error, "Type '#{value}' already registered for another class (#{mapping[value]})" unless self == mapping[value]
24
+ end
25
+ mapping[value] = self
19
26
  @entity_type = value
20
27
  end
21
28
 
29
+ private
30
+
31
+ def normalize_entity_type(entity_type_or_class)
32
+ case entity_type_or_class
33
+ when Class
34
+ entity_type_or_class.entity_type
35
+ when Symbol, String
36
+ entity_type_or_class.to_sym
37
+ end
38
+ end
39
+
22
40
  end
23
41
 
24
- def self.valid_for_type?(entity)
25
- entity.respond_to? :type and entity.class.respond_to? :entity_type
42
+ # **
43
+ # Module methods to be directly used
44
+
45
+ def self.type_class_mapping(type_or_class)
46
+ case type_or_class
47
+ when Symbol, String
48
+ types_classes_mapping[type_or_class.to_sym]
49
+ when Class
50
+ type_or_class
51
+ end
26
52
  end
27
53
 
28
- def type
29
- self.class.entity_type
54
+ def self.types_classes_mapping
55
+ @types_classes_mapping ||= {}
56
+ end
57
+
58
+ def self.valid_for_type?(entity)
59
+ entity.respond_to? :type and entity.class.respond_to? :entity_type
30
60
  end
31
61
 
32
62
  def self.included(base)
@@ -37,6 +67,13 @@ module UniverseCompiler
37
67
  included base.class
38
68
  end
39
69
 
70
+ # **
71
+ # The only instance method
72
+
73
+ def type
74
+ self.class.entity_type
75
+ end
76
+
40
77
  end
41
78
 
42
79
  end
@@ -53,6 +53,12 @@ module UniverseCompiler
53
53
  end
54
54
  end
55
55
  end
56
+ when :reverse_method
57
+ begin
58
+ send value[:actual_method] if value[:unique_result]
59
+ rescue UniverseCompiler::Error => uce
60
+ invalid_for_constraint invalid, value[:actual_method], constraint_name, value
61
+ end
56
62
  else
57
63
  UniverseCompiler.logger.warn "Cannot handle unknown constraint '#{constraint_name}'! Skipping..."
58
64
  end
@@ -1,3 +1,3 @@
1
1
  module UniverseCompiler
2
- VERSION = '0.3.12'.freeze
2
+ VERSION = '0.4.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: universe_compiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.12
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Laurent B.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-09-16 00:00:00.000000000 Z
11
+ date: 2019-09-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler