rom-support 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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