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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.overcommit.yml +37 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +56 -0
  6. data/.rubocop_todo.yml +26 -0
  7. data/.ruby-gemset +1 -0
  8. data/.ruby-version +1 -0
  9. data/.travis.yml +5 -0
  10. data/CHANGELOG.md +17 -0
  11. data/CODE_OF_CONDUCT.md +74 -0
  12. data/Gemfile +8 -0
  13. data/Gemfile.lock +92 -0
  14. data/Guardfile +30 -0
  15. data/LICENSE.txt +21 -0
  16. data/README.md +114 -0
  17. data/Rakefile +8 -0
  18. data/bin/console +15 -0
  19. data/bin/setup +8 -0
  20. data/entity_schema.gemspec +49 -0
  21. data/lib/entity_schema/class_methods.rb +28 -0
  22. data/lib/entity_schema/dsl.rb +40 -0
  23. data/lib/entity_schema/dsl_helper.rb +26 -0
  24. data/lib/entity_schema/fields/abstract.rb +89 -0
  25. data/lib/entity_schema/fields/builders/.rubocop.yml +7 -0
  26. data/lib/entity_schema/fields/builders/abstract.rb +98 -0
  27. data/lib/entity_schema/fields/builders/belongs_to.rb +91 -0
  28. data/lib/entity_schema/fields/builders/collection.rb +20 -0
  29. data/lib/entity_schema/fields/builders/fk_belongs_to.rb +21 -0
  30. data/lib/entity_schema/fields/builders/object.rb +40 -0
  31. data/lib/entity_schema/fields/builders/object_belongs_to.rb +17 -0
  32. data/lib/entity_schema/fields/builders/property.rb +33 -0
  33. data/lib/entity_schema/fields/collection.rb +35 -0
  34. data/lib/entity_schema/fields/fk_belongs_to.rb +16 -0
  35. data/lib/entity_schema/fields/object.rb +36 -0
  36. data/lib/entity_schema/fields/object_belongs_to.rb +16 -0
  37. data/lib/entity_schema/fields/observer_belongs_to.rb +33 -0
  38. data/lib/entity_schema/fields/property.rb +24 -0
  39. data/lib/entity_schema/instance_methods.rb +42 -0
  40. data/lib/entity_schema/schema.rb +77 -0
  41. data/lib/entity_schema/version.rb +5 -0
  42. data/lib/entity_schema.rb +18 -0
  43. metadata +227 -0
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
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,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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,7 @@
1
+ inherit_from: ../../../../.rubocop.yml
2
+
3
+ Metrics/AbcSize:
4
+ Max: 20
5
+
6
+ Metrics/LineLength:
7
+ Max: 130
@@ -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