rom-repository 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|