rom 0.5.0 → 0.6.0.beta1
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 +4 -4
- data/.rubocop.yml +19 -15
- data/.rubocop_todo.yml +28 -0
- data/.travis.yml +8 -1
- data/CHANGELOG.md +40 -0
- data/Gemfile +10 -2
- data/Guardfile +12 -10
- data/README.md +42 -43
- data/Rakefile +13 -23
- data/lib/rom.rb +19 -27
- data/lib/rom/command.rb +118 -0
- data/lib/rom/command_registry.rb +13 -27
- data/lib/rom/commands.rb +1 -59
- data/lib/rom/commands/abstract.rb +147 -0
- data/lib/rom/commands/composite.rb +47 -0
- data/lib/rom/commands/create.rb +2 -17
- data/lib/rom/commands/delete.rb +5 -25
- data/lib/rom/commands/result.rb +5 -5
- data/lib/rom/commands/update.rb +3 -27
- data/lib/rom/constants.rb +19 -0
- data/lib/rom/env.rb +85 -35
- data/lib/rom/global.rb +173 -42
- data/lib/rom/header.rb +5 -5
- data/lib/rom/header/attribute.rb +2 -2
- data/lib/rom/lint/enumerable_dataset.rb +52 -0
- data/lib/rom/lint/linter.rb +64 -0
- data/lib/rom/lint/repository.rb +78 -0
- data/lib/rom/lint/spec.rb +20 -0
- data/lib/rom/lint/test.rb +98 -0
- data/lib/rom/mapper.rb +32 -5
- data/lib/rom/mapper/attribute_dsl.rb +240 -0
- data/lib/rom/mapper/dsl.rb +100 -0
- data/lib/rom/mapper/model_dsl.rb +55 -0
- data/lib/rom/mapper_registry.rb +8 -1
- data/lib/rom/memory.rb +4 -0
- data/lib/rom/memory/commands.rb +46 -0
- data/lib/rom/memory/dataset.rb +72 -0
- data/lib/rom/memory/relation.rb +44 -0
- data/lib/rom/memory/repository.rb +62 -0
- data/lib/rom/memory/storage.rb +57 -0
- data/lib/rom/model_builder.rb +44 -5
- data/lib/rom/processor.rb +1 -1
- data/lib/rom/processor/transproc.rb +109 -16
- data/lib/rom/reader.rb +91 -39
- data/lib/rom/relation.rb +165 -26
- data/lib/rom/relation/composite.rb +132 -0
- data/lib/rom/relation/curried.rb +48 -0
- data/lib/rom/relation/lazy.rb +173 -0
- data/lib/rom/relation/loaded.rb +75 -0
- data/lib/rom/relation/registry_reader.rb +23 -0
- data/lib/rom/repository.rb +93 -34
- data/lib/rom/setup.rb +54 -98
- data/lib/rom/setup/finalize.rb +85 -76
- data/lib/rom/setup_dsl/command.rb +36 -0
- data/lib/rom/setup_dsl/command_dsl.rb +34 -0
- data/lib/rom/setup_dsl/mapper.rb +32 -0
- data/lib/rom/setup_dsl/mapper_dsl.rb +30 -0
- data/lib/rom/setup_dsl/relation.rb +21 -0
- data/lib/rom/setup_dsl/setup.rb +75 -0
- data/lib/rom/support/array_dataset.rb +38 -0
- data/lib/rom/support/class_builder.rb +44 -0
- data/lib/rom/support/class_macros.rb +56 -0
- data/lib/rom/support/data_proxy.rb +102 -0
- data/lib/rom/support/enumerable_dataset.rb +58 -0
- data/lib/rom/support/inflector.rb +73 -0
- data/lib/rom/support/options.rb +188 -0
- data/lib/rom/support/registry.rb +4 -8
- data/lib/rom/version.rb +1 -1
- data/rakelib/benchmark.rake +13 -0
- data/rakelib/mutant.rake +16 -0
- data/rakelib/rubocop.rake +18 -0
- data/rom.gemspec +4 -7
- data/spec/integration/commands/create_spec.rb +32 -24
- data/spec/integration/commands/delete_spec.rb +15 -7
- data/spec/integration/commands/update_spec.rb +13 -11
- data/spec/integration/mappers/deep_embedded_spec.rb +4 -11
- data/spec/integration/mappers/definition_dsl_spec.rb +31 -44
- data/spec/integration/mappers/embedded_spec.rb +9 -24
- data/spec/integration/mappers/group_spec.rb +22 -30
- data/spec/integration/mappers/prefixing_attributes_spec.rb +18 -23
- data/spec/integration/mappers/renaming_attributes_spec.rb +23 -38
- data/spec/integration/mappers/symbolizing_attributes_spec.rb +18 -24
- data/spec/integration/mappers/wrap_spec.rb +22 -30
- data/spec/integration/multi_repo_spec.rb +15 -37
- data/spec/integration/relations/reading_spec.rb +82 -14
- data/spec/integration/repositories/extending_relations_spec.rb +50 -0
- data/spec/integration/{adapters → repositories}/setting_logger_spec.rb +6 -5
- data/spec/integration/setup_spec.rb +59 -62
- data/spec/shared/enumerable_dataset.rb +49 -0
- data/spec/shared/one_behavior.rb +26 -0
- data/spec/shared/users_and_tasks.rb +11 -23
- data/spec/spec_helper.rb +16 -7
- data/spec/support/constant_leak_finder.rb +14 -0
- data/spec/test/memory_repository_lint_test.rb +27 -0
- data/spec/unit/rom/command_registry_spec.rb +44 -0
- data/spec/unit/rom/commands/result_spec.rb +14 -0
- data/spec/unit/rom/commands_spec.rb +174 -0
- data/spec/unit/rom/env_spec.rb +40 -7
- data/spec/unit/rom/global_spec.rb +14 -0
- data/spec/unit/rom/{mapper_builder_spec.rb → mapper/dsl_spec.rb} +52 -38
- data/spec/unit/rom/mapper_spec.rb +51 -10
- data/spec/unit/rom/{adapter/memory → memory}/dataset_spec.rb +6 -4
- data/spec/unit/rom/memory/repository_spec.rb +12 -0
- data/spec/unit/rom/memory/storage_spec.rb +45 -0
- data/spec/unit/rom/model_builder_spec.rb +4 -3
- data/spec/unit/rom/processor/transproc_spec.rb +1 -0
- data/spec/unit/rom/reader_spec.rb +97 -24
- data/spec/unit/rom/relation/composite_spec.rb +65 -0
- data/spec/unit/rom/relation/lazy_spec.rb +145 -0
- data/spec/unit/rom/relation/loaded_spec.rb +28 -0
- data/spec/unit/rom/relation_spec.rb +111 -6
- data/spec/unit/rom/repository_spec.rb +59 -9
- data/spec/unit/rom/setup_spec.rb +99 -11
- 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 +17 -0
- data/spec/unit/rom/support/inflector_spec.rb +89 -0
- data/spec/unit/rom/support/options_spec.rb +119 -0
- metadata +74 -112
- data/lib/rom/adapter.rb +0 -191
- data/lib/rom/adapter/memory.rb +0 -32
- data/lib/rom/adapter/memory/commands.rb +0 -31
- data/lib/rom/adapter/memory/dataset.rb +0 -67
- data/lib/rom/adapter/memory/storage.rb +0 -26
- data/lib/rom/commands/with_options.rb +0 -18
- data/lib/rom/config.rb +0 -70
- data/lib/rom/mapper_builder.rb +0 -52
- data/lib/rom/mapper_builder/mapper_dsl.rb +0 -114
- data/lib/rom/mapper_builder/model_dsl.rb +0 -29
- data/lib/rom/reader_builder.rb +0 -48
- data/lib/rom/relation_builder.rb +0 -62
- data/lib/rom/setup/base_relation_dsl.rb +0 -46
- data/lib/rom/setup/command_dsl.rb +0 -46
- data/lib/rom/setup/mapper_dsl.rb +0 -19
- data/lib/rom/setup/relation_dsl.rb +0 -20
- data/lib/rom/setup/schema_dsl.rb +0 -33
- data/spec/integration/adapters/extending_relations_spec.rb +0 -41
- data/spec/integration/commands/try_spec.rb +0 -27
- data/spec/integration/schema_spec.rb +0 -77
- data/spec/unit/config_spec.rb +0 -60
- data/spec/unit/rom/adapter_spec.rb +0 -79
- data/spec/unit/rom_spec.rb +0 -14
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'rom/support/enumerable_dataset'
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
# A helper module that adds data-proxy behavior to an array-like object
|
5
|
+
#
|
6
|
+
# @see EnumerableDataset
|
7
|
+
#
|
8
|
+
# @api public
|
9
|
+
module ArrayDataset
|
10
|
+
extend DataProxy::ClassMethods
|
11
|
+
include EnumerableDataset
|
12
|
+
|
13
|
+
# Extends the class with data-proxy behavior
|
14
|
+
#
|
15
|
+
# @api private
|
16
|
+
def self.included(klass)
|
17
|
+
klass.send(:include, DataProxy)
|
18
|
+
end
|
19
|
+
|
20
|
+
forward(
|
21
|
+
:*, :+, :-, :compact, :compact!, :flatten, :flatten!, :length, :pop,
|
22
|
+
:reverse, :reverse!, :sample, :select!, :size, :shift, :shuffle, :shuffle!,
|
23
|
+
:slice, :slice!, :sort!, :sort_by!, :uniq, :uniq!, :unshift, :values_at
|
24
|
+
)
|
25
|
+
|
26
|
+
[
|
27
|
+
:map!, :combination, :cycle, :delete_if, :keep_if, :permutation, :reject!,
|
28
|
+
:select!, :sort_by!
|
29
|
+
].each do |method|
|
30
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
31
|
+
def #{method}(*args, &block)
|
32
|
+
return to_enum unless block
|
33
|
+
self.class.new(data.send(:#{method}, *args, &block))
|
34
|
+
end
|
35
|
+
RUBY
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module ROM
|
2
|
+
# Internal support class for generating classes
|
3
|
+
#
|
4
|
+
# @private
|
5
|
+
class ClassBuilder
|
6
|
+
include Options
|
7
|
+
|
8
|
+
option :name, type: String, reader: true
|
9
|
+
option :parent, type: Class, reader: true, parent: Object
|
10
|
+
|
11
|
+
# Generate a class based on options
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# builder = ROM::ClasBuilder.new(name: 'MyClass')
|
15
|
+
#
|
16
|
+
# klass = builder.call
|
17
|
+
# klass.name # => "MyClass"
|
18
|
+
#
|
19
|
+
# @return [Class]
|
20
|
+
#
|
21
|
+
# @api private
|
22
|
+
def call
|
23
|
+
klass = Class.new(parent)
|
24
|
+
|
25
|
+
klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
26
|
+
def self.name
|
27
|
+
#{name.inspect}
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.inspect
|
31
|
+
name
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.to_s
|
35
|
+
name
|
36
|
+
end
|
37
|
+
RUBY
|
38
|
+
|
39
|
+
yield(klass) if block_given?
|
40
|
+
|
41
|
+
klass
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module ROM
|
2
|
+
# Internal support module for class-level settings
|
3
|
+
#
|
4
|
+
# @private
|
5
|
+
module ClassMacros
|
6
|
+
# Specify what macros a class will use
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# class MyClass
|
10
|
+
# extend ROM::ClassMacros
|
11
|
+
#
|
12
|
+
# defines :one, :two
|
13
|
+
#
|
14
|
+
# one 1
|
15
|
+
# two 2
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# class OtherClass < MyClass
|
19
|
+
# two 'two'
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# MyClass.one # => 1
|
23
|
+
# MyClass.two # => 2
|
24
|
+
#
|
25
|
+
# OtherClass.one # => 1
|
26
|
+
# OtherClass.two # => 'two'
|
27
|
+
#
|
28
|
+
# @api private
|
29
|
+
def defines(*args)
|
30
|
+
mod = Module.new
|
31
|
+
|
32
|
+
args.each do |name|
|
33
|
+
mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
34
|
+
def #{name}(value = Undefined)
|
35
|
+
if value == Undefined
|
36
|
+
defined?(@#{name}) && @#{name}
|
37
|
+
else
|
38
|
+
@#{name} = value
|
39
|
+
end
|
40
|
+
end
|
41
|
+
RUBY
|
42
|
+
end
|
43
|
+
|
44
|
+
delegates = args.map { |name| "klass.#{name}(#{name})" }.join("\n")
|
45
|
+
|
46
|
+
mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
47
|
+
def inherited(klass)
|
48
|
+
super
|
49
|
+
#{delegates}
|
50
|
+
end
|
51
|
+
RUBY
|
52
|
+
|
53
|
+
extend(mod)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'equalizer'
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
# Helper module for dataset classes
|
5
|
+
#
|
6
|
+
# It provides a constructor accepting data, header and an optional row_proc.
|
7
|
+
# This module is used internally by EnumerableDataset and ArrayDataset.
|
8
|
+
#
|
9
|
+
# @private
|
10
|
+
module DataProxy
|
11
|
+
NON_FORWARDABLE = [
|
12
|
+
:each, :to_a, :to_ary, :kind_of?, :instance_of?, :is_a?
|
13
|
+
].freeze
|
14
|
+
|
15
|
+
# @return [Object] Data object for the iterator
|
16
|
+
#
|
17
|
+
# @api private
|
18
|
+
attr_reader :data
|
19
|
+
|
20
|
+
# @return [Proc] tuple processing proc
|
21
|
+
#
|
22
|
+
# @api private
|
23
|
+
attr_reader :row_proc
|
24
|
+
|
25
|
+
# Extends the class with `forward` DSL and Equalizer using `data` attribute
|
26
|
+
#
|
27
|
+
# @see ClassMethods#forward
|
28
|
+
#
|
29
|
+
# @api private
|
30
|
+
def self.included(klass)
|
31
|
+
klass.class_eval do
|
32
|
+
extend ClassMethods
|
33
|
+
include Equalizer.new(:data)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Constructor for dataset objects
|
38
|
+
#
|
39
|
+
# @param [Object] data
|
40
|
+
# @param [Proc] row_proc processing proc
|
41
|
+
#
|
42
|
+
# @api private
|
43
|
+
def initialize(data, row_proc = self.class.row_proc)
|
44
|
+
@data = data
|
45
|
+
@row_proc = row_proc
|
46
|
+
end
|
47
|
+
|
48
|
+
# Iterate over data using row_proc
|
49
|
+
#
|
50
|
+
# @return [Enumerator] if block is not given
|
51
|
+
#
|
52
|
+
# @api private
|
53
|
+
def each
|
54
|
+
return to_enum unless block_given?
|
55
|
+
data.each { |tuple| yield(row_proc[tuple]) }
|
56
|
+
end
|
57
|
+
|
58
|
+
module ClassMethods
|
59
|
+
# Default no-op tuple proc
|
60
|
+
#
|
61
|
+
# @return [Proc]
|
62
|
+
#
|
63
|
+
# @api private
|
64
|
+
def row_proc
|
65
|
+
-> tuple { tuple }
|
66
|
+
end
|
67
|
+
|
68
|
+
# Forward provided methods to the underlaying data object
|
69
|
+
#
|
70
|
+
# @example
|
71
|
+
#
|
72
|
+
# class MyDataset
|
73
|
+
# include DataProxy
|
74
|
+
#
|
75
|
+
# forward(:find_all, :map)
|
76
|
+
# end
|
77
|
+
#
|
78
|
+
# @return [undefined]
|
79
|
+
#
|
80
|
+
# @api public
|
81
|
+
def forward(*methods)
|
82
|
+
# FIXME: we should probably raise if one of the non-forwardable methods
|
83
|
+
# was provided
|
84
|
+
(methods - NON_FORWARDABLE).each do |method_name|
|
85
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
86
|
+
def #{method_name}(*args, &block)
|
87
|
+
response = data.public_send(#{method_name.inspect}, *args, &block)
|
88
|
+
|
89
|
+
if response.equal?(data)
|
90
|
+
self
|
91
|
+
elsif response.is_a?(data.class)
|
92
|
+
self.class.new(response)
|
93
|
+
else
|
94
|
+
response
|
95
|
+
end
|
96
|
+
end
|
97
|
+
RUBY
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,58 @@
|
|
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 repositories
|
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
|
+
include Enumerable
|
27
|
+
|
28
|
+
# Coerce a dataset to an array
|
29
|
+
#
|
30
|
+
# @return [Array]
|
31
|
+
#
|
32
|
+
# @api public
|
33
|
+
alias_method :to_ary, :to_a
|
34
|
+
|
35
|
+
# Included hook which extends a class with DataProxy behavior
|
36
|
+
#
|
37
|
+
# This module can also be included into other modules so we apply the
|
38
|
+
# extension only for classes
|
39
|
+
#
|
40
|
+
# @api private
|
41
|
+
def self.included(klass)
|
42
|
+
return unless klass.is_a?(Class)
|
43
|
+
klass.send(:include, DataProxy)
|
44
|
+
end
|
45
|
+
|
46
|
+
[
|
47
|
+
:chunk, :collect, :collect_concat, :drop_while, :find_all, :flat_map,
|
48
|
+
:grep, :map, :reject, :select, :sort, :sort_by, :take, :take_while
|
49
|
+
].each do |method|
|
50
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
51
|
+
def #{method}(*args, &block)
|
52
|
+
return to_enum unless block
|
53
|
+
self.class.new(super(*args, &block))
|
54
|
+
end
|
55
|
+
RUBY
|
56
|
+
end
|
57
|
+
end
|
58
|
+
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
|
+
@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,188 @@
|
|
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
|
+
# @return [Hash<Option>] Option definitions
|
27
|
+
#
|
28
|
+
# @api public
|
29
|
+
attr_reader :options
|
30
|
+
|
31
|
+
def self.included(klass)
|
32
|
+
klass.extend ClassMethods
|
33
|
+
klass.option_definitions = Definitions.new
|
34
|
+
end
|
35
|
+
|
36
|
+
# Defines a single option
|
37
|
+
#
|
38
|
+
# @api private
|
39
|
+
class Option
|
40
|
+
attr_reader :name, :type, :allow, :default
|
41
|
+
|
42
|
+
def initialize(name, options = {})
|
43
|
+
@name = name
|
44
|
+
@type = options.fetch(:type) { Object }
|
45
|
+
@reader = options.fetch(:reader) { false }
|
46
|
+
@allow = options.fetch(:allow) { [] }
|
47
|
+
@default = options.fetch(:default) { Undefined }
|
48
|
+
end
|
49
|
+
|
50
|
+
def reader?
|
51
|
+
@reader
|
52
|
+
end
|
53
|
+
|
54
|
+
def default?
|
55
|
+
@default != Undefined
|
56
|
+
end
|
57
|
+
|
58
|
+
def default_value(object)
|
59
|
+
default.is_a?(Proc) ? default.call(object) : default
|
60
|
+
end
|
61
|
+
|
62
|
+
def type_matches?(value)
|
63
|
+
value.is_a?(type)
|
64
|
+
end
|
65
|
+
|
66
|
+
def allow?(value)
|
67
|
+
allow.none? || allow.include?(value)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Manage all available options
|
72
|
+
#
|
73
|
+
# @api private
|
74
|
+
class Definitions
|
75
|
+
def initialize
|
76
|
+
@options = {}
|
77
|
+
end
|
78
|
+
|
79
|
+
def initialize_copy(source)
|
80
|
+
super
|
81
|
+
@options = @options.dup
|
82
|
+
end
|
83
|
+
|
84
|
+
def define(option)
|
85
|
+
@options[option.name] = option
|
86
|
+
end
|
87
|
+
|
88
|
+
def validate_options(options)
|
89
|
+
options.each do |name, value|
|
90
|
+
validate_option_value(name, value)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def set_defaults(object, options)
|
95
|
+
each do |name, option|
|
96
|
+
next unless option.default? && !options.key?(name)
|
97
|
+
options[name] = option.default_value(object)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def set_option_values(object, options)
|
102
|
+
each do |name, option|
|
103
|
+
object.instance_variable_set("@#{name}", options[name]) if option.reader?
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def each(&block)
|
110
|
+
@options.each(&block)
|
111
|
+
end
|
112
|
+
|
113
|
+
def validate_option_value(name, value)
|
114
|
+
option = @options.fetch(name) do
|
115
|
+
raise InvalidOptionKeyError,
|
116
|
+
"#{name.inspect} is not a valid option"
|
117
|
+
end
|
118
|
+
|
119
|
+
unless option.type_matches?(value)
|
120
|
+
raise InvalidOptionValueError,
|
121
|
+
"#{name.inspect}:#{value.inspect} has incorrect type"
|
122
|
+
end
|
123
|
+
|
124
|
+
unless option.allow?(value)
|
125
|
+
raise InvalidOptionValueError,
|
126
|
+
"#{name.inspect}:#{value.inspect} has incorrect value"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# @api private
|
132
|
+
module ClassMethods
|
133
|
+
# Available options
|
134
|
+
#
|
135
|
+
# @return [Definitions]
|
136
|
+
#
|
137
|
+
# @api private
|
138
|
+
attr_accessor :option_definitions
|
139
|
+
|
140
|
+
# Defines an option
|
141
|
+
#
|
142
|
+
# @param [Symbol] name option name
|
143
|
+
#
|
144
|
+
# @param [Hash] settings option settings
|
145
|
+
# @option settings [Class] :type Restrict option type. Default: +Object+
|
146
|
+
# @option settings [Boolean] :reader Define a reader? Default: +false+
|
147
|
+
# @option settings [Array] :allow Allow certain values. Default: Allow anything
|
148
|
+
# @option settings [Object] :default Set default value for missing option
|
149
|
+
#
|
150
|
+
# @api public
|
151
|
+
def option(name, settings = {})
|
152
|
+
option = Option.new(name, settings)
|
153
|
+
option_definitions.define(option)
|
154
|
+
attr_reader(name) if option.reader?
|
155
|
+
end
|
156
|
+
|
157
|
+
# @api private
|
158
|
+
def inherited(descendant)
|
159
|
+
descendant.option_definitions = option_definitions.dup
|
160
|
+
super
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Initialize options provided as optional last argument hash
|
165
|
+
#
|
166
|
+
# @example
|
167
|
+
# class Commands
|
168
|
+
# include Options
|
169
|
+
#
|
170
|
+
# # ...
|
171
|
+
#
|
172
|
+
# def initialize(relations, options={})
|
173
|
+
# @relation = relation
|
174
|
+
# super
|
175
|
+
# end
|
176
|
+
# end
|
177
|
+
#
|
178
|
+
# @param [Array] args
|
179
|
+
def initialize(*args)
|
180
|
+
options = args.last ? args.last.dup : {}
|
181
|
+
definitions = self.class.option_definitions
|
182
|
+
definitions.set_defaults(self, options)
|
183
|
+
definitions.validate_options(options)
|
184
|
+
definitions.set_option_values(self, options)
|
185
|
+
@options = options.freeze
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|