universe_compiler 0.3.12 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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