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.
Files changed (142) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +19 -15
  3. data/.rubocop_todo.yml +28 -0
  4. data/.travis.yml +8 -1
  5. data/CHANGELOG.md +40 -0
  6. data/Gemfile +10 -2
  7. data/Guardfile +12 -10
  8. data/README.md +42 -43
  9. data/Rakefile +13 -23
  10. data/lib/rom.rb +19 -27
  11. data/lib/rom/command.rb +118 -0
  12. data/lib/rom/command_registry.rb +13 -27
  13. data/lib/rom/commands.rb +1 -59
  14. data/lib/rom/commands/abstract.rb +147 -0
  15. data/lib/rom/commands/composite.rb +47 -0
  16. data/lib/rom/commands/create.rb +2 -17
  17. data/lib/rom/commands/delete.rb +5 -25
  18. data/lib/rom/commands/result.rb +5 -5
  19. data/lib/rom/commands/update.rb +3 -27
  20. data/lib/rom/constants.rb +19 -0
  21. data/lib/rom/env.rb +85 -35
  22. data/lib/rom/global.rb +173 -42
  23. data/lib/rom/header.rb +5 -5
  24. data/lib/rom/header/attribute.rb +2 -2
  25. data/lib/rom/lint/enumerable_dataset.rb +52 -0
  26. data/lib/rom/lint/linter.rb +64 -0
  27. data/lib/rom/lint/repository.rb +78 -0
  28. data/lib/rom/lint/spec.rb +20 -0
  29. data/lib/rom/lint/test.rb +98 -0
  30. data/lib/rom/mapper.rb +32 -5
  31. data/lib/rom/mapper/attribute_dsl.rb +240 -0
  32. data/lib/rom/mapper/dsl.rb +100 -0
  33. data/lib/rom/mapper/model_dsl.rb +55 -0
  34. data/lib/rom/mapper_registry.rb +8 -1
  35. data/lib/rom/memory.rb +4 -0
  36. data/lib/rom/memory/commands.rb +46 -0
  37. data/lib/rom/memory/dataset.rb +72 -0
  38. data/lib/rom/memory/relation.rb +44 -0
  39. data/lib/rom/memory/repository.rb +62 -0
  40. data/lib/rom/memory/storage.rb +57 -0
  41. data/lib/rom/model_builder.rb +44 -5
  42. data/lib/rom/processor.rb +1 -1
  43. data/lib/rom/processor/transproc.rb +109 -16
  44. data/lib/rom/reader.rb +91 -39
  45. data/lib/rom/relation.rb +165 -26
  46. data/lib/rom/relation/composite.rb +132 -0
  47. data/lib/rom/relation/curried.rb +48 -0
  48. data/lib/rom/relation/lazy.rb +173 -0
  49. data/lib/rom/relation/loaded.rb +75 -0
  50. data/lib/rom/relation/registry_reader.rb +23 -0
  51. data/lib/rom/repository.rb +93 -34
  52. data/lib/rom/setup.rb +54 -98
  53. data/lib/rom/setup/finalize.rb +85 -76
  54. data/lib/rom/setup_dsl/command.rb +36 -0
  55. data/lib/rom/setup_dsl/command_dsl.rb +34 -0
  56. data/lib/rom/setup_dsl/mapper.rb +32 -0
  57. data/lib/rom/setup_dsl/mapper_dsl.rb +30 -0
  58. data/lib/rom/setup_dsl/relation.rb +21 -0
  59. data/lib/rom/setup_dsl/setup.rb +75 -0
  60. data/lib/rom/support/array_dataset.rb +38 -0
  61. data/lib/rom/support/class_builder.rb +44 -0
  62. data/lib/rom/support/class_macros.rb +56 -0
  63. data/lib/rom/support/data_proxy.rb +102 -0
  64. data/lib/rom/support/enumerable_dataset.rb +58 -0
  65. data/lib/rom/support/inflector.rb +73 -0
  66. data/lib/rom/support/options.rb +188 -0
  67. data/lib/rom/support/registry.rb +4 -8
  68. data/lib/rom/version.rb +1 -1
  69. data/rakelib/benchmark.rake +13 -0
  70. data/rakelib/mutant.rake +16 -0
  71. data/rakelib/rubocop.rake +18 -0
  72. data/rom.gemspec +4 -7
  73. data/spec/integration/commands/create_spec.rb +32 -24
  74. data/spec/integration/commands/delete_spec.rb +15 -7
  75. data/spec/integration/commands/update_spec.rb +13 -11
  76. data/spec/integration/mappers/deep_embedded_spec.rb +4 -11
  77. data/spec/integration/mappers/definition_dsl_spec.rb +31 -44
  78. data/spec/integration/mappers/embedded_spec.rb +9 -24
  79. data/spec/integration/mappers/group_spec.rb +22 -30
  80. data/spec/integration/mappers/prefixing_attributes_spec.rb +18 -23
  81. data/spec/integration/mappers/renaming_attributes_spec.rb +23 -38
  82. data/spec/integration/mappers/symbolizing_attributes_spec.rb +18 -24
  83. data/spec/integration/mappers/wrap_spec.rb +22 -30
  84. data/spec/integration/multi_repo_spec.rb +15 -37
  85. data/spec/integration/relations/reading_spec.rb +82 -14
  86. data/spec/integration/repositories/extending_relations_spec.rb +50 -0
  87. data/spec/integration/{adapters → repositories}/setting_logger_spec.rb +6 -5
  88. data/spec/integration/setup_spec.rb +59 -62
  89. data/spec/shared/enumerable_dataset.rb +49 -0
  90. data/spec/shared/one_behavior.rb +26 -0
  91. data/spec/shared/users_and_tasks.rb +11 -23
  92. data/spec/spec_helper.rb +16 -7
  93. data/spec/support/constant_leak_finder.rb +14 -0
  94. data/spec/test/memory_repository_lint_test.rb +27 -0
  95. data/spec/unit/rom/command_registry_spec.rb +44 -0
  96. data/spec/unit/rom/commands/result_spec.rb +14 -0
  97. data/spec/unit/rom/commands_spec.rb +174 -0
  98. data/spec/unit/rom/env_spec.rb +40 -7
  99. data/spec/unit/rom/global_spec.rb +14 -0
  100. data/spec/unit/rom/{mapper_builder_spec.rb → mapper/dsl_spec.rb} +52 -38
  101. data/spec/unit/rom/mapper_spec.rb +51 -10
  102. data/spec/unit/rom/{adapter/memory → memory}/dataset_spec.rb +6 -4
  103. data/spec/unit/rom/memory/repository_spec.rb +12 -0
  104. data/spec/unit/rom/memory/storage_spec.rb +45 -0
  105. data/spec/unit/rom/model_builder_spec.rb +4 -3
  106. data/spec/unit/rom/processor/transproc_spec.rb +1 -0
  107. data/spec/unit/rom/reader_spec.rb +97 -24
  108. data/spec/unit/rom/relation/composite_spec.rb +65 -0
  109. data/spec/unit/rom/relation/lazy_spec.rb +145 -0
  110. data/spec/unit/rom/relation/loaded_spec.rb +28 -0
  111. data/spec/unit/rom/relation_spec.rb +111 -6
  112. data/spec/unit/rom/repository_spec.rb +59 -9
  113. data/spec/unit/rom/setup_spec.rb +99 -11
  114. data/spec/unit/rom/support/array_dataset_spec.rb +59 -0
  115. data/spec/unit/rom/support/class_builder_spec.rb +42 -0
  116. data/spec/unit/rom/support/enumerable_dataset_spec.rb +17 -0
  117. data/spec/unit/rom/support/inflector_spec.rb +89 -0
  118. data/spec/unit/rom/support/options_spec.rb +119 -0
  119. metadata +74 -112
  120. data/lib/rom/adapter.rb +0 -191
  121. data/lib/rom/adapter/memory.rb +0 -32
  122. data/lib/rom/adapter/memory/commands.rb +0 -31
  123. data/lib/rom/adapter/memory/dataset.rb +0 -67
  124. data/lib/rom/adapter/memory/storage.rb +0 -26
  125. data/lib/rom/commands/with_options.rb +0 -18
  126. data/lib/rom/config.rb +0 -70
  127. data/lib/rom/mapper_builder.rb +0 -52
  128. data/lib/rom/mapper_builder/mapper_dsl.rb +0 -114
  129. data/lib/rom/mapper_builder/model_dsl.rb +0 -29
  130. data/lib/rom/reader_builder.rb +0 -48
  131. data/lib/rom/relation_builder.rb +0 -62
  132. data/lib/rom/setup/base_relation_dsl.rb +0 -46
  133. data/lib/rom/setup/command_dsl.rb +0 -46
  134. data/lib/rom/setup/mapper_dsl.rb +0 -19
  135. data/lib/rom/setup/relation_dsl.rb +0 -20
  136. data/lib/rom/setup/schema_dsl.rb +0 -33
  137. data/spec/integration/adapters/extending_relations_spec.rb +0 -41
  138. data/spec/integration/commands/try_spec.rb +0 -27
  139. data/spec/integration/schema_spec.rb +0 -77
  140. data/spec/unit/config_spec.rb +0 -60
  141. data/spec/unit/rom/adapter_spec.rb +0 -79
  142. data/spec/unit/rom_spec.rb +0 -14
