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.
@@ -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,11 @@
1
+ require 'wisper'
2
+
3
+ module ROM
4
+ module Support
5
+ module Publisher
6
+ def self.included(klass)
7
+ klass.__send__(:include, Wisper::Publisher)
8
+ end
9
+ end
10
+ end
11
+ 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