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 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
@@ -0,0 +1,3 @@
1
+ --color
2
+ --order random
3
+ --require ./spec/spec_helper
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,2 @@
1
+ require 'rom'
2
+ require 'rom/repository/base'
@@ -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