data/lib/rom/header.rb CHANGED
@@ -31,9 +31,9 @@ module ROM
31
31
 
32
32
  # Coerce array with attribute definitions into a header object
33
33
  #
34
- # @param [Array<Array>] attribute name/option pairs
34
+ # @param [Array<Array>] input attribute name/option pairs
35
35
  #
36
- # @param [Class] optional model
36
+ # @param [Class] model optional
37
37
  #
38
38
  # @return [Header]
39
39
  #
@@ -63,8 +63,8 @@ module ROM
63
63
  # @yield [Attribute]
64
64
  #
65
65
  # @api private
66
- def each(&block)
67
- attributes.values.each(&block)
66
+ def each
67
+ attributes.each_value { |attribute| yield(attribute) }
68
68
  end
69
69
 
70
70
  # Return if there are any aliased attributes
@@ -150,7 +150,7 @@ module ROM
150
150
  #
151
151
  # @api private
152
152
  def initialize_tuple_keys
153
- @tuple_keys = mapping.keys + non_primitives.map(&:tuple_keys).flatten
153
+ @tuple_keys = mapping.keys + non_primitives.flat_map(&:tuple_keys)
154
154
  end
155
155
  end
156
156
  end
@@ -33,7 +33,7 @@ module ROM
33
33
 
