rom 0.1.2 → 0.2.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.
Files changed (139) hide show
  1. data/.rspec +2 -0
  2. data/.travis.yml +22 -0
  3. data/Changelog.md +16 -0
  4. data/Gemfile +13 -6
  5. data/Gemfile.devtools +71 -0
  6. data/Guardfile +19 -0
  7. data/LICENSE +1 -1
  8. data/README.md +52 -30
  9. data/Rakefile +3 -0
  10. data/config/devtools.yml +4 -0
  11. data/config/flay.yml +3 -0
  12. data/config/flog.yml +2 -0
  13. data/config/mutant.yml +5 -0
  14. data/config/reek.yml +103 -0
  15. data/config/rubocop.yml +62 -0
  16. data/config/yardstick.yml +2 -0
  17. data/lib/rom.rb +13 -5
  18. data/lib/rom/constants.rb +16 -0
  19. data/lib/rom/environment.rb +105 -0
  20. data/lib/rom/environment/builder.rb +71 -0
  21. data/lib/rom/mapper.rb +176 -0
  22. data/lib/rom/mapper/attribute.rb +108 -0
  23. data/lib/rom/mapper/builder.rb +58 -0
  24. data/lib/rom/mapper/builder/definition.rb +162 -0
  25. data/lib/rom/mapper/header.rb +103 -0
  26. data/lib/rom/mapper/loader_builder.rb +26 -0
  27. data/lib/rom/relation.rb +375 -0
  28. data/lib/rom/repository.rb +71 -0
  29. data/lib/rom/schema.rb +21 -0
  30. data/lib/rom/schema/builder.rb +59 -0
  31. data/lib/rom/schema/definition.rb +84 -0
  32. data/lib/rom/schema/definition/relation.rb +80 -0
  33. data/lib/rom/schema/definition/relation/base.rb +27 -0
  34. data/lib/rom/session.rb +111 -0
  35. data/lib/rom/session/environment.rb +67 -0
  36. data/lib/rom/session/identity_map.rb +43 -0
  37. data/lib/rom/session/mapper.rb +62 -0
  38. data/lib/rom/session/relation.rb +140 -0
  39. data/lib/rom/session/state.rb +59 -0
  40. data/lib/rom/session/state/created.rb +22 -0
  41. data/lib/rom/session/state/deleted.rb +25 -0
  42. data/lib/rom/session/state/persisted.rb +34 -0
  43. data/lib/rom/session/state/transient.rb +20 -0
  44. data/lib/rom/session/state/updated.rb +29 -0
  45. data/lib/rom/session/tracker.rb +62 -0
  46. data/lib/rom/support/axiom/adapter.rb +111 -0
  47. data/lib/rom/support/axiom/adapter/data_objects.rb +38 -0
  48. data/lib/rom/support/axiom/adapter/memory.rb +25 -0
  49. data/lib/rom/support/axiom/adapter/postgres.rb +19 -0
  50. data/lib/rom/support/axiom/adapter/sqlite3.rb +20 -0
  51. data/lib/version.rb +1 -1
  52. data/rom.gemspec +7 -3
  53. data/spec/integration/environment_setup_spec.rb +24 -0
  54. data/spec/integration/grouped_mappers_spec.rb +87 -0
  55. data/spec/integration/join_and_group_spec.rb +76 -0
  56. data/spec/integration/join_and_wrap_spec.rb +68 -0
  57. data/spec/integration/mapping_embedded_relations_spec.rb +73 -0
  58. data/spec/integration/mapping_relations_spec.rb +120 -0
  59. data/spec/integration/schema_definition_spec.rb +152 -0
  60. data/spec/integration/session_spec.rb +87 -0
  61. data/spec/integration/wrapped_mappers_spec.rb +73 -0
  62. data/spec/shared/unit/environment_context.rb +6 -0
  63. data/spec/shared/unit/loader.rb +11 -0
  64. data/spec/shared/unit/loader_identity.rb +13 -0
  65. data/spec/shared/unit/mapper_context.rb +11 -0
  66. data/spec/shared/unit/relation_context.rb +82 -0
  67. data/spec/shared/unit/session_environment_context.rb +11 -0
  68. data/spec/shared/unit/session_relation_context.rb +18 -0
  69. data/spec/spec_helper.rb +49 -0
  70. data/spec/support/helper.rb +34 -0
  71. data/spec/support/ice_nine_config.rb +10 -0
  72. data/spec/support/test_mapper.rb +110 -0
  73. data/spec/unit/rom/environment/builder/mapping_spec.rb +24 -0
  74. data/spec/unit/rom/environment/builder/schema_spec.rb +33 -0
  75. data/spec/unit/rom/environment/class_methods/setup_spec.rb +18 -0
  76. data/spec/unit/rom/environment/repository_spec.rb +21 -0
  77. data/spec/unit/rom/mapper/attribute/embedded_collection/to_ast_spec.rb +18 -0
  78. data/spec/unit/rom/mapper/attribute/embedded_value/to_ast_spec.rb +16 -0
  79. data/spec/unit/rom/mapper/attribute/rename_spec.rb +9 -0
  80. data/spec/unit/rom/mapper/attribute/to_ast_spec.rb +9 -0
  81. data/spec/unit/rom/mapper/builder/class_methods/call_spec.rb +61 -0
  82. data/spec/unit/rom/mapper/class_methods/build_spec.rb +55 -0
  83. data/spec/unit/rom/mapper/dump_spec.rb +11 -0
  84. data/spec/unit/rom/mapper/group_spec.rb +35 -0
  85. data/spec/unit/rom/mapper/header/each_spec.rb +26 -0
  86. data/spec/unit/rom/mapper/header/element_reader_spec.rb +21 -0
  87. data/spec/unit/rom/mapper/header/group_spec.rb +18 -0
  88. data/spec/unit/rom/mapper/header/join_spec.rb +14 -0
  89. data/spec/unit/rom/mapper/header/keys_spec.rb +29 -0
  90. data/spec/unit/rom/mapper/header/project_spec.rb +13 -0
  91. data/spec/unit/rom/mapper/header/rename_spec.rb +11 -0
  92. data/spec/unit/rom/mapper/header/to_ast_spec.rb +11 -0
  93. data/spec/unit/rom/mapper/header/wrap_spec.rb +18 -0
  94. data/spec/unit/rom/mapper/identity_from_tuple_spec.rb +11 -0
  95. data/spec/unit/rom/mapper/identity_spec.rb +11 -0
  96. data/spec/unit/rom/mapper/join_spec.rb +15 -0
  97. data/spec/unit/rom/mapper/load_spec.rb +11 -0
  98. data/spec/unit/rom/mapper/new_object_spec.rb +14 -0
  99. data/spec/unit/rom/mapper/project_spec.rb +11 -0
  100. data/spec/unit/rom/mapper/rename_spec.rb +16 -0
  101. data/spec/unit/rom/mapper/wrap_spec.rb +35 -0
  102. data/spec/unit/rom/relation/delete_spec.rb +15 -0
  103. data/spec/unit/rom/relation/drop_spec.rb +11 -0
  104. data/spec/unit/rom/relation/each_spec.rb +23 -0
  105. data/spec/unit/rom/relation/first_spec.rb +19 -0
  106. data/spec/unit/rom/relation/group_spec.rb +29 -0
  107. data/spec/unit/rom/relation/inject_mapper_spec.rb +17 -0
  108. data/spec/unit/rom/relation/insert_spec.rb +13 -0
  109. data/spec/unit/rom/relation/last_spec.rb +19 -0
  110. data/spec/unit/rom/relation/one_spec.rb +49 -0
  111. data/spec/unit/rom/relation/rename_spec.rb +21 -0
  112. data/spec/unit/rom/relation/replace_spec.rb +13 -0
  113. data/spec/unit/rom/relation/restrict_spec.rb +25 -0
  114. data/spec/unit/rom/relation/sort_by_spec.rb +25 -0
  115. data/spec/unit/rom/relation/take_spec.rb +11 -0
  116. data/spec/unit/rom/relation/to_a_spec.rb +20 -0
  117. data/spec/unit/rom/relation/update_spec.rb +25 -0
  118. data/spec/unit/rom/relation/wrap_spec.rb +29 -0
  119. data/spec/unit/rom/repository/class_methods/build_spec.rb +27 -0
  120. data/spec/unit/rom/repository/element_reader_spec.rb +21 -0
  121. data/spec/unit/rom/repository/element_writer_spec.rb +18 -0
  122. data/spec/unit/rom/schema/builder/class_methods/build_spec.rb +103 -0
  123. data/spec/unit/rom/schema/element_reader_spec.rb +15 -0
  124. data/spec/unit/rom/session/class_methods/start_spec.rb +23 -0
  125. data/spec/unit/rom/session/clean_predicate_spec.rb +21 -0
  126. data/spec/unit/rom/session/environment/element_reader_spec.rb +13 -0
  127. data/spec/unit/rom/session/flush_spec.rb +58 -0
  128. data/spec/unit/rom/session/mapper/load_spec.rb +47 -0
  129. data/spec/unit/rom/session/relation/delete_spec.rb +28 -0
  130. data/spec/unit/rom/session/relation/dirty_predicate_spec.rb +35 -0
  131. data/spec/unit/rom/session/relation/identity_spec.rb +11 -0
  132. data/spec/unit/rom/session/relation/new_spec.rb +50 -0
  133. data/spec/unit/rom/session/relation/save_spec.rb +50 -0
  134. data/spec/unit/rom/session/relation/state_spec.rb +23 -0
  135. data/spec/unit/rom/session/relation/track_spec.rb +23 -0
  136. data/spec/unit/rom/session/relation/tracking_predicate_spec.rb +23 -0
  137. data/spec/unit/rom/session/relation/update_attributes_spec.rb +45 -0
  138. data/spec/unit/rom/session/state_spec.rb +79 -0
  139. metadata +212 -11
