...
 
Commits (2)
......@@ -15,6 +15,8 @@ UniverseCompiler
- [Relational directives](#relational-directives)
- [Basic relations](#basic-relations)
- [Advanced relations](#advanced-relations)
- [Reverse methods](#reverse-methods)
- [Strict vs permissive](#strict-vs-permissive)
- [Validations](#validations)
- [Compilation](#compilation)
- [Inheritance](#inheritance)
......@@ -274,6 +276,8 @@ e.bars == e[:bars]
#### Advanced relations
##### Reverse methods
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**.
This is called **reverse methods**.
......@@ -342,6 +346,52 @@ t.leaves.last.trunk
An exception is returned. the `unique` option actually specifies that only one entity should reference it !
If you don't specify this option, an array is returned instead and this check is not performed.
##### Strict vs permissive
By default, when a relation is declared between entities using `has_one` or `has_many`, the relation is said to be _permissive_, ie subclasses are allowed to be used for the relation. For example, let's consider the following classes:
```ruby
class Level1 < UniverseCompiler::Entity::Base
entity_type :level1
has_one :level1, name: :permissive_link
end
class Level2 < RootLevel
entity_type :level2
end
class Level3 < Level2
entity_type :level3
end
```
It means that any instance of `Level2` or `Level3` can be used in the `has_one` relation declared in `Level1`. This is the default behavior.
But if you consider the following classes:
```ruby
class Level1 < UniverseCompiler::Entity::Base
entity_type :level1
has_one :level1, name: :strict_link, strict_type: true
end
class Level2 < RootLevel
entity_type :level2
end
class Level3 < Level2
entity_type :level3
end
```
Then only `Level1` instances will be valid for the `has_one` relation declared. The keyword as you guessed is `strict_type: true`.
:information_source: It works the same way for `has_many` relations, applied to **all** instances added to the array...
### Validations
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:
......
......@@ -5,10 +5,11 @@ module UniverseCompiler
module RelationsManagement
def has_one(target_entity_type_or_class, name: nil, with_reverse_method: nil, unique: false)
def has_one(target_entity_type_or_class, name: nil, strict_type: false, with_reverse_method: nil, unique: false)
target_entity_type = normalize_entity_type target_entity_type_or_class
field_name = relation_field_name name, target_entity_type
define_constraint field_name, :has_one, target_entity_type
define_constraint field_name, :strict_type, target_entity_type if strict_type
return unless with_reverse_method
define_constraint_for_reverse_method :has_one,
......@@ -18,11 +19,12 @@ module UniverseCompiler
unique
end
def has_many(target_entity_type_or_class, name: nil, with_reverse_method: nil, unique: false)
def has_many(target_entity_type_or_class, name: nil, strict_type: false, with_reverse_method: nil, unique: false)
target_entity_type = normalize_entity_type target_entity_type_or_class
field_name = relation_field_name name, target_entity_type
field_name = field_name.to_s.pluralize.to_sym if name.nil?
define_constraint field_name, :has_many, target_entity_type
define_constraint field_name, :strict_type, target_entity_type if strict_type
return unless with_reverse_method
define_constraint_for_reverse_method :has_many,
......
......@@ -35,9 +35,7 @@ module UniverseCompiler
end
when :has_one
unless fields[field_name].nil?
if fields[field_name].respond_to? :type
invalid_for_constraint invalid, field_name, constraint_name, value unless fields[field_name].type == value
else
unless provided_entity_compatible_with_type? fields[field_name], value, constraints[:strict_type]
invalid_for_constraint invalid, field_name, constraint_name, value
end
end
......@@ -46,9 +44,7 @@ module UniverseCompiler
invalid_for_constraint invalid, field_name, constraint_name, value
else
fields[field_name].each do |related_object|
if related_object.respond_to? :type
invalid_for_constraint invalid, field_name, constraint_name, value unless related_object.type == value
else
unless provided_entity_compatible_with_type? related_object, value, constraints[:strict_type]
invalid_for_constraint invalid, field_name, constraint_name, value
end
end
......@@ -87,6 +83,15 @@ module UniverseCompiler
private
def provided_entity_compatible_with_type?(linked_object, declared_type, strict_type)
declared_class = UniverseCompiler::Entity::TypeManagement.type_class_mapping(declared_type)
if strict_type
linked_object.class == declared_class
else
linked_object.is_a? declared_class
end
end
def invalid_for_constraint(invalid_fields_definition, field_name, constraint_name, value)
invalid_fields_definition[field_name] ||= []
invalid_fields_definition[field_name] << { constraint_name => value}
......
......@@ -41,7 +41,7 @@ describe UniverseCompiler::Entity do
expect(subject).to respond_to :fields
end
context 'when an entity inherits from enother one' do
context 'when an entity inherits from another one' do
subject { InheritedTestEntity }
let(:superclass_constraints) { %i(foo bar siblings pal) }
let(:added_constraints) { %i(in_inherited) }
......@@ -111,6 +111,60 @@ describe UniverseCompiler::Entity do
end
context 'when there is an entity chain of inheritance' do
context 'when links are permissive' do
let(:level3) { Level3.new }
let(:level2) { Level2.new }
let(:root_level) { RootLevel.new }
subject { RootLevel.new }
it 'should accept inherited types' do
expect(subject).to be_valid
subject.permissive_link = root_level
expect(subject).to be_valid
subject.permissive_link = level2
expect(subject).to be_valid
subject.permissive_link = level3
expect(subject).to be_valid
end
end
context 'when links are strict' do
let(:level3) { Level3.new }
let(:level2) { Level2.new }
let(:root_level) { RootLevel.new }
subject { Level2.new }
it 'should not accept inherited types' do
expect(subject).to be_valid
subject.strict_link = level2
expect(subject).to be_valid
subject.strict_link = level3
expect(subject).not_to be_valid
subject.strict_link = root_level
expect(subject).not_to be_valid
subject.strict_link = nil
expect(subject).to be_valid
subject.strict_links << level2
expect(subject).to be_valid
subject.strict_links << level3
expect(subject).not_to be_valid
subject.strict_links.pop
expect(subject).to be_valid
subject.strict_links << root_level
expect(subject).not_to be_valid
end
end
end
context 'when accessing a field with accessor' do
it 'should fail accessing a non existing field' do
......
......@@ -51,8 +51,30 @@ class InheritedTestEntity < TestEntity
field :in_inherited
has_one :second_level_inherited_entity_type, name: :strict_test, strict_type: true
end
class RootLevel < UniverseCompiler::Entity::Base
entity_type :level1
has_one :level1, name: :permissive_link
end
class Level2 < RootLevel
entity_type :level2
has_one :level2, name: :strict_link, strict_type: true
has_many :level2, name: :strict_links, strict_type: true
end
class Level3 < Level2
entity_type :level3
end
class TestEntity2 < UniverseCompiler::Entity::Base
auto_named_entity_type
end
......