34
34
  # Return attribute class for a give meta hash
35
35
  #
36
- # @param [Hash] hash with type information and optional transformation info
36
+ # @param [Hash] meta hash with type information and optional transformation info
37
37
  #
38
38
  # @return [Class]
39
39
  #
@@ -52,7 +52,7 @@ module ROM
52
52
 
53
53
  # Coerce an array with attribute meta-data into an attribute object
54
54
  #
55
- # @param [Array<Symbol,Hash>] name/options pair
55
+ # @param [Array<Symbol,Hash>] input attribute name/options pair
56
56
  #
57
57
  # @return [Attribute]
58
58
  #
@@ -0,0 +1,52 @@
1
+ require 'rom/lint/linter'
2
+
3
+ module ROM
4
+ module Lint
5
+ # Ensures that a [ROM::EnumerableDataset] extension correctly yields
6
+ # arrays and tuples
7
+ #
8
+ # @api public
9
+ class EnumerableDataset < ROM::Lint::Linter
10
+ # The linted subject
11
+ #
12
+ # @api public
13
+ attr_reader :dataset
14
+
15
+ # The expected data
16
+ #
17
+ # @api public
18
+ attr_reader :data
19
+
20
+ # Create a linter for EnumerableDataset
21
+ #
22
+ # @param [EnumerableDataset] dataset the linted subject
23
+ # @param [Object] data the expected data
24
+ #
25
+ # @api public
26
+ def initialize(dataset, data)
27
+ @dataset = dataset
28
+ @data = data
29
+ end
30
+
31
+ # Lint: Ensure that +dataset+ yield tuples via +each+
32
+ #
33
+ # @api public
34
+ def lint_each
35
+ result = []
36
+ dataset.each { |tuple| result << tuple }
37
+ return if result == data
38
+
39
+ complain "#{dataset.class}#each must yield tuples"
40
+ end
41
+
42
+ # Lint: Ensure that +dataset+'s array equals to expected +data+
43
+ #
44
+ # @api public
45
+ def lint_to_a
46
+ return if dataset.to_a == data
47
+
48
+ complain "#{dataset.class}#to_a must cast dataset to an array"
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,64 @@
1
+ module ROM
2
+ module Lint
3
+ # Base class for building linters that check source code
4
+ #
5
+ # Linters are used by authors of ROM adapters to verify that their
6
+ # integration complies with the ROM api.
7
+ #
8
+ # Most of the time, authors won't need to construct linters directly
9
+ # because the provided test helpers will automatically run when required
10
+ # in tests and specs.
11
+ #
12
+ # @example
13
+ # require 'rom/lint/spec'
14
+ #
15
+ #
16
+ # @api public
17
+ class Linter
18
+ # A failure raised by +complain+
19
+ Failure = Class.new(StandardError)
20
+
21
+ # Iterate over all lint methods
22
+ #
23
+ # @yield [String, ROM::Lint]
24
+ #
25
+ # @api public
26
+ def self.each_lint
27
+ return to_enum unless block_given?
28
+ lints.each { |lint| yield lint, self }
29
+ end
30
+
31
+ # Run a lint method
32
+ #
33
+ # @param [String] name
34
+ #
35
+ # @raise [ROM::Lint::Linter::Failure] if linting fails
36
+ #
37
+ # @api public
38
+ def lint(name)
39
+ public_send name
40
+ true # for assertions
41
+ end
42
+
43
+ private
44
+
45
+ # Return a list a lint methods
46
+ #
47
+ # @return [String]
48
+ #
49
+ # @api private
50
+ def self.lints
51
+ public_instance_methods(true).grep(/^lint_/).map(&:to_s)
52
+ end
53
+
54
+ # Raise a failure if a lint verification fails
55
+ #
56
+ # @raise [ROM::Lint::Linter::Failure]
57
+ #
58
+ # @api private
59
+ def complain(*args)
60
+ raise Failure, *args
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,78 @@
1
+ require 'rom/lint/linter'
2
+
3
+ module ROM
4
+ module Lint
5
+ # Ensures that a [ROM::Repository] extension provides datasets through the
6
+ # expected methods
7
+ #
8
+ # @api public
9
+ class Repository < ROM::Lint::Linter
10
+ # The repository identifier e.g. +:memory+
11
+ #
12
+ # @api public
13
+ attr_reader :identifier
14
+
15
+ # The repository class
16
+ #
17
+ # @api public
18
+ attr_reader :repository
19
+
20
+ # The optional URI
21
+ #
22
+ # @api public
23
+ attr_reader :uri
24
+
25
+ # Create a repository linter
26
+ #
27
+ # @param [Symbol] identifier
28
+ # @param [Class] repository
29
+ # @param [String] uri optional
30
+ def initialize(identifier, repository, uri = nil)
31
+ @identifier = identifier
32
+ @repository = repository
33
+ @uri = uri
34
+ end
35
+
36
+ # Lint: Ensure that +repository+ setups up its instance
37
+ #
38
+ # @api public
39
+ def lint_repository_setup
40
+ return if repository_instance.instance_of? repository
41
+
42
+ complain <<-STRING
43
+ #{repository}.setup must return a repository instance but
44
+ returned #{repository_instance.inspect}
45
+ STRING
46
+ end
47
+
48
+ # Lint: Ensure that +repository_instance+ responds to +[]+
49
+ #
50
+ # @api public
51
+ def lint_dataset_reader
52
+ return if repository_instance.respond_to? :[]
53
+
54
+ complain "#{repository_instance} must respond to []"
55
+ end
56
+
57
+ # Lint: Ensure that +repository_instance+ responds to +dataset?+
58
+ #
59
+ # @api public
60
+ def lint_dataset_predicate
61
+ return if repository_instance.respond_to? :dataset?
62
+
63
+ complain "#{repository_instance} must respond to dataset?"
64
+ end
65
+
66
+ # Setup repository instance
67
+ #
68
+ # @api public
69
+ def repository_instance
70
+ if uri
71
+ ROM::Repository.setup(identifier, uri)
72
+ else
73
+ ROM::Repository.setup(identifier)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,20 @@
1
+ require 'rom/lint/repository'
2
+ require 'rom/lint/enumerable_dataset'
3
+
4
+ RSpec.shared_examples "a rom repository" do
5
+ ROM::Lint::Repository.each_lint do |name, linter|
6
+ it name do
7
+ result = linter.new(identifier, repository, uri).lint(name)
8
+ expect(result).to be_truthy
9
+ end
10
+ end
11
+ end
12
+
13
+ RSpec.shared_examples "a rom enumerable dataset" do
14
+ ROM::Lint::EnumerableDataset.each_lint do |name, linter|
15
+ it name do
16
+ result = linter.new(dataset, data).lint(name)
17
+ expect(result).to be_truthy
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,98 @@
1
+ require 'rom/lint/repository'
2
+ require 'rom/lint/enumerable_dataset'
3
+
4
+ module ROM
5
+ module Lint
6
+ # A module that helps to define test methods
7
+ module Test
8
+ # Defines a test method converting lint failures to assertions
9
+ #
10
+ # @param [String] name
11
+ #
12
+ # @api private
13
+ def define_test_method(name, &block)
14
+ define_method "test_#{name}" do
15
+ begin
16
+ instance_eval(&block)
17
+ rescue ROM::Lint::Linter::Failure => f
18
+ raise Minitest::Assertion, f.message
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ # This is a simple lint-test for repository class to ensure the
25
+ # basic interfaces are in place
26
+ #
27
+ # @example
28
+ #
29
+ # class MyRepoTest < Minitest::Test
30
+ # include ROM::Lint::TestRepository
31
+ #
32
+ # def setup
33
+ # @repository = MyRepository
34
+ # @uri = "super_db://something"
35
+ # end
36
+ # end
37
+ #
38
+ # @api public
39
+ module TestRepository
40
+ extend ROM::Lint::Test
41
+
42
+ # Returns the repository identifier e.g. +:memory+
43
+ #
44
+ # @api public
45
+ attr_reader :identifier
46
+
47
+ # Returns the repository class
48
+ #
49
+ # @api public
50
+ attr_reader :repository
51
+
52
+ # Returns repostory's URI e.g. "super_db://something"
53
+ #
54
+ # @api public
55
+ attr_reader :uri
56
+
57
+ ROM::Lint::Repository.each_lint do |name, linter|
58
+ define_test_method name do
59
+ assert linter.new(identifier, repository, uri).lint(name)
60
+ end
61
+ end
62
+ end
63
+
64
+ # This is a simple lint-test for an repository dataset class to ensure the
65
+ # basic behavior is correct
66
+ #
67
+ # @example
68
+ #
69
+ # class MyDatasetLintTest < Minitest::Test
70
+ # include ROM::Repository::Lint::TestEnumerableDataset
71
+ #
72
+ # def setup
73
+ # @data = [{ name: 'Jane', age: 24 }, { name: 'Joe', age: 25 }]
74
+ # @dataset = MyDataset.new(@data, [:name, :age])
75
+ # end
76
+ # end
77
+ # @api public
78
+ module TestEnumerableDataset
79
+ extend ROM::Lint::Test
80
+
81
+ # Returns the dataset instance
82
+ #
83
+ # @api public
84
+ attr_reader :dataset
85
+
86
+ # Returns the expected data
87
+ #
88
+ # @api public
89
+ attr_reader :data
90
+
91
+ ROM::Lint::EnumerableDataset.each_lint do |name, linter|
92
+ define_test_method name do
93
+ assert linter.new(dataset, data).lint(name)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
data/lib/rom/mapper.rb CHANGED
@@ -1,8 +1,19 @@
1
+ require 'rom/mapper/dsl'
2
+
1
3
  module ROM
