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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4ae5380662bd3fb3fb249bf893a491bb0accbb35
|
4
|
+
data.tar.gz: 43da8d866c970abe7d471a19ccee12d9882d6d87
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c9fb3246025ba68d17f37e4364263975dba15776c9e516ea9caabb982a22b54555c9ab1ca8fb1eeaebae767b75c514b5aa2e06b3a550675e3cd4f7d23724b0f2
|
7
|
+
data.tar.gz: 0cde9b16532c6ab85b063480af8bacba6e05f668710752a3f94b07f4685ea0250a203dde2019565019dd3df6a258872085ebfb0fd16dc0ae49fc29356a501c75
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Gemfile.lock
|
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
language: ruby
|
2
|
+
sudo: false
|
3
|
+
cache: bundler
|
4
|
+
bundler_args: --without yard guard benchmarks tools
|
5
|
+
before_script:
|
6
|
+
- psql -c 'create database rom;' -U postgres
|
7
|
+
script: "bundle exec rake ci"
|
8
|
+
rvm:
|
9
|
+
- 2.0
|
10
|
+
- 2.1
|
11
|
+
- 2.2
|
12
|
+
- rbx-2
|
13
|
+
- jruby
|
14
|
+
- jruby-head
|
15
|
+
- ruby-head
|
16
|
+
env:
|
17
|
+
global:
|
18
|
+
- CODECLIMATE_REPO_TOKEN=173b95dae5c6ac281bc36a4b212291a89fed9b520b39a86bafdf603692250e60
|
19
|
+
- JRUBY_OPTS='--dev -J-Xmx1024M'
|
20
|
+
matrix:
|
21
|
+
allow_failures:
|
22
|
+
- rvm: ruby-head
|
23
|
+
- rvm: jruby-head
|
24
|
+
- rvm: rbx-2
|
25
|
+
notifications:
|
26
|
+
webhooks:
|
27
|
+
urls:
|
28
|
+
- https://webhooks.gitter.im/e/39e1225f489f38b0bd09
|
29
|
+
on_success: change
|
30
|
+
on_failure: always
|
31
|
+
on_start: false
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# v0.0.1 2015-08-05
|
2
|
+
|
3
|
+
First public release \o/
|
4
|
+
|
5
|
+
### Added
|
6
|
+
|
7
|
+
* `Relation#combine_parents` for auto-combine using eager-loading strategy aka
|
8
|
+
relation composition (solnic)
|
9
|
+
* `Relation#combine_children` for auto-combine using eager-loading strategy aka
|
10
|
+
relation composition (solnic)
|
11
|
+
* `Relation#wrap_parent` for auto-wrap using inner join (solnic)
|
12
|
+
* `Relation.view` for explicit relation view definitions with header and query (solnic)
|
13
|
+
* Auto-mapping feature which builds mappers that turn relations into simple structs (solnic)
|
data/Gemfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gemspec
|
4
|
+
|
5
|
+
gem 'rom', github: 'rom-rb/rom', branch: 'master'
|
6
|
+
gem 'rom-sql', github: 'rom-rb/rom-sql', branch: 'master'
|
7
|
+
gem 'inflecto'
|
8
|
+
|
9
|
+
group :test do
|
10
|
+
gem 'rspec'
|
11
|
+
gem 'byebug', platforms: :mri
|
12
|
+
gem 'pg', platforms: [:mri, :rbx]
|
13
|
+
gem 'pg_jruby', platforms: :jruby
|
14
|
+
gem "codeclimate-test-reporter", require: nil
|
15
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Piotr Solnica
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
[gem]: https://rubygems.org/gems/rom-repository
|
2
|
+
[travis]: https://travis-ci.org/rom-rb/rom-repository
|
3
|
+
[gemnasium]: https://gemnasium.com/rom-rb/rom-repository
|
4
|
+
[codeclimate]: https://codeclimate.com/github/rom-rb/rom-repository
|
5
|
+
[inchpages]: http://inch-ci.org/github/rom-rb/rom-repository
|
6
|
+
|
7
|
+
# ROM::Repository
|
8
|
+
|
9
|
+
[![Gem Version](https://badge.fury.io/rb/rom-repository.svg)][gem]
|
10
|
+
[![Build Status](https://travis-ci.org/rom-rb/rom-repository.svg?branch=master)][travis]
|
11
|
+
[![Dependency Status](https://gemnasium.com/rom-rb/rom-repository.png)][gemnasium]
|
12
|
+
[![Code Climate](https://codeclimate.com/github/rom-rb/rom-repository/badges/gpa.svg)][codeclimate]
|
13
|
+
[![Test Coverage](https://codeclimate.com/github/rom-rb/rom-repository/badges/coverage.svg)][codeclimate]
|
14
|
+
[![Inline docs](http://inch-ci.org/github/rom-rb/rom-repository.svg?branch=master)][inchpages]
|
15
|
+
|
16
|
+
Repository for [ROM](https://github.com/rom-rb/rom) with auto-mapping and relation
|
17
|
+
extensions.
|
18
|
+
|
19
|
+
## Conventions & Definitions
|
20
|
+
|
21
|
+
Repositories in ROM are simple objects that allow you to compose rich relation
|
22
|
+
views specific to your application layer. Every repository can access multiple
|
23
|
+
relations that you define and use them to build more complex views.
|
24
|
+
|
25
|
+
Repository relations are enhanced with a couple of extra features on top of ROM
|
26
|
+
relations:
|
27
|
+
|
28
|
+
- Every relation has an auto-generated mapper which turns raw data into simple, immutable structs
|
29
|
+
- `Relation#combine` can accept a simple hash defining what other relations should be joined
|
30
|
+
- `Relation#combine_parents` automatically joins parents using eager-loading
|
31
|
+
- `Relation#combine_children` automatically joins children using eager-loading
|
32
|
+
- `Relation#wrap_parent` automatically joins a parent using inner join
|
33
|
+
|
34
|
+
### Relation Views
|
35
|
+
|
36
|
+
A relation view is a result of some query which returns results specific to your
|
37
|
+
application. You can define them using a simple DSL where you specify a name, what
|
38
|
+
attributes resulting tuples will have and of course the query itself:
|
39
|
+
|
40
|
+
``` ruby
|
41
|
+
class Users < ROM::Relation[:sql]
|
42
|
+
view(:by_id, [:id, :name]) do |id|
|
43
|
+
where(id: id).select(:id, :name)
|
44
|
+
end
|
45
|
+
|
46
|
+
view(:listing, [:id, :name, :email, :created_at]) do
|
47
|
+
select(:id, :name, :email, :created_at).order(:name)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
This way we can explicitly define all our relation view that our application will
|
53
|
+
depend on. It encapsulates access to application-specific data structures and allows
|
54
|
+
you to easily test individual views in isolation.
|
55
|
+
|
56
|
+
Thanks to explicit definition of attributes mappers are derived automatically.
|
57
|
+
|
58
|
+
### Auto-combine & Auto-wrap
|
59
|
+
|
60
|
+
Repository relations support automatic `combine` and `wrap` by using a simple
|
61
|
+
convention that every relation defines `for_combine(keys, other)` and `for_wrap(keys, other)`.
|
62
|
+
|
63
|
+
You can override the default behavior for combine by defining `for_other_rel_name`
|
64
|
+
in example, if you combine tasks with users you can define `Tasks#for_users` and
|
65
|
+
this will be used instead of the generic `for_combine`.
|
66
|
+
|
67
|
+
### Mapping & Structs
|
68
|
+
|
69
|
+
Currently repositories map to `ROM::Struct` by default. In the near future this
|
70
|
+
will be configurable.
|
71
|
+
|
72
|
+
ROM structs are simple and don't expose an interface to mutate them; however, they
|
73
|
+
are not being frozen (at least not yet, we could add a feature for freezing them).
|
74
|
+
|
75
|
+
They are coercible to `Hash` so it should be possible to map them further in some
|
76
|
+
special cases using ROM mappers.
|
77
|
+
|
78
|
+
``` ruby
|
79
|
+
class Users < ROM::Relation[:sql]
|
80
|
+
view(:by_id, [:id, :name]) do |id|
|
81
|
+
where(id: id).select(:id, :name)
|
82
|
+
end
|
83
|
+
|
84
|
+
view(:listing, [:id, :name, :email, :created_at]) do
|
85
|
+
select(:id, :name, :email, :created_at).order(:name)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class UserRepository < ROM::Repository::Base
|
90
|
+
relations :users, :tasks
|
91
|
+
|
92
|
+
def by_id(id)
|
93
|
+
users.by_id(id)
|
94
|
+
end
|
95
|
+
|
96
|
+
def with_tasks(id)
|
97
|
+
users.by_id(id).combine_children(many: tasks)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
rom = ROM.finalize.env
|
102
|
+
|
103
|
+
user_repo = UserRepository.new(rom)
|
104
|
+
|
105
|
+
puts user_repo.by_id(1).to_a.inspect
|
106
|
+
# [#<ROM::Struct[User] id=1 name="Jane">]
|
107
|
+
|
108
|
+
puts user_repo.with_tasks.to_a.inspect
|
109
|
+
# [#<ROM::Struct[User] id=1 name="Jane" tasks=[#<ROM::Struct[Task] id=2 user_id=1 title="Jane Task">]>, #<ROM::Struct[User] id=2 name="Joe" tasks=[#<ROM::Struct[Task] id=1 user_id=2 title="Joe Task">]>]
|
110
|
+
```
|
111
|
+
|
112
|
+
### Decorating Structs
|
113
|
+
|
114
|
+
Nothing is stopping you from decorating your structs using registered mappers and
|
115
|
+
custom decorator models:
|
116
|
+
|
117
|
+
``` ruby
|
118
|
+
class UserStructMapper < ROM::Mapper
|
119
|
+
register_as :ui_presenter
|
120
|
+
model UI::UserPresenter
|
121
|
+
end
|
122
|
+
|
123
|
+
user_repo.by_id(1).as(:ui_presenter)
|
124
|
+
```
|
125
|
+
|
126
|
+
## Limitations
|
127
|
+
|
128
|
+
This is an early alpha and works only with rom-sql for now. There are a couple
|
129
|
+
improvements waiting to be done in the rom core and then rom-repository will receive
|
130
|
+
more love and features.
|
131
|
+
|
132
|
+
Stay tuned.
|
133
|
+
|
134
|
+
## License
|
135
|
+
|
136
|
+
See `LICENSE` file.
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "rspec/core/rake_task"
|
2
|
+
|
3
|
+
RSpec::Core::RakeTask.new(:spec)
|
4
|
+
task default: [:ci]
|
5
|
+
|
6
|
+
desc "Run CI tasks"
|
7
|
+
task ci: [:spec]
|
8
|
+
|
9
|
+
begin
|
10
|
+
require "rubocop/rake_task"
|
11
|
+
|
12
|
+
Rake::Task[:default].enhance [:rubocop]
|
13
|
+
|
14
|
+
RuboCop::RakeTask.new do |task|
|
15
|
+
task.options << "--display-cop-names"
|
16
|
+
end
|
17
|
+
rescue LoadError
|
18
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'rom/support/options'
|
2
|
+
|
3
|
+
require 'rom/repository/ext/relation'
|
4
|
+
|
5
|
+
require 'rom/repository/mapper_builder'
|
6
|
+
require 'rom/repository/loading_proxy'
|
7
|
+
|
8
|
+
module ROM
|
9
|
+
class Repository < Gateway
|
10
|
+
# Abstract repository class to inherit from
|
11
|
+
#
|
12
|
+
# TODO: rename this to Repository once deprecated Repository from rom core is gone
|
13
|
+
#
|
14
|
+
# @api public
|
15
|
+
class Base # :trollface:
|
16
|
+
include Options
|
17
|
+
|
18
|
+
option :mapper_builder, reader: true, default: proc { MapperBuilder.new }
|
19
|
+
|
20
|
+
# Define which relations your repository is going to use
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# class MyRepo < ROM::Repository::Base
|
24
|
+
# relations :users, :tasks
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# my_repo = MyRepo.new(rom_env)
|
28
|
+
#
|
29
|
+
# my_repo.users
|
30
|
+
# my_repo.tasks
|
31
|
+
#
|
32
|
+
# @return [Array<Symbol>]
|
33
|
+
#
|
34
|
+
# @api public
|
35
|
+
def self.relations(*names)
|
36
|
+
if names.any?
|
37
|
+
attr_reader(*names)
|
38
|
+
@relations = names
|
39
|
+
else
|
40
|
+
@relations
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# @api private
|
45
|
+
def initialize(env, options = {})
|
46
|
+
super
|
47
|
+
self.class.relations.each do |name|
|
48
|
+
proxy = LoadingProxy.new(
|
49
|
+
env.relation(name), name: name, mapper_builder: mapper_builder
|
50
|
+
)
|
51
|
+
instance_variable_set("@#{name}", proxy)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'rom/repository/ext/relation/view_dsl'
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
module SQL
|
5
|
+
# A bunch of extensions that will be ported to other adapters
|
6
|
+
#
|
7
|
+
# @api public
|
8
|
+
class Relation < ROM::Relation
|
9
|
+
# @api private
|
10
|
+
def self.inherited(klass)
|
11
|
+
super
|
12
|
+
klass.class_eval do
|
13
|
+
exposed_relations.merge(Set[:columns, :for_combine, :for_wrap])
|
14
|
+
|
15
|
+
defines :attributes
|
16
|
+
attributes({})
|
17
|
+
|
18
|
+
option :attributes, reader: true, default: -> relation { relation.class.attributes }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Define a relation view with a specific header
|
23
|
+
#
|
24
|
+
# With headers defined all the mappers will be inferred automatically
|
25
|
+
#
|
26
|
+
# @example
|
27
|
+
# class Users < ROM::Relation[:sql]
|
28
|
+
# view(:by_name, [:id, :name]) do |name|
|
29
|
+
# where(name: name)
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# view(:listing, [:id, :name, :email]) do
|
33
|
+
# select(:id, :name, :email).order(:name)
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# @api public
|
38
|
+
def self.view(*args, &block)
|
39
|
+
name, names, relation_block =
|
40
|
+
if block.arity == 0
|
41
|
+
ViewDSL.new(*args, &block).call
|
42
|
+
else
|
43
|
+
[*args, block]
|
44
|
+
end
|
45
|
+
|
46
|
+
attributes[name] = names
|
47
|
+
|
48
|
+
define_method(name, &relation_block)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Return column names that will be selected for this relation
|
52
|
+
#
|
53
|
+
# By default we use dataset columns but first we look at configured
|
54
|
+
# attributes by `view` DSL
|
55
|
+
#
|
56
|
+
# @return [Array<Symbol>]
|
57
|
+
#
|
58
|
+
# @api private
|
59
|
+
def columns
|
60
|
+
self.class.attributes.fetch(name, dataset.columns)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Default methods for fetching combined relation
|
64
|
+
#
|
65
|
+
# This method is used by default by `combine`
|
66
|
+
#
|
67
|
+
# @return [SQL::Relation]
|
68
|
+
#
|
69
|
+
# @api private
|
70
|
+
def for_combine(keys, relation)
|
71
|
+
pk, fk = keys.to_a.flatten
|
72
|
+
where(fk => relation.map { |tuple| tuple[pk] })
|
73
|
+
end
|
74
|
+
|
75
|
+
# Default methods for fetching wrapped relation
|
76
|
+
#
|
77
|
+
# This method is used by default by `wrap` and `wrap_parents`
|
78
|
+
#
|
79
|
+
# @return [SQL::Relation]
|
80
|
+
#
|
81
|
+
# @api private
|
82
|
+
def for_wrap(name, keys)
|
83
|
+
other = __registry__[name]
|
84
|
+
|
85
|
+
inner_join(name, keys)
|
86
|
+
.select(*qualified.header.columns)
|
87
|
+
.select_append(*other.prefix(other.name).qualified.header)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Infer foreign_key name for this relation
|
91
|
+
#
|
92
|
+
# TODO: this should be configurable and handled by an injected policy
|
93
|
+
#
|
94
|
+
# @return [Symbol]
|
95
|
+
#
|
96
|
+
# @api private
|
97
|
+
def foreign_key
|
98
|
+
:"#{Inflector.singularize(name)}_id"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# TODO: remove this once Relation::Lazy is gone
|
104
|
+
class Relation
|
105
|
+
class Lazy
|
106
|
+
def base_name
|
107
|
+
relation.name
|
108
|
+
end
|
109
|
+
|
110
|
+
def primary_key
|
111
|
+
relation.primary_key
|
112
|
+
end
|
113
|
+
|
114
|
+
def foreign_key
|
115
|
+
relation.foreign_key
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
class Curried < Lazy
|
120
|
+
def columns
|
121
|
+
relation.attributes.fetch(options[:name], relation.columns)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|