rom-support 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 +23 -0
- data/.rspec +4 -0
- data/.travis.yml +29 -0
- data/CHANGELOG.md +3 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +35 -0
- data/LICENSE +20 -0
- data/README.md +16 -0
- data/Rakefile +15 -0
- data/lib/rom-support.rb +1 -0
- data/lib/rom/support/array_dataset.rb +41 -0
- data/lib/rom/support/auto_curry.rb +29 -0
- data/lib/rom/support/class_builder.rb +46 -0
- data/lib/rom/support/class_macros.rb +56 -0
- data/lib/rom/support/constants.rb +5 -0
- data/lib/rom/support/data_proxy.rb +102 -0
- data/lib/rom/support/deprecations.rb +36 -0
- data/lib/rom/support/enumerable_dataset.rb +65 -0
- data/lib/rom/support/guarded_inheritance_hook.rb +21 -0
- data/lib/rom/support/inflector.rb +73 -0
- data/lib/rom/support/inheritance_hook.rb +20 -0
- data/lib/rom/support/options.rb +198 -0
- data/lib/rom/support/publisher.rb +11 -0
- data/lib/rom/support/registry.rb +43 -0
- data/lib/rom/support/version.rb +5 -0
- data/rakelib/mutant.rake +16 -0
- data/rakelib/rubocop.rake +18 -0
- data/rom-support.gemspec +22 -0
- data/spec/shared/enumerable_dataset.rb +52 -0
- data/spec/spec_helper.rb +46 -0
- data/spec/support/constant_leak_finder.rb +14 -0
- data/spec/support/mutant.rb +10 -0
- data/spec/unit/rom/support/array_dataset_spec.rb +59 -0
- data/spec/unit/rom/support/class_builder_spec.rb +42 -0
- data/spec/unit/rom/support/enumerable_dataset_spec.rb +15 -0
- data/spec/unit/rom/support/inflector_spec.rb +89 -0
- data/spec/unit/rom/support/options_spec.rb +119 -0
- metadata +128 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
module ROM
|
2
|
+
module Deprecations
|
3
|
+
# @api private
|
4
|
+
def deprecate(old_name, new_name, msg = nil)
|
5
|
+
class_eval do
|
6
|
+
define_method(old_name) do |*args, &block|
|
7
|
+
ROM::Deprecations.announce "#{self.class}##{old_name} is", <<-MSG
|
8
|
+
Please use #{self.class}##{new_name} instead.
|
9
|
+
#{msg}
|
10
|
+
MSG
|
11
|
+
__send__(new_name, *args, &block)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def deprecate_class_method(old_name, new_name, msg = nil)
|
17
|
+
class_eval do
|
18
|
+
define_singleton_method(old_name) do |*args, &block|
|
19
|
+
ROM::Deprecations.announce"#{self}.#{old_name} is", <<-MSG
|
20
|
+
Please use #{self}.#{new_name} instead.
|
21
|
+
#{msg}
|
22
|
+
MSG
|
23
|
+
__send__(new_name, *args, &block)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.announce(name, msg)
|
29
|
+
warn <<-MSG.gsub(/^\s+/, '')
|
30
|
+
#{name} deprecated and will be removed in 1.0.0.
|
31
|
+
#{msg}
|
32
|
+
#{caller.detect { |l| !l.include?('lib/rom')}}
|
33
|
+
MSG
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'rom/support/data_proxy'
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
# A helper module that adds data-proxy behavior to an enumerable object
|
5
|
+
#
|
6
|
+
# This module is intended to be used by gateways
|
7
|
+
#
|
8
|
+
# Class that includes this module can define `row_proc` class method which
|
9
|
+
# must return a proc-like object which will be used to process each element
|
10
|
+
# in the enumerable
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# class MyDataset
|
14
|
+
# include ROM::EnumerableDataset
|
15
|
+
#
|
16
|
+
# def self.row_proc
|
17
|
+
# -> tuple { tuple.each_with_object({}) { |(k,v), h| h[k.to_sym] = v } }
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# ds = MyDataset.new([{ 'name' => 'Jane' }, [:name])
|
22
|
+
# ds.to_a # => { :name => 'Jane' }
|
23
|
+
#
|
24
|
+
# @api public
|
25
|
+
module EnumerableDataset
|
26
|
+
extend DataProxy::ClassMethods
|
27
|
+
include Enumerable
|
28
|
+
|
29
|
+
# Coerce a dataset to an array
|
30
|
+
#
|
31
|
+
# @return [Array]
|
32
|
+
#
|
33
|
+
# @api public
|
34
|
+
alias_method :to_ary, :to_a
|
35
|
+
|
36
|
+
# Included hook which extends a class with DataProxy behavior
|
37
|
+
#
|
38
|
+
# This module can also be included into other modules so we apply the
|
39
|
+
# extension only for classes
|
40
|
+
#
|
41
|
+
# @api private
|
42
|
+
def self.included(klass)
|
43
|
+
return unless klass.is_a?(Class)
|
44
|
+
|
45
|
+
klass.class_eval do
|
46
|
+
include Options
|
47
|
+
include DataProxy
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
forward :take
|
52
|
+
|
53
|
+
[
|
54
|
+
:chunk, :collect, :collect_concat, :drop_while, :find_all, :flat_map,
|
55
|
+
:grep, :map, :reject, :select, :sort, :sort_by, :take_while
|
56
|
+
].each do |method|
|
57
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
58
|
+
def #{method}(*args, &block)
|
59
|
+
return to_enum unless block
|
60
|
+
self.class.new(super(*args, &block), options)
|
61
|
+
end
|
62
|
+
RUBY
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'rom/support/publisher'
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
module Support
|
5
|
+
module GuardedInheritanceHook
|
6
|
+
def self.extended(base)
|
7
|
+
base.class_eval <<-RUBY
|
8
|
+
class << self
|
9
|
+
include ROM::Support::Publisher
|
10
|
+
|
11
|
+
def inherited(klass)
|
12
|
+
super
|
13
|
+
return if klass.superclass == #{base}
|
14
|
+
#{base}.__send__(:broadcast, :inherited, klass)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
RUBY
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module ROM
|
2
|
+
# Helper module providing thin interface around an inflection backend.
|
3
|
+
#
|
4
|
+
# @private
|
5
|
+
module Inflector
|
6
|
+
BACKENDS = {
|
7
|
+
activesupport: [
|
8
|
+
'active_support/inflector',
|
9
|
+
proc { ::ActiveSupport::Inflector }
|
10
|
+
],
|
11
|
+
inflecto: [
|
12
|
+
'inflecto',
|
13
|
+
proc { ::Inflecto }
|
14
|
+
]
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
def self.realize_backend(path, inflector_backend_factory)
|
18
|
+
require path
|
19
|
+
inflector_backend_factory.call
|
20
|
+
rescue LoadError
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.detect_backend
|
25
|
+
BACKENDS.find do |_, (path, inflector_class)|
|
26
|
+
backend = realize_backend(path, inflector_class)
|
27
|
+
break backend if backend
|
28
|
+
end ||
|
29
|
+
raise(LoadError,
|
30
|
+
"No inflector library could be found: "\
|
31
|
+
"please install either the `inflecto` or `activesupport` gem.")
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.select_backend(name = nil)
|
35
|
+
if name && !BACKENDS.key?(name)
|
36
|
+
raise NameError, "Invalid inflector library selection: '#{name}'"
|
37
|
+
end
|
38
|
+
@inflector = name ? realize_backend(*BACKENDS[name]) : detect_backend
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.inflector
|
42
|
+
defined?(@inflector) && @inflector || select_backend
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.camelize(input)
|
46
|
+
inflector.camelize(input)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.underscore(input)
|
50
|
+
inflector.underscore(input)
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.singularize(input)
|
54
|
+
inflector.singularize(input)
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.pluralize(input)
|
58
|
+
inflector.pluralize(input)
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.demodulize(input)
|
62
|
+
inflector.demodulize(input)
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.constantize(input)
|
66
|
+
inflector.constantize(input)
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.classify(input)
|
70
|
+
inflector.classify(input)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rom/support/publisher'
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
module Support
|
5
|
+
module InheritanceHook
|
6
|
+
def self.extended(base)
|
7
|
+
base.class_eval <<-RUBY
|
8
|
+
class << self
|
9
|
+
include ROM::Support::Publisher
|
10
|
+
|
11
|
+
def inherited(klass)
|
12
|
+
super
|
13
|
+
#{base}.__send__(:broadcast, :inherited, klass)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
RUBY
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
module ROM
|
2
|
+
# Helper module for classes with a constructor accepting option hash
|
3
|
+
#
|
4
|
+
# This allows us to DRY up code as option hash is a very common pattern used
|
5
|
+
# across the codebase. It is an internal implementation detail not meant to
|
6
|
+
# be used outside of ROM
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# class User
|
10
|
+
# include Options
|
11
|
+
#
|
12
|
+
# option :name, type: String, reader: true
|
13
|
+
# option :admin, allow: [true, false], reader: true, default: false
|
14
|
+
#
|
15
|
+
# def initialize(options={})
|
16
|
+
# super
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# user = User.new(name: 'Piotr')
|
21
|
+
# user.name # => "Piotr"
|
22
|
+
# user.admin # => false
|
23
|
+
#
|
24
|
+
# @api public
|
25
|
+
module Options
|
26
|
+
InvalidOptionValueError = Class.new(StandardError)
|
27
|
+
InvalidOptionKeyError = Class.new(StandardError)
|
28
|
+
|
29
|
+
# @return [Hash<Option>] Option definitions
|
30
|
+
#
|
31
|
+
# @api public
|
32
|
+
attr_reader :options
|
33
|
+
|
34
|
+
def self.included(klass)
|
35
|
+
klass.extend ClassMethods
|
36
|
+
klass.option_definitions = Definitions.new
|
37
|
+
end
|
38
|
+
|
39
|
+
# Defines a single option
|
40
|
+
#
|
41
|
+
# @api private
|
42
|
+
class Option
|
43
|
+
attr_reader :name, :type, :allow, :default
|
44
|
+
|
45
|
+
def initialize(name, options = {})
|
46
|
+
@name = name
|
47
|
+
@type = options.fetch(:type) { Object }
|
48
|
+
@reader = options.fetch(:reader) { false }
|
49
|
+
@allow = options.fetch(:allow) { [] }
|
50
|
+
@default = options.fetch(:default) { Undefined }
|
51
|
+
@ivar = :"@#{name}" if @reader
|
52
|
+
end
|
53
|
+
|
54
|
+
def reader?
|
55
|
+
@reader
|
56
|
+
end
|
57
|
+
|
58
|
+
def assign_reader_value(object, value)
|
59
|
+
object.instance_variable_set(@ivar, value)
|
60
|
+
end
|
61
|
+
|
62
|
+
def default?
|
63
|
+
@default != Undefined
|
64
|
+
end
|
65
|
+
|
66
|
+
def default_value(object)
|
67
|
+
default.is_a?(Proc) ? default.call(object) : default
|
68
|
+
end
|
69
|
+
|
70
|
+
def type_matches?(value)
|
71
|
+
value.is_a?(type)
|
72
|
+
end
|
73
|
+
|
74
|
+
def allow?(value)
|
75
|
+
allow.empty? || allow.include?(value)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Manage all available options
|
80
|
+
#
|
81
|
+
# @api private
|
82
|
+
class Definitions
|
83
|
+
def initialize
|
84
|
+
@options = {}
|
85
|
+
end
|
86
|
+
|
87
|
+
def initialize_copy(source)
|
88
|
+
super
|
89
|
+
@options = @options.dup
|
90
|
+
end
|
91
|
+
|
92
|
+
def define(option)
|
93
|
+
@options[option.name] = option
|
94
|
+
end
|
95
|
+
|
96
|
+
def process(object, options)
|
97
|
+
ensure_known_options(options)
|
98
|
+
|
99
|
+
each do |name, option|
|
100
|
+
if option.default? && !options.key?(name)
|
101
|
+
options[name] = option.default_value(object)
|
102
|
+
end
|
103
|
+
|
104
|
+
if options.key?(name)
|
105
|
+
validate_option_value(option, name, options[name])
|
106
|
+
end
|
107
|
+
|
108
|
+
option.assign_reader_value(object, options[name]) if option.reader?
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def names
|
113
|
+
@options.keys
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def each(&block)
|
119
|
+
@options.each(&block)
|
120
|
+
end
|
121
|
+
|
122
|
+
def ensure_known_options(options)
|
123
|
+
options.each_key do |name|
|
124
|
+
@options.fetch(name) do
|
125
|
+
raise InvalidOptionKeyError,
|
126
|
+
"#{name.inspect} is not a valid option"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def validate_option_value(option, name, value)
|
132
|
+
unless option.type_matches?(value)
|
133
|
+
raise InvalidOptionValueError,
|
134
|
+
"#{name.inspect}:#{value.inspect} has incorrect type"
|
135
|
+
end
|
136
|
+
|
137
|
+
unless option.allow?(value)
|
138
|
+
raise InvalidOptionValueError,
|
139
|
+
"#{name.inspect}:#{value.inspect} has incorrect value"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# @api private
|
145
|
+
module ClassMethods
|
146
|
+
# Available options
|
147
|
+
#
|
148
|
+
# @return [Definitions]
|
149
|
+
#
|
150
|
+
# @api private
|
151
|
+
attr_accessor :option_definitions
|
152
|
+
|
153
|
+
# Defines an option
|
154
|
+
#
|
155
|
+
# @param [Symbol] name option name
|
156
|
+
#
|
157
|
+
# @param [Hash] settings option settings
|
158
|
+
# @option settings [Class] :type Restrict option type. Default: +Object+
|
159
|
+
# @option settings [Boolean] :reader Define a reader? Default: +false+
|
160
|
+
# @option settings [Array] :allow Allow certain values. Default: Allow anything
|
161
|
+
# @option settings [Object] :default Set default value for missing option
|
162
|
+
#
|
163
|
+
# @api public
|
164
|
+
def option(name, settings = {})
|
165
|
+
option = Option.new(name, settings)
|
166
|
+
option_definitions.define(option)
|
167
|
+
attr_reader(name) if option.reader?
|
168
|
+
end
|
169
|
+
|
170
|
+
# @api private
|
171
|
+
def inherited(descendant)
|
172
|
+
descendant.option_definitions = option_definitions.dup
|
173
|
+
super
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Initialize options provided as optional last argument hash
|
178
|
+
#
|
179
|
+
# @example
|
180
|
+
# class Commands
|
181
|
+
# include Options
|
182
|
+
#
|
183
|
+
# # ...
|
184
|
+
#
|
185
|
+
# def initialize(relations, options={})
|
186
|
+
# @relation = relation
|
187
|
+
# super
|
188
|
+
# end
|
189
|
+
# end
|
190
|
+
#
|
191
|
+
# @param [Array] args
|
192
|
+
def initialize(*args)
|
193
|
+
options = args.last ? args.last.dup : {}
|
194
|
+
self.class.option_definitions.process(self, options)
|
195
|
+
@options = options.freeze
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module ROM
|
2
|
+
# @api private
|
3
|
+
class Registry
|
4
|
+
include Enumerable
|
5
|
+
include Equalizer.new(:elements)
|
6
|
+
|
7
|
+
class ElementNotFoundError < KeyError
|
8
|
+
def initialize(key, name)
|
9
|
+
super("#{key.inspect} doesn't exist in #{name} registry")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :elements, :name
|
14
|
+
|
15
|
+
def initialize(elements = {}, name = self.class.name)
|
16
|
+
@elements = elements
|
17
|
+
@name = name
|
18
|
+
end
|
19
|
+
|
20
|
+
def each(&block)
|
21
|
+
return to_enum unless block
|
22
|
+
elements.each { |element| yield(element) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def key?(name)
|
26
|
+
elements.key?(name)
|
27
|
+
end
|
28
|
+
|
29
|
+
def [](key)
|
30
|
+
elements.fetch(key) { raise ElementNotFoundError.new(key, name) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def respond_to_missing?(name, include_private = false)
|
34
|
+
elements.key?(name) || super
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def method_missing(name, *)
|
40
|
+
elements.fetch(name) { super }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|