2
4
  # Mapper is a simple object that uses a transformer to load relations
3
5
  #
4
6
  # @private
5
7
  class Mapper
8
+ include DSL
9
+ include Equalizer.new(:transformer, :header)
10
+
11
+ defines :relation, :register_as, :symbolize_keys,
12
+ :prefix, :prefix_separator, :inherit_header
13
+
14
+ inherit_header true
15
+ prefix_separator '_'.freeze
16
+
6
17
  # @return [Object] transformer object built by a processor
7
18
  #
8
19
  # @api private
@@ -13,6 +24,14 @@ module ROM
13
24
  # @api private
14
25
  attr_reader :header
15
26
 
27
+ # Register suclasses during setup phase
28
+ #
29
+ # @api private
30
+ def self.inherited(klass)
31
+ super
32
+ ROM.register_mapper(klass)
33
+ end
34
+
16
35
  # @return [Hash] registered processors
17
36
  #
18
37
  # @api private
@@ -35,8 +54,16 @@ module ROM
35
54
  # @return [Mapper]
36
55
  #
37
56
  # @api private
38
- def self.build(header, processor = :transproc)
39
- new(processors.fetch(processor).build(header), header)
57
+ def self.build(header = self.header, processor = :transproc)
58
+ new(Mapper.processors.fetch(processor).build(header), header)
59
+ end
60
+
61
+ # @api private
62
+ def self.registry(descendants)
63
+ descendants.each_with_object({}) do |klass, h|
64
+ name = klass.register_as || klass.relation
65
+ (h[klass.base_relation] ||= {})[name] = klass.build
66
+ end
40
67
  end
