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.
- data/.rspec +2 -0
- data/.travis.yml +22 -0
- data/Changelog.md +16 -0
- data/Gemfile +13 -6
- data/Gemfile.devtools +71 -0
- data/Guardfile +19 -0
- data/LICENSE +1 -1
- data/README.md +52 -30
- data/Rakefile +3 -0
- data/config/devtools.yml +4 -0
- data/config/flay.yml +3 -0
- data/config/flog.yml +2 -0
- data/config/mutant.yml +5 -0
- data/config/reek.yml +103 -0
- data/config/rubocop.yml +62 -0
- data/config/yardstick.yml +2 -0
- data/lib/rom.rb +13 -5
- data/lib/rom/constants.rb +16 -0
- data/lib/rom/environment.rb +105 -0
- data/lib/rom/environment/builder.rb +71 -0
- data/lib/rom/mapper.rb +176 -0
- data/lib/rom/mapper/attribute.rb +108 -0
- data/lib/rom/mapper/builder.rb +58 -0
- data/lib/rom/mapper/builder/definition.rb +162 -0
- data/lib/rom/mapper/header.rb +103 -0
- data/lib/rom/mapper/loader_builder.rb +26 -0
- data/lib/rom/relation.rb +375 -0
- data/lib/rom/repository.rb +71 -0
- data/lib/rom/schema.rb +21 -0
- data/lib/rom/schema/builder.rb +59 -0
- data/lib/rom/schema/definition.rb +84 -0
- data/lib/rom/schema/definition/relation.rb +80 -0
- data/lib/rom/schema/definition/relation/base.rb +27 -0
- data/lib/rom/session.rb +111 -0
- data/lib/rom/session/environment.rb +67 -0
- data/lib/rom/session/identity_map.rb +43 -0
- data/lib/rom/session/mapper.rb +62 -0
- data/lib/rom/session/relation.rb +140 -0
- data/lib/rom/session/state.rb +59 -0
- data/lib/rom/session/state/created.rb +22 -0
- data/lib/rom/session/state/deleted.rb +25 -0
- data/lib/rom/session/state/persisted.rb +34 -0
- data/lib/rom/session/state/transient.rb +20 -0
- data/lib/rom/session/state/updated.rb +29 -0
- data/lib/rom/session/tracker.rb +62 -0
- data/lib/rom/support/axiom/adapter.rb +111 -0
- data/lib/rom/support/axiom/adapter/data_objects.rb +38 -0
- data/lib/rom/support/axiom/adapter/memory.rb +25 -0
- data/lib/rom/support/axiom/adapter/postgres.rb +19 -0
- data/lib/rom/support/axiom/adapter/sqlite3.rb +20 -0
- data/lib/version.rb +1 -1
- data/rom.gemspec +7 -3
- data/spec/integration/environment_setup_spec.rb +24 -0
- data/spec/integration/grouped_mappers_spec.rb +87 -0
- data/spec/integration/join_and_group_spec.rb +76 -0
- data/spec/integration/join_and_wrap_spec.rb +68 -0
- data/spec/integration/mapping_embedded_relations_spec.rb +73 -0
- data/spec/integration/mapping_relations_spec.rb +120 -0
- data/spec/integration/schema_definition_spec.rb +152 -0
- data/spec/integration/session_spec.rb +87 -0
- data/spec/integration/wrapped_mappers_spec.rb +73 -0
- data/spec/shared/unit/environment_context.rb +6 -0
- data/spec/shared/unit/loader.rb +11 -0
- data/spec/shared/unit/loader_identity.rb +13 -0
- data/spec/shared/unit/mapper_context.rb +11 -0
- data/spec/shared/unit/relation_context.rb +82 -0
- data/spec/shared/unit/session_environment_context.rb +11 -0
- data/spec/shared/unit/session_relation_context.rb +18 -0
- data/spec/spec_helper.rb +49 -0
- data/spec/support/helper.rb +34 -0
- data/spec/support/ice_nine_config.rb +10 -0
- data/spec/support/test_mapper.rb +110 -0
- data/spec/unit/rom/environment/builder/mapping_spec.rb +24 -0
- data/spec/unit/rom/environment/builder/schema_spec.rb +33 -0
- data/spec/unit/rom/environment/class_methods/setup_spec.rb +18 -0
- data/spec/unit/rom/environment/repository_spec.rb +21 -0
- data/spec/unit/rom/mapper/attribute/embedded_collection/to_ast_spec.rb +18 -0
- data/spec/unit/rom/mapper/attribute/embedded_value/to_ast_spec.rb +16 -0
- data/spec/unit/rom/mapper/attribute/rename_spec.rb +9 -0
- data/spec/unit/rom/mapper/attribute/to_ast_spec.rb +9 -0
- data/spec/unit/rom/mapper/builder/class_methods/call_spec.rb +61 -0
- data/spec/unit/rom/mapper/class_methods/build_spec.rb +55 -0
- data/spec/unit/rom/mapper/dump_spec.rb +11 -0
- data/spec/unit/rom/mapper/group_spec.rb +35 -0
- data/spec/unit/rom/mapper/header/each_spec.rb +26 -0
- data/spec/unit/rom/mapper/header/element_reader_spec.rb +21 -0
- data/spec/unit/rom/mapper/header/group_spec.rb +18 -0
- data/spec/unit/rom/mapper/header/join_spec.rb +14 -0
- data/spec/unit/rom/mapper/header/keys_spec.rb +29 -0
- data/spec/unit/rom/mapper/header/project_spec.rb +13 -0
- data/spec/unit/rom/mapper/header/rename_spec.rb +11 -0
- data/spec/unit/rom/mapper/header/to_ast_spec.rb +11 -0
- data/spec/unit/rom/mapper/header/wrap_spec.rb +18 -0
- data/spec/unit/rom/mapper/identity_from_tuple_spec.rb +11 -0
- data/spec/unit/rom/mapper/identity_spec.rb +11 -0
- data/spec/unit/rom/mapper/join_spec.rb +15 -0
- data/spec/unit/rom/mapper/load_spec.rb +11 -0
- data/spec/unit/rom/mapper/new_object_spec.rb +14 -0
- data/spec/unit/rom/mapper/project_spec.rb +11 -0
- data/spec/unit/rom/mapper/rename_spec.rb +16 -0
- data/spec/unit/rom/mapper/wrap_spec.rb +35 -0
- data/spec/unit/rom/relation/delete_spec.rb +15 -0
- data/spec/unit/rom/relation/drop_spec.rb +11 -0
- data/spec/unit/rom/relation/each_spec.rb +23 -0
- data/spec/unit/rom/relation/first_spec.rb +19 -0
- data/spec/unit/rom/relation/group_spec.rb +29 -0
- data/spec/unit/rom/relation/inject_mapper_spec.rb +17 -0
- data/spec/unit/rom/relation/insert_spec.rb +13 -0
- data/spec/unit/rom/relation/last_spec.rb +19 -0
- data/spec/unit/rom/relation/one_spec.rb +49 -0
- data/spec/unit/rom/relation/rename_spec.rb +21 -0
- data/spec/unit/rom/relation/replace_spec.rb +13 -0
- data/spec/unit/rom/relation/restrict_spec.rb +25 -0
- data/spec/unit/rom/relation/sort_by_spec.rb +25 -0
- data/spec/unit/rom/relation/take_spec.rb +11 -0
- data/spec/unit/rom/relation/to_a_spec.rb +20 -0
- data/spec/unit/rom/relation/update_spec.rb +25 -0
- data/spec/unit/rom/relation/wrap_spec.rb +29 -0
- data/spec/unit/rom/repository/class_methods/build_spec.rb +27 -0
- data/spec/unit/rom/repository/element_reader_spec.rb +21 -0
- data/spec/unit/rom/repository/element_writer_spec.rb +18 -0
- data/spec/unit/rom/schema/builder/class_methods/build_spec.rb +103 -0
- data/spec/unit/rom/schema/element_reader_spec.rb +15 -0
- data/spec/unit/rom/session/class_methods/start_spec.rb +23 -0
- data/spec/unit/rom/session/clean_predicate_spec.rb +21 -0
- data/spec/unit/rom/session/environment/element_reader_spec.rb +13 -0
- data/spec/unit/rom/session/flush_spec.rb +58 -0
- data/spec/unit/rom/session/mapper/load_spec.rb +47 -0
- data/spec/unit/rom/session/relation/delete_spec.rb +28 -0
- data/spec/unit/rom/session/relation/dirty_predicate_spec.rb +35 -0
- data/spec/unit/rom/session/relation/identity_spec.rb +11 -0
- data/spec/unit/rom/session/relation/new_spec.rb +50 -0
- data/spec/unit/rom/session/relation/save_spec.rb +50 -0
- data/spec/unit/rom/session/relation/state_spec.rb +23 -0
- data/spec/unit/rom/session/relation/track_spec.rb +23 -0
- data/spec/unit/rom/session/relation/tracking_predicate_spec.rb +23 -0
- data/spec/unit/rom/session/relation/update_attributes_spec.rb +45 -0
- data/spec/unit/rom/session/state_spec.rb +79 -0
- 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
|
data/lib/rom/relation.rb
ADDED
|
@@ -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
|