rom-repository 0.0.1
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 +7 -0
- data/.gitignore +1 -0
- data/.rspec +3 -0
- data/.travis.yml +31 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile +15 -0
- data/LICENSE.txt +22 -0
- data/README.md +136 -0
- data/Rakefile +18 -0
- data/lib/rom-repository.rb +2 -0
- data/lib/rom/repository/base.rb +56 -0
- data/lib/rom/repository/ext/relation.rb +125 -0
- data/lib/rom/repository/ext/relation/view_dsl.rb +33 -0
- data/lib/rom/repository/header_builder.rb +63 -0
- data/lib/rom/repository/loading_proxy.rb +173 -0
- data/lib/rom/repository/loading_proxy/combine.rb +158 -0
- data/lib/rom/repository/loading_proxy/wrap.rb +60 -0
- data/lib/rom/repository/mapper_builder.rb +30 -0
- data/lib/rom/repository/struct_builder.rb +38 -0
- data/lib/rom/repository/version.rb +9 -0
- data/lib/rom/struct.rb +32 -0
- data/rom-repository.gemspec +24 -0
- data/spec/integration/repository_spec.rb +40 -0
- data/spec/shared/database.rb +26 -0
- data/spec/shared/relations.rb +29 -0
- data/spec/shared/repo.rb +49 -0
- data/spec/shared/seeds.rb +11 -0
- data/spec/shared/structs.rb +103 -0
- data/spec/spec_helper.rb +43 -0
- data/spec/support/mapper_registry.rb +11 -0
- data/spec/unit/header_builder_spec.rb +74 -0
- data/spec/unit/loading_proxy_spec.rb +138 -0
- data/spec/unit/sql/relation_spec.rb +48 -0
- data/spec/unit/struct_builder_spec.rb +25 -0
- metadata +164 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rom/repository/header_builder'
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
class Repository < Gateway
|
5
|
+
# @api private
|
6
|
+
class MapperBuilder
|
7
|
+
attr_reader :header_builder
|
8
|
+
|
9
|
+
attr_reader :registry
|
10
|
+
|
11
|
+
def self.registry
|
12
|
+
@__registry__ ||= {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.new(header_builder = HeaderBuilder.new)
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(header_builder)
|
20
|
+
@header_builder = header_builder
|
21
|
+
@registry = self.class.registry
|
22
|
+
end
|
23
|
+
|
24
|
+
def call(ast)
|
25
|
+
registry[ast.hash] ||= Mapper.build(header_builder[ast])
|
26
|
+
end
|
27
|
+
alias_method :[], :call
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'anima'
|
2
|
+
|
3
|
+
require 'rom/struct'
|
4
|
+
|
5
|
+
module ROM
|
6
|
+
class Repository < Gateway
|
7
|
+
# @api private
|
8
|
+
class StructBuilder
|
9
|
+
attr_reader :registry
|
10
|
+
|
11
|
+
def self.registry
|
12
|
+
@__registry__ ||= {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@registry = self.class.registry
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(*args)
|
20
|
+
name, columns = args
|
21
|
+
registry[args.hash] ||= build_class(name) { |klass|
|
22
|
+
klass.send(:include, Anima.new(*columns))
|
23
|
+
}
|
24
|
+
end
|
25
|
+
alias_method :[], :call
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def build_class(name, &block)
|
30
|
+
ROM::ClassBuilder.new(name: class_name(name), parent: Struct).call(&block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def class_name(name)
|
34
|
+
"ROM::Struct[#{Inflector.classify(Inflector.singularize(name))}]"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/rom/struct.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'anima'
|
2
|
+
|
3
|
+
require 'rom/support/class_builder'
|
4
|
+
|
5
|
+
module ROM
|
6
|
+
# Simple data-struct
|
7
|
+
#
|
8
|
+
# By default mappers use this as the model
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
class Struct
|
12
|
+
# Coerce to hash
|
13
|
+
#
|
14
|
+
# @return [Hash]
|
15
|
+
#
|
16
|
+
# @api private
|
17
|
+
def to_hash
|
18
|
+
to_h
|
19
|
+
end
|
20
|
+
|
21
|
+
# Access attribute value
|
22
|
+
#
|
23
|
+
# @param [Symbol] name The name of the attribute
|
24
|
+
#
|
25
|
+
# @return [Object]
|
26
|
+
#
|
27
|
+
# @api public
|
28
|
+
def [](name)
|
29
|
+
instance_variable_get("@#{name}")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require File.expand_path('../lib/rom/repository/version', __FILE__)
|
4
|
+
|
5
|
+
Gem::Specification.new do |gem|
|
6
|
+
gem.name = 'rom-repository'
|
7
|
+
gem.summary = 'Repository for ROM with auto-mapping and relation extensions'
|
8
|
+
gem.description = gem.summary
|
9
|
+
gem.author = 'Piotr Solnica'
|
10
|
+
gem.email = 'piotr.solnica@gmail.com'
|
11
|
+
gem.homepage = 'http://rom-rb.org'
|
12
|
+
gem.require_paths = ['lib']
|
13
|
+
gem.version = ROM::Repository::VERSION.dup
|
14
|
+
gem.files = `git ls-files`.split("\n").reject { |name| name.include?('benchmarks') }
|
15
|
+
gem.test_files = `git ls-files -- {spec}/*`.split("\n")
|
16
|
+
gem.license = 'MIT'
|
17
|
+
|
18
|
+
gem.add_runtime_dependency 'anima', '~> 0.2', '>= 0.2'
|
19
|
+
gem.add_runtime_dependency 'rom', '~> 0.8', '>= 0.8.1'
|
20
|
+
gem.add_runtime_dependency 'rom-sql', '~> 0.5', '>= 0.5.3'
|
21
|
+
|
22
|
+
gem.add_development_dependency 'rake', '~> 10.3'
|
23
|
+
gem.add_development_dependency 'rspec', '~> 3.3'
|
24
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
RSpec.describe 'ROM repository' do
|
2
|
+
include_context 'database'
|
3
|
+
include_context 'relations'
|
4
|
+
include_context 'seeds'
|
5
|
+
include_context 'structs'
|
6
|
+
|
7
|
+
it 'loads a single relation' do
|
8
|
+
expect(repo.all_users.to_a).to eql([jane, joe])
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'loads a combine relation with one parent' do
|
12
|
+
expect(repo.task_with_user.first).to eql(task_with_user)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'loads a combine relation with one parent with custom tuple key' do
|
16
|
+
expect(repo.task_with_owner.first).to eql(task_with_owner)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'loads a combined relation with many children' do
|
20
|
+
expect(repo.users_with_tasks.to_a).to eql([jane_with_tasks, joe_with_tasks])
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'loads a combined relation with one child' do
|
24
|
+
expect(repo.users_with_task.to_a).to eql([jane_with_task, joe_with_task])
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'loads a combined relation with one child restricted by given criteria' do
|
28
|
+
expect(repo.users_with_task_by_title('Joe Task').to_a).to eql([
|
29
|
+
jane_without_task, joe_with_task
|
30
|
+
])
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'loads nested combined relations' do
|
34
|
+
expect(repo.users_with_tasks_and_tags.first).to eql(user_with_task_and_tags)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'loads a wrapped relation' do
|
38
|
+
expect(repo.tag_with_wrapped_task.first).to eql(tag_with_task)
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
RSpec.shared_context 'database' do
|
2
|
+
let(:setup) { ROM.setup(:sql, 'postgres://localhost/rom') }
|
3
|
+
let(:conn) { setup.gateways[:default].connection }
|
4
|
+
let(:rom) { setup.finalize }
|
5
|
+
|
6
|
+
before do
|
7
|
+
[:tags, :tasks, :users].each { |table| conn.drop_table?(table) }
|
8
|
+
|
9
|
+
conn.create_table :users do
|
10
|
+
primary_key :id
|
11
|
+
column :name, String
|
12
|
+
end
|
13
|
+
|
14
|
+
conn.create_table :tasks do
|
15
|
+
primary_key :id
|
16
|
+
foreign_key :user_id, :users, null: false, on_delete: :cascade
|
17
|
+
column :title, String
|
18
|
+
end
|
19
|
+
|
20
|
+
conn.create_table :tags do
|
21
|
+
primary_key :id
|
22
|
+
foreign_key :task_id, :tasks, null: false, on_delete: :cascade
|
23
|
+
column :name, String
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
RSpec.shared_context 'relations' do
|
2
|
+
let(:users) { rom.relation(:users) }
|
3
|
+
let(:tasks) { rom.relation(:tasks) }
|
4
|
+
let(:tags) { rom.relation(:tags) }
|
5
|
+
|
6
|
+
before do
|
7
|
+
setup.relation(:users) do
|
8
|
+
def all
|
9
|
+
select(:id, :name).order(:name, :id)
|
10
|
+
end
|
11
|
+
|
12
|
+
def find(criteria)
|
13
|
+
where(criteria)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
setup.relation(:tasks) do
|
18
|
+
def find(criteria)
|
19
|
+
where(criteria)
|
20
|
+
end
|
21
|
+
|
22
|
+
def for_users(users)
|
23
|
+
where(user_id: users.map { |u| u[:id] })
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
setup.relation(:tags)
|
28
|
+
end
|
29
|
+
end
|
data/spec/shared/repo.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
RSpec.shared_context('repo') do
|
2
|
+
let(:repo) { repo_class.new(rom) }
|
3
|
+
|
4
|
+
let(:repo_class) do
|
5
|
+
Class.new(ROM::Repository::Base) do
|
6
|
+
relations :users, :tasks, :tags
|
7
|
+
|
8
|
+
def find_users(criteria)
|
9
|
+
users.find(criteria)
|
10
|
+
end
|
11
|
+
|
12
|
+
def all_users
|
13
|
+
users.all
|
14
|
+
end
|
15
|
+
|
16
|
+
def task_with_user
|
17
|
+
tasks.find(id: 2).combine_parents(one: users)
|
18
|
+
end
|
19
|
+
|
20
|
+
def task_with_owner
|
21
|
+
tasks.find(id: 2).combine_parents(one: { owner: users })
|
22
|
+
end
|
23
|
+
|
24
|
+
def users_with_tasks
|
25
|
+
users.combine_children(many: { all_tasks: tasks.for_users })
|
26
|
+
end
|
27
|
+
|
28
|
+
def users_with_tasks_and_tags
|
29
|
+
users.combine_children(many: { all_tasks: tasks_with_tags(tasks.for_users) })
|
30
|
+
end
|
31
|
+
|
32
|
+
def tasks_with_tags(tasks = self.tasks)
|
33
|
+
tasks.combine_children(many: tags)
|
34
|
+
end
|
35
|
+
|
36
|
+
def users_with_task
|
37
|
+
users.combine_children(one: tasks)
|
38
|
+
end
|
39
|
+
|
40
|
+
def users_with_task_by_title(title)
|
41
|
+
users.combine_children(one: tasks.find(title: title))
|
42
|
+
end
|
43
|
+
|
44
|
+
def tag_with_wrapped_task
|
45
|
+
tags.wrap_parent(task: tasks)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
RSpec.shared_context 'seeds' do
|
2
|
+
before do
|
3
|
+
jane_id = conn[:users].insert name: 'Jane'
|
4
|
+
joe_id = conn[:users].insert name: 'Joe'
|
5
|
+
|
6
|
+
conn[:tasks].insert user_id: joe_id, title: 'Joe Task'
|
7
|
+
task_id = conn[:tasks].insert user_id: jane_id, title: 'Jane Task'
|
8
|
+
|
9
|
+
conn[:tags].insert task_id: task_id, name: 'red'
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
RSpec.shared_context 'structs' do
|
2
|
+
include_context 'repo'
|
3
|
+
|
4
|
+
let(:user_struct) do
|
5
|
+
repo.users.mapper.model
|
6
|
+
end
|
7
|
+
|
8
|
+
let(:task_struct) do
|
9
|
+
repo.tasks.mapper.model
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:tag_struct) do
|
13
|
+
repo.tags.mapper.model
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:tag_with_task_struct) do
|
17
|
+
mapper_for(repo.tag_with_wrapped_task).model
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:user_with_tasks_struct) do
|
21
|
+
mapper_for(repo.users_with_tasks).model
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:user_with_task_struct) do
|
25
|
+
mapper_for(repo.users_with_task).model
|
26
|
+
end
|
27
|
+
|
28
|
+
let(:task_with_tags_struct) do
|
29
|
+
mapper_for(repo.tasks_with_tags).model
|
30
|
+
end
|
31
|
+
|
32
|
+
let(:task_with_user_struct) do
|
33
|
+
mapper_for(repo.task_with_user).model
|
34
|
+
end
|
35
|
+
|
36
|
+
let(:task_with_owner_struct) do
|
37
|
+
mapper_for(repo.task_with_owner).model
|
38
|
+
end
|
39
|
+
|
40
|
+
let(:jane) do
|
41
|
+
user_struct.new(id: 1, name: 'Jane')
|
42
|
+
end
|
43
|
+
|
44
|
+
let(:jane_with_tasks) do
|
45
|
+
user_with_tasks_struct.new(id: 1, name: 'Jane', all_tasks: [jane_task])
|
46
|
+
end
|
47
|
+
|
48
|
+
let(:jane_with_task) do
|
49
|
+
user_with_task_struct.new(id: 1, name: 'Jane', task: jane_task)
|
50
|
+
end
|
51
|
+
|
52
|
+
let(:jane_without_task) do
|
53
|
+
user_with_task_struct.new(id: 1, name: 'Jane', task: nil)
|
54
|
+
end
|
55
|
+
|
56
|
+
let(:jane_task) do
|
57
|
+
task_struct.new(id: 2, user_id: 1, title: 'Jane Task')
|
58
|
+
end
|
59
|
+
|
60
|
+
let(:task_with_user) do
|
61
|
+
task_with_user_struct.new(id: 2, user_id: 1, title: 'Jane Task', user: jane)
|
62
|
+
end
|
63
|
+
|
64
|
+
let(:task_with_owner) do
|
65
|
+
task_with_owner_struct.new(id: 2, user_id: 1, title: 'Jane Task', owner: jane)
|
66
|
+
end
|
67
|
+
|
68
|
+
let(:tag) do
|
69
|
+
tag_struct.new(id: 1, task_id: 2, name: 'red')
|
70
|
+
end
|
71
|
+
|
72
|
+
let(:task) do
|
73
|
+
task_struct.new(id: 2, user_id: 1, title: 'Jane Task')
|
74
|
+
end
|
75
|
+
|
76
|
+
let(:tag_with_task) do
|
77
|
+
tag_with_task_struct.new(id: 1, task_id: 2, name: 'red', task: task)
|
78
|
+
end
|
79
|
+
|
80
|
+
let(:task_with_tag) do
|
81
|
+
task_with_tags_struct.new(id: 2, user_id: 1, title: 'Jane Task', tags: [tag])
|
82
|
+
end
|
83
|
+
|
84
|
+
let(:user_with_task_and_tags) do
|
85
|
+
user_with_tasks_struct.new(id: 1, name: 'Jane', all_tasks: [task_with_tag])
|
86
|
+
end
|
87
|
+
|
88
|
+
let(:joe) do
|
89
|
+
user_struct.new(id: 2, name: 'Joe')
|
90
|
+
end
|
91
|
+
|
92
|
+
let(:joe_with_tasks) do
|
93
|
+
user_with_tasks_struct.new(id: 2, name: 'Joe', all_tasks: [joe_task])
|
94
|
+
end
|
95
|
+
|
96
|
+
let(:joe_with_task) do
|
97
|
+
user_with_task_struct.new(id: 2, name: 'Joe', task: joe_task)
|
98
|
+
end
|
99
|
+
|
100
|
+
let(:joe_task) do
|
101
|
+
task_struct.new(id: 1, user_id: 2, title: 'Joe Task')
|
102
|
+
end
|
103
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# this is needed for guard to work, not sure why :(
|
4
|
+
require "bundler"
|
5
|
+
Bundler.setup
|
6
|
+
|
7
|
+
if RUBY_ENGINE == "rbx"
|
8
|
+
require "codeclimate-test-reporter"
|
9
|
+
CodeClimate::TestReporter.start
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'rom-repository'
|
13
|
+
require 'rom-sql'
|
14
|
+
|
15
|
+
begin
|
16
|
+
require 'byebug'
|
17
|
+
rescue LoadError
|
18
|
+
end
|
19
|
+
|
20
|
+
root = Pathname(__FILE__).dirname
|
21
|
+
|
22
|
+
Dir[root.join('support/*.rb').to_s].each do |f|
|
23
|
+
require f
|
24
|
+
end
|
25
|
+
|
26
|
+
Dir[root.join('shared/*.rb').to_s].each do |f|
|
27
|
+
require f
|
28
|
+
end
|
29
|
+
|
30
|
+
# Namespace holding all objects created during specs
|
31
|
+
module Test
|
32
|
+
def self.remove_constants
|
33
|
+
constants.each(&method(:remove_const))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
RSpec.configure do |config|
|
38
|
+
config.after do
|
39
|
+
Test.remove_constants
|
40
|
+
end
|
41
|
+
|
42
|
+
config.include(MapperRegistry)
|
43
|
+
end
|