entity_schema 0.1.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 +7 -0
- data/.gitignore +11 -0
- data/.overcommit.yml +37 -0
- data/.rspec +3 -0
- data/.rubocop.yml +56 -0
- data/.rubocop_todo.yml +26 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/CHANGELOG.md +17 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +92 -0
- data/Guardfile +30 -0
- data/LICENSE.txt +21 -0
- data/README.md +114 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/entity_schema.gemspec +49 -0
- data/lib/entity_schema/class_methods.rb +28 -0
- data/lib/entity_schema/dsl.rb +40 -0
- data/lib/entity_schema/dsl_helper.rb +26 -0
- data/lib/entity_schema/fields/abstract.rb +89 -0
- data/lib/entity_schema/fields/builders/.rubocop.yml +7 -0
- data/lib/entity_schema/fields/builders/abstract.rb +98 -0
- data/lib/entity_schema/fields/builders/belongs_to.rb +91 -0
- data/lib/entity_schema/fields/builders/collection.rb +20 -0
- data/lib/entity_schema/fields/builders/fk_belongs_to.rb +21 -0
- data/lib/entity_schema/fields/builders/object.rb +40 -0
- data/lib/entity_schema/fields/builders/object_belongs_to.rb +17 -0
- data/lib/entity_schema/fields/builders/property.rb +33 -0
- data/lib/entity_schema/fields/collection.rb +35 -0
- data/lib/entity_schema/fields/fk_belongs_to.rb +16 -0
- data/lib/entity_schema/fields/object.rb +36 -0
- data/lib/entity_schema/fields/object_belongs_to.rb +16 -0
- data/lib/entity_schema/fields/observer_belongs_to.rb +33 -0
- data/lib/entity_schema/fields/property.rb +24 -0
- data/lib/entity_schema/instance_methods.rb +42 -0
- data/lib/entity_schema/schema.rb +77 -0
- data/lib/entity_schema/version.rb +5 -0
- data/lib/entity_schema.rb +18 -0
- metadata +227 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'entity_schema'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
require 'pry'
|
12
|
+
Pry.start
|
13
|
+
|
14
|
+
# require 'irb'
|
15
|
+
# IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
lib = File.expand_path('lib', __dir__)
|
5
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
6
|
+
require 'entity_schema/version'
|
7
|
+
|
8
|
+
Gem::Specification.new do |spec|
|
9
|
+
spec.name = 'entity_schema'
|
10
|
+
spec.version = EntitySchema::VERSION
|
11
|
+
spec.authors = ['Captain Philipp']
|
12
|
+
spec.email = ['ph-s@mail.ru']
|
13
|
+
|
14
|
+
spec.summary = 'DSL for describe Entities'
|
15
|
+
spec.description = 'DSL for describe Entities that can have value objects, ' \
|
16
|
+
'associations and properties. Focused on performance and simplicity'
|
17
|
+
spec.homepage = 'https://github.com/CaptainPhilipp/entity_schema'
|
18
|
+
spec.license = 'MIT'
|
19
|
+
|
20
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
21
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
22
|
+
if spec.respond_to?(:metadata)
|
23
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
24
|
+
else
|
25
|
+
raise 'RubyGems 2.0 or newer is required to protect against ' \
|
26
|
+
'public gem pushes.'
|
27
|
+
end
|
28
|
+
|
29
|
+
# Specify which files should be added to the gem when it is released.
|
30
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
31
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
32
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
33
|
+
end
|
34
|
+
spec.bindir = 'exe'
|
35
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
36
|
+
spec.require_paths = ['lib']
|
37
|
+
|
38
|
+
spec.add_development_dependency 'bundler', '~> 1.16'
|
39
|
+
spec.add_development_dependency 'byebug'
|
40
|
+
spec.add_development_dependency 'overcommit'
|
41
|
+
spec.add_development_dependency 'pry'
|
42
|
+
spec.add_development_dependency 'pry-byebug'
|
43
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
44
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
45
|
+
|
46
|
+
spec.add_development_dependency 'guard'
|
47
|
+
spec.add_development_dependency 'guard-bundler'
|
48
|
+
spec.add_development_dependency 'guard-rspec'
|
49
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'schema'
|
4
|
+
|
5
|
+
module EntitySchema
|
6
|
+
# TODO: doc
|
7
|
+
module ClassMethods
|
8
|
+
# rubocop:disable Metrics/MethodLength:
|
9
|
+
def entity_schema
|
10
|
+
@entity_schema ||= begin
|
11
|
+
if superclass.respond_to?(:entity_schema)
|
12
|
+
superschema = superclass.entity_schema
|
13
|
+
|
14
|
+
unless superschema.is_a?(Schema)
|
15
|
+
raise Exception, "class-level method `#{superclass}.entity_schema` is required " \
|
16
|
+
'by gem "entity_schema" and must return instance of ' \
|
17
|
+
"`#{Schema}`, but returns a `#{superschema.class}`"
|
18
|
+
end
|
19
|
+
|
20
|
+
Schema.new(owner_name: to_s).extends(superschema)
|
21
|
+
else
|
22
|
+
Schema.new(owner_name: to_s)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
# rubocop:enable Metrics/MethodLength:
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'fields/builders/property'
|
4
|
+
require_relative 'fields/builders/object'
|
5
|
+
require_relative 'fields/builders/collection'
|
6
|
+
require_relative 'fields/builders/belongs_to'
|
7
|
+
require_relative 'dsl_helper'
|
8
|
+
|
9
|
+
module EntitySchema
|
10
|
+
# class-level methods for define entity_schema
|
11
|
+
module Dsl
|
12
|
+
include DslHelper
|
13
|
+
|
14
|
+
def property(name, **opts)
|
15
|
+
setup_field Fields::Builders::Property.(name, to_s, opts)
|
16
|
+
end
|
17
|
+
|
18
|
+
def property?(name, **opts)
|
19
|
+
property(name, opts.merge!(predicate: true))
|
20
|
+
end
|
21
|
+
|
22
|
+
def object(name, **opts)
|
23
|
+
setup_field Fields::Builders::Object.(name, to_s, opts)
|
24
|
+
end
|
25
|
+
|
26
|
+
alias has_one object
|
27
|
+
|
28
|
+
def collection(name, **opts)
|
29
|
+
setup_field Fields::Builders::Collection.(name, to_s, opts)
|
30
|
+
end
|
31
|
+
|
32
|
+
alias has_many collection
|
33
|
+
|
34
|
+
def belongs_to(name, **opts)
|
35
|
+
fk, object = Fields::Builders::BelongsTo.(name, to_s, opts)
|
36
|
+
setup_field object
|
37
|
+
setup_field fk
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EntitySchema
|
4
|
+
# TODO: remove this shit
|
5
|
+
# TODO: doc
|
6
|
+
module DslHelper
|
7
|
+
def setup_field(field)
|
8
|
+
remove_method(field.name) if method_defined?(field.name)
|
9
|
+
remove_method(field.predicate_name) if method_defined?(field.predicate_name)
|
10
|
+
remove_method(field.setter_name) if method_defined?(field.setter_name)
|
11
|
+
|
12
|
+
entity_schema.add_field field
|
13
|
+
|
14
|
+
define_method(field.name) { field.get(self) }
|
15
|
+
private(field.name) unless field.public_getter?
|
16
|
+
|
17
|
+
if field.predicate?
|
18
|
+
define_method(field.predicate_name) { field.get(self) }
|
19
|
+
private(field.predicate_name) unless field.public_getter?
|
20
|
+
end
|
21
|
+
|
22
|
+
define_method(field.setter_name) { |value| field.set(self, value) }
|
23
|
+
private(field.setter_name) unless field.public_setter?
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EntitySchema
|
4
|
+
module Fields
|
5
|
+
# TODO: doc
|
6
|
+
class Abstract
|
7
|
+
attr_reader :src_key, :name, :predicate_name, :setter_name, :ivar_name
|
8
|
+
|
9
|
+
def initialize(name, owner_name, options)
|
10
|
+
@name = name.to_sym
|
11
|
+
@owner_name = owner_name
|
12
|
+
@src_key = options.delete(:src_key)
|
13
|
+
@public_getter = options.delete(:public_getter)
|
14
|
+
@public_setter = options.delete(:public_setter)
|
15
|
+
|
16
|
+
@predicate_name = :"#{name}?"
|
17
|
+
@setter_name = :"#{name}="
|
18
|
+
@ivar_name = :"@#{name}"
|
19
|
+
end
|
20
|
+
|
21
|
+
# set from public caller
|
22
|
+
def public_set(obj, value)
|
23
|
+
raise_public_set unless public_setter?
|
24
|
+
set(obj, value)
|
25
|
+
end
|
26
|
+
|
27
|
+
def public_get(obj)
|
28
|
+
raise_public_get unless public_getter?
|
29
|
+
get(obj)
|
30
|
+
end
|
31
|
+
|
32
|
+
def given?(obj)
|
33
|
+
obj.instance_variable_defined?(ivar_name)
|
34
|
+
end
|
35
|
+
|
36
|
+
def delete(obj)
|
37
|
+
obj.remove_instance_variable(ivar_name)
|
38
|
+
end
|
39
|
+
|
40
|
+
def set(obj, value)
|
41
|
+
write(obj, value)
|
42
|
+
end
|
43
|
+
|
44
|
+
def get(_obj)
|
45
|
+
raise NotImplementedError
|
46
|
+
end
|
47
|
+
|
48
|
+
def predicate?
|
49
|
+
false
|
50
|
+
end
|
51
|
+
|
52
|
+
def serialize(obj, output)
|
53
|
+
output[src_key] = read(obj) if given?(obj)
|
54
|
+
end
|
55
|
+
|
56
|
+
def public_getter?
|
57
|
+
@public_getter
|
58
|
+
end
|
59
|
+
|
60
|
+
def public_setter?
|
61
|
+
@public_setter
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
attr_reader :owner_name, :serialize_method
|
67
|
+
|
68
|
+
def raise_public_set
|
69
|
+
raise NameError, "Private Setter called for field `#{name}` of `#{owner_name}`"
|
70
|
+
end
|
71
|
+
|
72
|
+
def raise_public_get
|
73
|
+
raise NameError, "Private Getter called for field `#{name}` of `#{owner_name}`"
|
74
|
+
end
|
75
|
+
|
76
|
+
def guard_unknown_options!(opts)
|
77
|
+
raise "Unknown options given: #{opts.inspect}" if opts.any?
|
78
|
+
end
|
79
|
+
|
80
|
+
def read(obj)
|
81
|
+
obj.instance_variable_get(ivar_name)
|
82
|
+
end
|
83
|
+
|
84
|
+
def write(obj, value)
|
85
|
+
obj.instance_variable_set(ivar_name, value)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
require_relative '../property'
|
5
|
+
|
6
|
+
module EntitySchema
|
7
|
+
module Fields
|
8
|
+
module Builders
|
9
|
+
# TODO: doc
|
10
|
+
class Abstract
|
11
|
+
include Singleton
|
12
|
+
|
13
|
+
def self.call(*args)
|
14
|
+
instance.call(*args)
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(name, owner_name, options)
|
18
|
+
options = options.dup
|
19
|
+
opts = extract_options(options)
|
20
|
+
guard_unknown_options!(options, name)
|
21
|
+
create_field(name, owner_name, opts)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def extract_options(o)
|
27
|
+
{
|
28
|
+
key: check!(:key, o, [Symbol, nil]),
|
29
|
+
getter: check!(:getter, o, [:private, nil]),
|
30
|
+
setter: check!(:setter, o, [:private, nil]),
|
31
|
+
private: check!(:private, o, [true, false, :getter, :setter, nil])
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
def create_field(name, owner_name, opts)
|
36
|
+
field_klass.new(name, owner_name, **create_field_params(opts, name))
|
37
|
+
end
|
38
|
+
|
39
|
+
# rubocop:disable Metrics/AbcSize:
|
40
|
+
def create_field_params(o, name)
|
41
|
+
{
|
42
|
+
src_key: first_of(o[:key], name),
|
43
|
+
public_getter: !first_of(true_(o[:getter] == :private), true_(o[:private] == :getter), true_(o[:private]), false),
|
44
|
+
public_setter: !first_of(true_(o[:setter] == :private), true_(o[:private] == :setter), true_(o[:private]), false)
|
45
|
+
}
|
46
|
+
end
|
47
|
+
# rubocop:enable Metrics/AbcSize:
|
48
|
+
|
49
|
+
def field_klass
|
50
|
+
raise NotImplementedError
|
51
|
+
end
|
52
|
+
|
53
|
+
# Helpers
|
54
|
+
|
55
|
+
# rubocop:disable Naming/UncommunicativeMethodParamName
|
56
|
+
def check!(key, h, allowed)
|
57
|
+
value = h.delete(key)
|
58
|
+
return value if allowed.any? do |v|
|
59
|
+
v.is_a?(Class) ? value.is_a?(v) : value == v
|
60
|
+
end
|
61
|
+
raise ArgumentError, "option `#{key}:` must be in #{allowed}, but '#{value.inspect}'"
|
62
|
+
end
|
63
|
+
# rubocop:enable Naming/UncommunicativeMethodParamName
|
64
|
+
|
65
|
+
# rubocop:disable Naming/UncommunicativeMethodParamName
|
66
|
+
def check_ducktype!(key, h, methods)
|
67
|
+
value = h.delete(key)
|
68
|
+
return value if value.nil? || methods.all? { |m| value.respond_to?(m) }
|
69
|
+
raise ArgumentError, "option `#{key}:` (#{value.inspect}) must respond to #{methods}"
|
70
|
+
end
|
71
|
+
# rubocop:enable Naming/UncommunicativeMethodParamName
|
72
|
+
|
73
|
+
def first_of(*alternatives)
|
74
|
+
alternatives.compact.first
|
75
|
+
end
|
76
|
+
|
77
|
+
def true_(value)
|
78
|
+
value == true ? true : nil
|
79
|
+
end
|
80
|
+
|
81
|
+
def not_bool(value)
|
82
|
+
case value
|
83
|
+
when true, false then nil
|
84
|
+
else value
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_bool(value)
|
89
|
+
value ? true : false
|
90
|
+
end
|
91
|
+
|
92
|
+
def guard_unknown_options!(opts, name)
|
93
|
+
raise "Unknown options given to `#{name}:` #{opts.inspect}" if opts.any?
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'abstract'
|
4
|
+
require_relative 'fk_belongs_to'
|
5
|
+
require_relative 'object_belongs_to'
|
6
|
+
require_relative '../observer_belongs_to'
|
7
|
+
|
8
|
+
module EntitySchema
|
9
|
+
module Fields
|
10
|
+
module Builders
|
11
|
+
# TODO: doc
|
12
|
+
class BelongsTo < Abstract
|
13
|
+
def call(name, schema, options)
|
14
|
+
options = options.dup
|
15
|
+
opts = extract_options(options)
|
16
|
+
guard_unknown_options!(options, name)
|
17
|
+
|
18
|
+
fk = create_fk(name, schema, opts)
|
19
|
+
object = create_object(name, schema, opts)
|
20
|
+
|
21
|
+
create_observer(fk, object, opts)
|
22
|
+
[fk, object]
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
# rubocop:disable Naming/UncommunicativeMethodParamName
|
28
|
+
def extract_options(h)
|
29
|
+
delete_keys(h, all_keys).merge!(
|
30
|
+
pk: check!(:pk, h, [Symbol, nil]),
|
31
|
+
fk: check!(:fk, h, [Symbol, nil])
|
32
|
+
)
|
33
|
+
end
|
34
|
+
# rubocop:enable Naming/UncommunicativeMethodParamName
|
35
|
+
|
36
|
+
def create_fk(object_name, schema, opts)
|
37
|
+
name = fk_name(opts[:fk], object_name)
|
38
|
+
Fields::Builders::FkBelongsTo.(name, schema, create_fk_params(opts, name))
|
39
|
+
end
|
40
|
+
|
41
|
+
def create_fk_params(opts, name)
|
42
|
+
opts.slice(*fk_keys).merge!(key: name)
|
43
|
+
end
|
44
|
+
|
45
|
+
def create_object(name, schema, opts)
|
46
|
+
Fields::Builders::ObjectBelongsTo.(name, schema, opts.slice(*object_keys))
|
47
|
+
end
|
48
|
+
|
49
|
+
def create_observer(fk, object, opts)
|
50
|
+
observer = ObserverBelongsTo.new(fk, object, object_pk: opts[:pk] || :id)
|
51
|
+
fk.observer_belongs_to = observer
|
52
|
+
object.observer_belongs_to = observer
|
53
|
+
end
|
54
|
+
|
55
|
+
def all_keys
|
56
|
+
common_keys + only_object_keys + only_fk_keys
|
57
|
+
end
|
58
|
+
|
59
|
+
def object_keys
|
60
|
+
common_keys + only_object_keys
|
61
|
+
end
|
62
|
+
|
63
|
+
def fk_keys
|
64
|
+
common_keys + only_fk_keys
|
65
|
+
end
|
66
|
+
|
67
|
+
def only_object_keys
|
68
|
+
%i[mapper map_to map_method serialize_method serializer serialize]
|
69
|
+
end
|
70
|
+
|
71
|
+
def only_fk_keys
|
72
|
+
[:predicate]
|
73
|
+
end
|
74
|
+
|
75
|
+
def common_keys
|
76
|
+
%i[key getter setter private]
|
77
|
+
end
|
78
|
+
|
79
|
+
def fk_name(fk_name, object_name)
|
80
|
+
fk_name || :"#{object_name}_id"
|
81
|
+
end
|
82
|
+
|
83
|
+
def delete_keys(input_hash, keys)
|
84
|
+
input_hash.slice(*keys).tap do
|
85
|
+
keys.each { |k| input_hash.delete(k) }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|