41
68
 
42
69
  # @api private
@@ -52,11 +79,11 @@ module ROM
52
79
  header.model
53
80
  end
54
81
 
55
- # Process a relation using the transfomer
82
+ # Process a relation using the transformer
56
83
  #
57
84
  # @api private
58
- def process(relation, &block)
59
- transformer[relation.to_a].each(&block)
85
+ def call(relation)
86
+ transformer[relation.to_a]
60
87
  end
61
88
  end
62
89
  end
@@ -0,0 +1,240 @@
1
+ require 'rom/header'
2
+ require 'rom/mapper/model_dsl'
3
+
4
+ module ROM
5
+ class Mapper
6
+ # Mapper attribute DSL exposed by mapper subclasses
7
+ #
8
+ # This class is private even though its methods are exposed by mappers.
9
+ # Typically it's not meant to be used directly.
10
+ #
11
+ # @private
12
+ class AttributeDSL
13
+ include ModelDSL
14
+
15
+ attr_reader :attributes, :options, :symbolize_keys, :prefix, :prefix_separator
16
+
17
+ # @param [Array] attributes accumulator array
18
+ # @param [Hash] options
19
+ #
20
+ # @api private
21
+ def initialize(attributes, options)
22
+ @attributes = attributes
23
+ @options = options
24
+ @symbolize_keys = options.fetch(:symbolize_keys)
25
+ @prefix = options.fetch(:prefix)
26
+ @prefix_separator = options.fetch(:prefix_separator)
27
+ end
28
+
29
+ # Define a mapping attribute with its options
30
+ #
31
+ # @example
32
+ # dsl = AttributeDSL.new([])
33
+ #
34
+ # dsl.attribute(:name)
35
+ # dsl.attribute(:email, from: 'user_email')
36
+ #
37
+ # @api public
38
+ def attribute(name, options = EMPTY_HASH)
39
+ with_attr_options(name, options) do |attr_options|
40
+ add_attribute(name, attr_options)
41
+ end
42
+ end
43
+
44
+ # Remove an attribute
45
+ #
46
+ # @example
47
+ # dsl = AttributeDSL.new([[:name]])
48
+ #
49
+ # dsl.exclude(:name)
50
+ # dsl.attributes # => []
51
+ #
52
+ # @api public
53
+ def exclude(*names)
54
+ attributes.delete_if { |attr| names.include?(attr.first) }
55
+ end
56
+
57
+ # Define an embedded attribute
58
+ #
59
+ # Block exposes the attribute dsl too
60
+ #
61
+ # @example
62
+ # dsl = AttributeDSL.new([])
63
+ #
64
+ # dsl.embedded :tags, type: :array do
65
+ # attribute :name
66
+ # end
67
+ #
68
+ # dsl.embedded :address, type: :hash do
69
+ # model Address
70
+ # attribute :name
71
+ # end
72
+ #
73
+ # @param [Symbol] name attribute
74
+ #
75
+ # @param [Hash] options
76
+ # @option options [Symbol] :type Embedded type can be :hash or :array
77
+ # @option options [Symbol] :prefix Prefix that should be used for
78
+ # its attributes
79
+ #
80
+ # @api public
81
+ def embedded(name, options, &block)
82
+ with_attr_options(name) do |attr_options|
83
+ dsl = new(options, &block)
84
+
85
+ attr_options.update(options)
86
+
87
+ add_attribute(
88
+ name, { header: dsl.header, type: :array }.update(attr_options)
89
+ )
90
+ end
91
+ end
92
+
93
+ # Define an embedded hash attribute that requires "wrapping" transformation
94
+ #
95
+ # Typically this is used in sql context when relation is a join.
96
+ #
97
+ # @example
98
+ # dsl = AttributeDSL.new([])
99
+ #
100
+ # dsl.wrap(address: [:street, :zipcode, :city])
101
+ #
102
+ # dsl.wrap(:address) do
103
+ # model Address
104
+ # attribute :street
105
+ # attribute :zipcode
106
+ # attribute :city
107
+ # end
108
+ #
109
+ # @see AttributeDSL#embedded
110
+ #
111
+ # @api public
112
+ def wrap(*args, &block)
113
+ with_name_or_options(*args) do |name, options|
114
+ dsl(name, { type: :hash, wrap: true }.update(options), &block)
115
+ end
116
+ end
117
+
118
+ # Define an embedded hash attribute that requires "grouping" transformation
119
+ #
120
+ # Typically this is used in sql context when relation is a join.
121
+ #
122
+ # @example
123
+ # dsl = AttributeDSL.new([])
124
+ #
125
+ # dsl.group(tags: [:name])
126
+ #
127
+ # dsl.group(:tags) do
128
+ # model Tag
129
+ # attribute :name
130
+ # end
131
+ #
132
+ # @see AttributeDSL#embedded
133
+ #
134
+ # @api public
135
+ def group(*args, &block)
136
+ with_name_or_options(*args) do |name, options|
137
+ dsl(name, { type: :array, group: true }.update(options), &block)
138
+ end
139
+ end
140
+
141
+ # Generate a header from attribute definitions
142
+ #
143
+ # @return [Header]
144
+ #
145
+ # @api private
146
+ def header
147
+ Header.coerce(attributes, model)
148
+ end
149
+
150
+ private
151
+
152
+ # Handle attribute options common for all definitions
153
+ #
154
+ # @api private
155
+ def with_attr_options(name, options = EMPTY_HASH)
156
+ attr_options = options.dup
157
+
158
+ attr_options[:from] ||= :"#{prefix}#{prefix_separator}#{name}" if prefix
159
+
160
+ if symbolize_keys
161
+ attr_options.update(from: attr_options.fetch(:from) { name }.to_s)
162
+ end
163
+
164
+ yield(attr_options)
165
+ end
166
+
167
+ # Handle "name or options" syntax used by `wrap` and `group`
168
+ #
169
+ # @api private
170
+ def with_name_or_options(*args)
171
+ name, options =
172
+ if args.size > 1
173
+ args
174
+ else
175
+ [args.first, {}]
176
+ end
177
+
178
+ yield(name, options)
179
+ end
180
+
181
+ # Create another instance of the dsl for nested definitions
182
+ #
183
+ # This is used by embedded, wrap and group
184
+ #
185
+ # @api private
186
+ def dsl(name_or_attrs, options, &block)
187
+ if block
188
+ attributes_from_block(name_or_attrs, options, &block)
189
+ else
190
+ attributes_from_hash(name_or_attrs, options)
191
+ end
192
+ end
193
+
194
+ # Define attributes from a nested block
195
+ #
196
+ # Used by embedded, wrap and group
197
+ #
198
+ # @api private
199
+ def attributes_from_block(name, options, &block)
200
+ dsl = new(options, &block)
201
+ header = dsl.header
202
+ add_attribute(name, options.update(header: header))
203
+ header.each { |attr| exclude(attr.key) }
204
+ end
205
+
206
+ # Define attributes from the `name => attributes` hash syntax
207
+ #
208
+ # Used by wrap and group
209
+ #
210
+ # @api private
211
+ def attributes_from_hash(hash, options)
212
+ hash.each do |name, header|
213
+ with_attr_options(name, options) do |attr_options|
214
+ add_attribute(name, attr_options.update(header: header.zip))
215
+ header.each { |attr| exclude(attr) }
216
+ end
217
+ end
218
+ end
219
+
220
+ # Add a new attribute and make sure it overrides previous definition
221
+ #
222
+ # @api private
223
+ def add_attribute(name, options)
224
+ exclude(name, name.to_s)
225
+ attributes << [name, options]
226
+ end
227
+
228
+ # Create a new dsl instance of potentially overidden options
229
+ #
230
+ # Embedded, wrap and group can override top-level options like `prefix`
231
+ #
232
+ # @api private
233
+ def new(options, &block)
234
+ dsl = self.class.new([], @options.merge(options))
235
+ dsl.instance_exec(&block)
236
+ dsl
237
+ end
238
+ end
239
+ end
240
+ end