@@ -0,0 +1,58 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rom/mapper/builder/definition'
4
+
5
+ module ROM
6
+ class Mapper
7
+
8
+ # Builder DSL for ROM mappers
9
+ #
10
+ class Builder
11
+ attr_reader :schema, :mappers
12
+
13
+ # @api public
14
+ def self.call(*args, &block)
15
+ new(*args).call(&block)
16
+ end
17
+
18
+ # Initialize a new mapping instance
19
+ #
20
+ # @return [undefined]
21
+ #
22
+ # @api private
23
+ def initialize(schema)
24
+ @schema = schema
25
+ @mappers = {}
26
+ end
27
+
28
+ # @api public
29
+ def relation(name, &block)
30
+ definition = Definition.build(schema[name].header, &block)
31
+ mappers[name] = definition.mapper
32
+ self
33
+ end
34
+
35
+ # @api private
36
+ def call(&block)
37
+ instance_eval(&block)
38
+ end
39
+
40
+ # @api private
41
+ def finalize
42
+ mappers.freeze
43
+ end
44
+
45
+ # @api private
46
+ def each(&block)
47
+ mappers.each(&block)
48
+ end
49
+
50
+ # @api private
51
+ def [](name)
52
+ mappers.fetch(name)
53
+ end
54
+
55
+ end # Builder
56
+
57
+ end # Mapper
58
+ end # ROM
@@ -0,0 +1,162 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rom/mapper'
4
+ require 'rom/mapper/attribute'
5
+
6
+ module ROM
7
+ class Mapper
8
+ class Builder
9
+
10
+ # Mapping definition DSL
11
+ #
12
+ # @private
13
+ class Definition
14
+ include Adamantium::Flat
15
+
16
+ attr_reader :attributes
17
+
18
+ LOADERS = [:instance_variables, :attribute_hash, :attribute_accessors].freeze
19
+
20
+ # Build new mapping definition
21
+ #
22
+ # @api private
23
+ def self.build(header, &block)
24
+ new(header, &block)
25
+ end
26
+
27
+ # Initialize a new Definition instance
28
+ #
29
+ # @return [undefined]
30
+ #
31
+ # @api private
32
+ def initialize(header, &block)
33
+ @header = header
34
+ @keys = header.keys.flat_map { |key_header| key_header.flat_map(&:name) }
35
+ @attributes = []
36
+ @loader = :load_instance_variables
37
+
38
+ instance_eval(&block)
39
+
40
+ build_mapper unless mapper
41
+ end
42
+
43
+ # @api private
44
+ def attribute_names
45
+ attributes.map(&:name)
46
+ end
47
+
48
+ # @api private
49
+ def loader(name = Undefined)
50
+ if name == Undefined
51
+ @loader
52
+ else
53
+ unless LOADERS.include?(name)
54
+ raise ArgumentError,
55
+ "loader +#{name.inspect}+ is not known. Valid loaders are #{LOADERS.inspect}"
56
+ end
57
+
58
+ @loader = :"load_#{name}"
59
+ end
60
+ end
61
+
62
+ # @api private
63
+ def header
64
+ @header.project(attributes.map(&:name))
65
+ end
66
+ memoize :header
67
+
68
+ # Get or set mapper
69
+ #
70
+ # @example
71
+ #
72
+ # Mapping.build do
73
+ # users do
74
+ # mapper my_custom_mapper
75
+ # end
76
+ # end
77
+ #
78
+ # @param [Object]
79
+ #
80
+ # @return [Object]
81
+ #
82
+ # @api public
83
+ def mapper(mapper = Undefined)
84
+ if mapper == Undefined
85
+ @mapper
86
+ else
87
+ @mapper = mapper
88
+ end
89
+ end
90
+
91
+ # Get or set model for the mapper
92
+ #
93
+ # @example
94
+ #
95
+ # Mapping.build do
96
+ # users do
97
+ # model User
98
+ # end
99
+ # end
100
+ #
101
+ # @param [Class]
102
+ #
103
+ # @return [Class]
104
+ #
105
+ # @api public
106
+ def model(model = Undefined)
107
+ if model == Undefined
108
+ @model
109
+ else
110
+ @model = model
111
+ end
112
+ end
113
+
114
+ # Configure attribute mappings
115
+ #
116
+ # @example
117
+ #
118
+ # Mapping.build do
119
+ # users do
120
+ # map :id, :email
121
+ # map :user_name, to: :name
122
+ # end
123
+ # end
124
+ #
125
+ # @params [Array<Symbol>,Symbol,Hash]
126
+ #
127
+ # @return [Definition]
128
+ #
129
+ # @api public
130
+ def map(*args)
131
+ if args.last.kind_of?(Hash)
132
+ attributes.concat([build_attribute(*args)])
133
+ else
134
+ attributes.concat(args.map { |name| build_attribute(name) })
135
+ end
136
+ end
137
+
138
+ private
139
+
140
+ # Build default rom mapper
141
+ #
142
+ # @api private
143
+ def build_mapper
144
+ @mapper = Mapper.build(attributes, model: model, type: loader)
145
+ end
146
+
147
+ def build_attribute(name, options = {})
148
+ header_name = options.fetch(:from, name)
149
+
150
+ defaults = {
151
+ key: @keys.include?(header_name),
152
+ type: @header[header_name].type.primitive
153
+ }
154
+
155
+ Attribute.build(name, defaults.merge(options))
156
+ end
157
+
158
+ end # Definition
159
+
160
+ end # DSL
161
+ end # Mapper
162
+ end # ROM
@@ -0,0 +1,103 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rom/mapper/attribute'
4
+
5
+ module ROM
6
+ class Mapper
7
+
8
+ # Mapper header wrapping axiom header and providing mapping information
9
+ #
10
+ # @private
11
+ class Header
12
+ include Enumerable, Concord.new(:attributes), Adamantium, Morpher::NodeHelpers
13
+
14
+ # Build a header
15
+ #
16
+ # @api private
17
+ def self.build(input)
18
+ if input.is_a?(self)
19
+ input
20
+ else
21
+ new(input.map { |args| Attribute.build(*args) })
22
+ end
23
+ end
24
+
25
+ # Return attribute mapping
26
+ #
27
+ # @api private
28
+ def mapping
29
+ each_with_object({}) { |attribute, hash| hash.update(attribute.mapping) }
30
+ end
31
+ memoize :mapping
32
+
33
+ # Return all key attributes
34
+ #
35
+ # @return [Array<Attribute>]
36
+ #
37
+ # @api public
38
+ def keys
39
+ select(&:key?)
40
+ end
41
+ memoize :keys
42
+
43
+ def to_ast
44
+ s(:hash_transform, *map(&:to_ast))
45
+ end
46
+ memoize :to_ast
47
+
48
+ # Return attribute with the given name
49
+ #
50
+ # @return [Attribute]
51
+ #
52
+ # @api public
53
+ def [](name)
54
+ detect { |attribute| attribute.name == name } || raise(KeyError)
55
+ end
56
+
57
+ # Return attribute names
58
+ #
59
+ # @api private
60
+ def attribute_names
61
+ map(&:name)
62
+ end
63
+
64
+ # Iterate over attributes
65
+ #
66
+ # @api private
67
+ def each(&block)
68
+ return to_enum unless block_given?
69
+ attributes.each(&block)
70
+ self
71
+ end
72
+
73
+ # TODO: this should receive a hash with header objects already
74
+ def wrap(other)
75
+ new_attributes = other.map { |name, mapper| mapper.attribute(Attribute::EmbeddedValue, name) }
76
+ self.class.new((attributes + new_attributes).uniq)
77
+ end
78
+
79
+ # TODO: this should receive a hash with header objects already
80
+ def group(other)
81
+ new_attributes = other.map { |name, mapper| mapper.attribute(Attribute::EmbeddedCollection, name) }
82
+ self.class.new((attributes + new_attributes).uniq)
83
+ end
84
+
85
+ # @api private
86
+ def join(other)
87
+ self.class.new((attributes + other.attributes).uniq)
88
+ end
89
+
90
+ # @api private
91
+ def project(names)
92
+ self.class.new(select { |attribute| names.include?(attribute.tuple_key) })
93
+ end
94
+
95
+ # @api private
96
+ def rename(names)
97
+ self.class.new(map { |attribute| names[attribute.name] ? attribute.rename(names[attribute.name]) : attribute })
98
+ end
99
+
100
+ end # Header
101
+
102
+ end # Mapper
103
+ end # ROM
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+
3
+ module ROM
4
+ class Mapper
5
+
6
+ # Abstract loader class
7
+ #
8
+ # @private
9
+ class LoaderBuilder
10
+ extend Morpher::NodeHelpers
11
+
12
+ def self.call(header, model, type)
13
+ param =
14
+ if type == :load_attribute_hash
15
+ s(:param, model)
16
+ else
17
+ s(:param, model, *header.attribute_names)
18
+ end
19
+
20
+ Morpher.compile(s(:block, header.to_ast, s(type, param)))
21
+ end
22
+
23
+ end # Loader
24
+
25
+ end # Mapper
26
+ end # ROM
@@ -0,0 +1,375 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rom/constants'
4
+
5
+ module ROM
6
+
7
+ # Enhanced ROM relation wrapping axiom relation and using injected mapper to
8
+ # load/dump tuples/objects
9
+ #
10
+ # @example
11
+ #
12
+ # # set up an axiom relation
13
+ # header = [[:id, Integer], [:name, String]]
14
+ # data = [[1, 'John'], [2, 'Jane']]
15
+ # axiom = Axiom::Relation.new(header, data)
16
+ #
17
+ # # provide a simple mapper
18
+ # class Mapper < Struct.new(:header)
19
+ # def load(tuple)
20
+ # data = header.map { |attribute|
21
+ # [attribute.name, tuple[attribute.name]]
22
+ # }
23
+ # Hash[data]
24
+ # end
25
+ #
26
+ # def dump(hash)
27
+ # header.each_with_object([]) { |attribute, tuple|
28
+ # tuple << hash[attribute.name]
29
+ # }
30
+ # end
31
+ # end
32
+ #
33
+ # # wrap axiom relation with ROM relation
34
+ # mapper = Mapper.new(axiom.header)
35
+ # relation = ROM::Relation.new(axiom, mapper)
36
+ #
37
+ # # relation is an enumerable and it uses mapper to load/dump tuples/objects
38
+ # relation.to_a
39
+ # # => [{:id=>1, :name=>'John'}, {:id=>2, :name=>'Jane'}]
40
+ #
41
+ # # you can insert/update/delete objects
42
+ # relation.insert(id: 3, name: 'Piotr').to_a
43
+ # # => [{:id=>1, :name=>"John"}, {:id=>2, :name=>"Jane"}, {:id=>3, :name=>"Piotr"}]
44
+ #
45
+ # relation.delete(id: 1, name: 'John').to_a
46
+ # # => [{:id=>2, :name=>"Jane"}]
47
+ #
48
+ class Relation
49
+ include Enumerable
50
+ include Equalizer.new(:mapper)
51
+ include Charlatan.new(:relation, :kind => Axiom::Relation)
52
+
53
+ undef_method :sort_by
54
+
55
+ attr_reader :mapper
56
+
57
+ def initialize(relation, mapper)
58
+ super(relation, mapper)
59
+ @mapper = mapper
60
+ end
61
+
62
+ # Iterate over tuples yielded by the wrapped relation
63
+ #
64
+ # @example
65
+ # mapper = Class.new {
66
+ # def load(value)
67
+ # value.to_s
68
+ # end
69
+ #
70
+ # def dump(value)
71
+ # value.to_i
72
+ # end
73
+ # }.new
74
+ #
75
+ # relation = ROM::Relation.new([1, 2, 3], mapper)
76
+ #
77
+ # relation.each do |value|
78
+ # puts value # => '1'
79
+ # end
80
+ #
81
+ # @yieldparam [Object]
82
+ #
83
+ # @return [Relation]
84
+ #
85
+ # @api public
86
+ def each
87
+ return to_enum unless block_given?
88
+ relation.each { |tuple| yield(mapper.load(tuple)) }
89
+ self
90
+ end
91
+
92
+ # Insert an object into relation
93
+ #
94
+ # @example
95
+ # axiom = Axiom::Relation.new([[:id, Integer]], [[1], [2]])
96
+ # relation = ROM::Relation.new(axiom, mapper)
97
+ #
98
+ # relation.insert(id: 3)
99
+ # relation.to_a # => [[1], [2], [3]]
100
+ #
101
+ # @param [Object]
102
+ #
103
+ # @return [Relation]
104
+ #
105
+ # @api public
106
+ def insert(object)
107
+ new(relation.insert([mapper.dump(object)]))
108
+ end
109
+ alias_method :<<, :insert
110
+
111
+ # Update an object
112
+ #
113
+ # @example
114
+ # data = [[1, 'John'], [2, 'Jane']]
115
+ # axiom = Axiom::Relation.new([[:id, Integer], [:name, String]], data)
116
+ # relation = ROM::Relation.new(axiom, mapper)
117
+ #
118
+ # relation.update({id: 2, name: 'Jane Doe'}, {id:2, name: 'Jane'})
119
+ # relation.to_a # => [[1, 'John'], [2, 'Jane Doe']]
120
+ #
121
+ # @param [Object]
122
+ # @param [Hash] original attributes
123
+ #
124
+ # @return [Relation]
125
+ #
126
+ # @api public
127
+ def update(object, original_tuple)
128
+ new(relation.delete([original_tuple]).insert([mapper.dump(object)]))
129
+ end
130
+
131
+ # Delete an object from the relation
132
+ #
133
+ # @example
134
+ # axiom = Axiom::Relation.new([[:id, Integer]], [[1], [2]])
135
+ # relation = ROM::Relation.new(axiom, mapper)
136
+ #
137
+ # relation.delete(id: 1)
138
+ # relation.to_a # => [[2]]
139
+ #
140
+ # @param [Object]
141
+ #
142
+ # @return [Relation]
143
+ #
144
+ # @api public
145
+ def delete(object)
146
+ new(relation.delete([mapper.dump(object)]))
147
+ end
148
+
149
+ # Replace all objects in the relation with new ones
150
+ #
151
+ # @example
152
+ # axiom = Axiom::Relation.new([[:id, Integer]], [[1], [2]])
153
+ # relation = ROM::Relation.new(axiom, mapper)
154
+ #
155
+ # relation.replace([{id: 3}, {id: 4}])
156
+ # relation.to_a # => [[3], [4]]
157
+ #
158
+ # @param [Array<Object>]
159
+ #
160
+ # @return [Relation]
161
+ #
162
+ # @api public
163
+ def replace(objects)
164
+ new(relation.replace(objects.map(&mapper.method(:dump))))
165
+ end
166
+
167
+ # Take objects form the relation with provided limit
168
+ #
169
+ # @example
170
+ # axiom = Axiom::Relation.new([[:id, Integer]], [[1], [2]])
171
+ # relation = ROM::Relation.new(axiom, mapper)
172
+ #
173
+ # relation.take(2).to_a # => [[2]]
174
+ #
175
+ # @param [Integer] limit
176
+ #
177
+ # @return [Relation]
178
+ #
179
+ # @api public
180
+ def take(limit)
181
+ new(sorted.take(limit))
182
+ end
183
+
184
+ # Take first n-objects from the relation
185
+ #
186
+ # @example
187
+ # axiom = Axiom::Relation.new([[:id, Integer]], [[1], [2]])
188
+ # relation = ROM::Relation.new(axiom, mapper)
189
+ #
190
+ # relation.first.to_a # => [[1]]
191
+ # relation.first(2).to_a # => [[1], [2]]
192
+ #
193
+ # @param [Integer]
194
+ #
195
+ # @return [Relation]
196
+ #
197
+ # @api public
198
+ def first(limit = 1)
199
+ new(sorted.first(limit))
200
+ end
201
+
202
+ # Take last n-objects from the relation
203
+ #
204
+ # @example
205
+ # axiom = Axiom::Relation.new([[:id, Integer]], [[1], [2]])
206
+ # relation = ROM::Relation.new(axiom, mapper)
207
+ #
208
+ # relation.last.to_a # => [[2]]
209
+ # relation.last(2).to_a # => [[1], [2]]
210
+ #
211
+ # @param [Integer] limit
212
+ #
213
+ # @return [Relation]
214
+ #
215
+ # @api public
216
+ def last(limit = 1)
217
+ new(sorted.last(limit))
218
+ end
219
+
220
+ # Drop objects from the relation by the given offset
221
+ #
222
+ # @example
223
+ # axiom = Axiom::Relation.new([[:id, Integer]], [[1], [2]])
224
+ # relation = ROM::Relation.new(axiom, mapper)
225
+ #
226
+ # relation.drop(1).to_a # => [[2]]
227
+ #
228
+ # @param [Integer]
229
+ #
230
+ # @return [Relation]
231
+ #
232
+ # @api public
233
+ def drop(offset)
234
+ new(sorted.drop(offset))
235
+ end
236
+
237
+ # Return exactly one object matching criteria or raise an error
238
+ #
239
+ # @example
240
+ # axiom = Axiom::Relation.new([[:id, Integer]], [1]])
241
+ # relation = ROM::Relation.new(axiom, mapper)
242
+ #
243
+ # relation.one.to_a # => {id: 1}
244
+ #
245
+ # @param [Proc] block
246
+ # optional block to call in case no tuple is returned
247
+ #
248
+ # @return [Object]
249
+ #
250
+ # @raise NoTuplesError
251
+ # if no tuples were returned
252
+ #
253
+ # @raise ManyTuplesError
254
+ # if more than one tuple was returned
255
+ #
256
+ # @api public
257
+ def one(&block)
258
+ block ||= ->() { raise NoTuplesError }
259
+ tuples = take(2).to_a
260
+
261
+ if tuples.count > 1
262
+ raise ManyTuplesError
263
+ else
264
+ tuples.first || block.call
265
+ end
266
+ end
267
+
268
+ # Inject a new mapper into this relation
269
+ #
270
+ # @example
271
+ #
272
+ # relation = ROM::Relation.new([], mapper)
273
+ # relation.inject_mapper(new_mapper)
274
+ #
275
+ # @param [Object] a mapper object
276
+ #
277
+ # @return [Relation]
278
+ #
279
+ # @api public
280
+ def inject_mapper(mapper)
281
+ new(relation, mapper)
282
+ end
283
+
284
+ # Join two relations
285
+ #
286
+ # @example
287
+ #
288
+ # users.join(tasks)
289
+ #
290
+ # @return [Relation]
291
+ #
292
+ # @api public
293
+ def join(other)
294
+ new(relation.join(other.relation), mapper.join(other.mapper))
295
+ end
296
+
297
+ # Wrap one or more relation
298
+ #
299
+ # @example
300
+ #
301
+ # tasks.join(users).wrap(user: tasks)
302
+ #
303
+ # @return [Relation]
304
+ #
305
+ # @api public
306
+ def wrap(other)
307
+ relation_wrap = other.each_with_object({}) { |(name, relation), o| o[name] = relation.header }
308
+ mapper_wrap = other.each_with_object({}) { |(name, relation), o| o[name] = relation.mapper }
309
+
310
+ new(relation.wrap(relation_wrap), mapper.wrap(mapper_wrap))
311
+ end
312
+
313
+ # Group one or more relation
314
+ #
315
+ # @example
316
+ #
317
+ # users.join(tasks).group(tasks: tasks)
318
+ #
319
+ # @return [Relation]
320
+ #
321
+ # @api public
322
+ def group(other)
323
+ relation_group = other.each_with_object({}) { |(name, relation), o| o[name] = relation.header }
324
+ mapper_group = other.each_with_object({}) { |(name, relation), o| o[name] = relation.mapper }
325
+
326
+ new(relation.group(relation_group), mapper.group(mapper_group))
327
+ end
328
+
329
+ # Project a relation
330
+ #
331
+ # @example
332
+ #
333
+ # users.project([:id, :name])
334
+ #
335
+ # @return [Relation]
336
+ #
337
+ # @api public
338
+ def project(names)
339
+ new(relation.project(names), mapper.project(names))
340
+ end
341
+
342
+ # Rename attributes in a relation
343
+ #
344
+ # @example
345
+ #
346
+ # users.rename(:user_id => :id)
347
+ #
348
+ # @return [Relation]
349
+ #
350
+ # @api public
351
+ def rename(names)
352
+ new(relation.rename(names), mapper.rename(names))
353
+ end
354
+
355
+ # Sort wrapped relation using all attributes in the header
356
+ #
357
+ # @return [Axiom::Relation]
358
+ #
359
+ # @api private
360
+ def sorted
361
+ relation.sort
362
+ end
363
+
364
+ # Return new relation instance
365
+ #
366
+ # @return [Relation]
367
+ #
368
+ # @api private
369
+ def new(new_relation, new_mapper = mapper)
370
+ self.class.new(new_relation, new_mapper)
371
+ end
372
+
373
+ end # class Relation
374
+
375
+ end # module ROM