receptacle 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3c3b2dda14cdba3e98e04dbe1c8610f5718f4979
4
+ data.tar.gz: d8ab4f84c9826113be2b566c659ee24b045bff96
5
+ SHA512:
6
+ metadata.gz: 299d55e1aa3afa8ed26a9a92083afd4f9c2320314a1fb3489f87123ed4d4c55774167fc5323b666de706e3cfece8f1a4c2705ce052068041cd23dc7feb3c6c2b
7
+ data.tar.gz: 9531ff2e7ce214b57a7fdbd9755058682551a4cbc3f5a527140b2ef3a67b261f215999be70292b44f56740e19493b5c295f2f96884dbe5bee559922991ea454a
data/.dir-locals.el ADDED
@@ -0,0 +1 @@
1
+ ((ruby-mode . ( (ruby-test-runner . minitest)) ) )
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ .ruby-version
data/.rubocop.yml ADDED
@@ -0,0 +1,27 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.3
3
+
4
+ Metrics/LineLength:
5
+ Max: 99
6
+ Exclude:
7
+ - test/*
8
+
9
+ Metrics/BlockLength:
10
+ Exclude:
11
+ - '*.gemspec'
12
+
13
+ Metrics/AbcSize:
14
+ Exclude:
15
+ - test/*
16
+
17
+ Metrics/ClassLength:
18
+ Exclude:
19
+ - test/*
20
+
21
+ Metrics/MethodLength:
22
+ Exclude:
23
+ - test/*
24
+
25
+
26
+ Style/Documentation:
27
+ Enabled: false
data/.travis.yml ADDED
@@ -0,0 +1,27 @@
1
+ language: ruby
2
+ cache: bundler
3
+ rvm:
4
+ - 2.3.3
5
+ - 2.4.0
6
+ - jruby-9.1.7.0
7
+ jdk:
8
+ - oraclejdk8
9
+ # - openjdk7
10
+ env:
11
+ - "JRUBY_OPTS='--dev'"
12
+ - "JRUBY_OPTS='-Xcompile.invokedynamic=true'"
13
+ matrix:
14
+ exclude:
15
+ - rvm: 2.3.3
16
+ jdk: oraclejdk8
17
+ env: "JRUBY_OPTS='-Xcompile.invokedynamic=true'"
18
+ - rvm: 2.4.0
19
+ jdk: oraclejdk8
20
+ env: "JRUBY_OPTS='-Xcompile.invokedynamic=true'"
21
+ allow_failures:
22
+ - rvm: jruby-9.1.7.0
23
+ jdk: oraclejdk8
24
+ env: "JRUBY_OPTS='-Xcompile.invokedynamic=true'"
25
+ before_install:
26
+ - gem update --system
27
+ - gem install bundler -v 1.14.3
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at dev@eger-andreas.de. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ source 'https://rubygems.org'
3
+
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ # A sample Guardfile
3
+ # More info at https://github.com/guard/guard#readme
4
+
5
+ ## Uncomment and set this to only include directories you want to watch
6
+ # directories %w(app lib config test spec features) \
7
+ # .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
8
+
9
+ ## Note: if you are using the `directories` clause above and you are not
10
+ ## watching the project directory ('.'), then you will want to move
11
+ ## the Guardfile to a watched dir and symlink it back, e.g.
12
+ #
13
+ # $ mkdir config
14
+ # $ mv Guardfile config/
15
+ # $ ln -s config/Guardfile .
16
+ #
17
+ # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
18
+ group :red_green_refactor, halt_on_fail: true do
19
+ guard :minitest do
20
+ # with Minitest::Unit
21
+ watch(%r{^test/(.*)\/?test_(.*)\.rb$})
22
+ watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}test_#{m[2]}.rb" }
23
+ watch(%r{^test/test_helper\.rb$}) { 'test' }
24
+ watch(%r{^test/fixture\.rb$}) { 'test' }
25
+ end
26
+ guard :rubocop, all_on_start: false, cli: ['--auto-correct'] do
27
+ watch(/.+\.rb$/)
28
+ watch(%r{(?:.+/)?\.rubocop\.yml%}) { |m| File.dirname(m[0]) }
29
+ end
30
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Andreas Eger
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,355 @@
1
+ # Receptacle
2
+
3
+
4
+ [![Gem Version](https://badge.fury.io/rb/receptacle.svg)](https://badge.fury.io/rb/receptacle)
5
+ [![Build Status](https://travis-ci.org/andreaseger/receptacle.svg?branch=master)](https://travis-ci.org/andreaseger/receptacle)
6
+ [![codecov](https://codecov.io/gh/andreaseger/receptacle/branch/master/graph/badge.svg)](https://codecov.io/gh/andreaseger/receptacle)
7
+
8
+ ## About
9
+
10
+ Provides easy and fast means to use the repository pattern to create separation
11
+ between your business logic and your data sources.
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ ```ruby
18
+ gem 'receptacle'
19
+ ```
20
+
21
+ And then execute:
22
+
23
+ $ bundle
24
+
25
+ Or install it yourself as:
26
+
27
+ $ gem install receptacle
28
+
29
+ ## Usage
30
+
31
+ A repository mediates requests based on it's configuration to a strategy which
32
+ then itself implements the necessary functions to access the data source.
33
+
34
+ ```
35
+ +--------------------+ +--------+
36
+ | | |Database|
37
+ | DatabaseStrategy +------> |
38
+ | | | |
39
+ +--------------------+ +--------------+ +----------^---------+ | |
40
+ | | | | | +--------+
41
+ | Business Logic +-----> Repository +----------------+
42
+ | | | |
43
+ +--------------------+ +--------|-----+ +--------------------+
44
+ | | |
45
+ | | InMemoryStrategy |
46
+ +-------|-----+ | |
47
+ |Configuration| +--------------------+
48
+ +-------------+
49
+ ```
50
+
51
+ Let's look at the pieces:
52
+
53
+ 1. the repository itself - which is a simple module including the
54
+ `Receptacle` mixin
55
+
56
+ ```ruby
57
+ module Repository
58
+ module User
59
+ include Receptacle::Repo
60
+
61
+ mediate :find
62
+ end
63
+ end
64
+ ```
65
+
66
+ 2. at least one strategy class which are implemented as plain ruby classes
67
+
68
+ ```ruby
69
+ module Strategy
70
+ class Database
71
+ def find(id:)
72
+ # get data from data source and return a business entity
73
+ end
74
+ end
75
+ end
76
+ ```
77
+
78
+ Optionally wrapper classes can be defined
79
+
80
+ ```ruby
81
+ module Wrapper
82
+ class Validator
83
+ def before_find(id:)
84
+ raise ArgumentError if id.nil?
85
+ {id: id}
86
+ end
87
+ end
88
+ class ModelMapper
89
+ def after_find(return_value, **_kwargs)
90
+ Model::User.new(return_value)
91
+ end
92
+ end
93
+ end
94
+ ```
95
+
96
+ ### Example
97
+
98
+ Everything combined a simple example could look like the following:
99
+
100
+ ```ruby
101
+ require "receptacle"
102
+
103
+ module Repository
104
+ module User
105
+ include Receptacle::Repo
106
+ mediate :find
107
+
108
+ module Strategy
109
+ class DB
110
+ def find(id:)
111
+ # code to find from the database
112
+ end
113
+ end
114
+ class InMemory
115
+ def find(id:)
116
+ # code to find from InMemory store
117
+ end
118
+ end
119
+ end
120
+
121
+ module Wrapper
122
+ class Validator
123
+ def before_find(id:)
124
+ raise ArgumentError if id.nil?
125
+ {id: id}
126
+ end
127
+ end
128
+ class ModelMapper
129
+ def after_find(return_value, **_kwargs)
130
+ Model::User.new(return_value)
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+ ```
137
+
138
+ For better separation to other repositories the fact that the repository itself
139
+ is a module can be used to nest both strategies and wrapper underneath.
140
+
141
+ Somewhere in your application config you now need to setup the strategy and the
142
+ wrappers for this repository like this:
143
+
144
+ ```ruby
145
+ Repository::User.strategy Repository::User::Strategy::DB
146
+ Repository::User.wrappers [Repository::User::Wrapper::Validator,
147
+ Repository::User::Wrapper::ModelMapper])
148
+ ```
149
+
150
+ With this setup to use the repository method is as simple and straight forward
151
+ as calling `Repository::User.find(id: 123)`
152
+
153
+ ## Repository Pattern
154
+
155
+ What is the matter with this repository pattern and why should I care using it?
156
+
157
+ ### Motivation
158
+
159
+ Often the business logic of applications directly accesses a data source like a
160
+ database. This has several disadvantages such as
161
+
162
+ - code duplication cased by repeated need to transform raw data into business
163
+ entities
164
+ - no separation between business logic and access to the data source
165
+ - harder to add or change global policies like caching
166
+ - caused by missing isolation it's harder to test the business logic independent
167
+ from the data source
168
+
169
+ ### Solution
170
+
171
+ To improve on the disadvantages above and more we can introduce a repository
172
+ which mediates between the business logic and the data source. The data source
173
+ can be for example a database, an API(be it internal or external) or other web
174
+ services.
175
+
176
+ A repository provides the business logic with a stable interface to interact
177
+ with the data source. Hereby is the repository mapping the data to business
178
+ entities. Because the repository is a central place to access the data source
179
+ caching policies or similar can be applied easily there.
180
+
181
+ During testing the repository can be switched to a different strategy for
182
+ example a fast and lightweight in memory data store to ease the process of
183
+ testing the business logic.
184
+
185
+ Due to the ability to switch strategies a repository can also help to keep the
186
+ application architecture flexible as a change in strategy has no impact on the
187
+ business logic above.
188
+
189
+ ## Details
190
+
191
+ ### Strategy
192
+
193
+ A strategy is implemented as simple ruby class which implements the direct
194
+ access to a data source by implementing the same method (as instance method)
195
+ which was setup in the repository.
196
+
197
+ On each call to the repository a new instance of this class is created on which
198
+ then the mediated method is called.
199
+
200
+ ```ruby
201
+ module Strategy
202
+ class Database
203
+ def find(id:)
204
+ # get data from data source and return a business entity
205
+ end
206
+ end
207
+ end
208
+ ```
209
+
210
+ Due to the nature of creating a new instance on each method call persistent
211
+ connections to the data source like a connection pool should be maintained
212
+ outside the strategy itself. For example in a singleton class.
213
+
214
+ ### Wrapper
215
+
216
+ In addition to create a separation between data access and business logic often
217
+ there is the need to perform certain actions in the context of a data source
218
+ access. For example there can be the need to send message on a message bus whenever a
219
+ resource was created - independent of the strategy.
220
+
221
+ This gem allow one to add such actions without adding them to all strategies or
222
+ applying them in the business logic by using wrappers.
223
+
224
+ One or multiple wrappers sit logically between the repository and the
225
+ strategies. Based on the repository configuration it knows when and in which
226
+ order they should be applied. Right now there is support for 2 1/2 types of
227
+ actions.
228
+
229
+ 1. a _before_ method action: This action is called before the final strategy
230
+ method is executed. It has access to the method parameter and can even modify
231
+ them.
232
+ 2. a _after_ method action: This action is called after the strategy method was
233
+ executed and has access to the method parameters passed to the strategy
234
+ method and the return value. The return value could be modified here too.
235
+
236
+ The extra 1/2 action type is born by the fact that if a single wrapper class
237
+ implements both an before and after action for the same method the same wrapper
238
+ instance is used to execute both. Although this doesn't cover the all use cases
239
+ an _around_ method action would but many which need state before and after the
240
+ data source is accessed are covered.
241
+
242
+ #### Implementation
243
+
244
+ Wrapper actions are implemented as plain ruby classes which provide instance
245
+ methods named like `before_<method_name>` or `after_<method_name>` where
246
+ `<method_name>` is the repository/strategy method this action should be applied
247
+ to.
248
+
249
+ ```ruby
250
+ module Wrapper
251
+ class Validator
252
+ def before_find(id:)
253
+ raise ArgumentError if id.nil?
254
+ {id: id}
255
+ end
256
+ end
257
+ end
258
+ ```
259
+
260
+ This wrapper class would provide a before action for the `find` method. The
261
+ return value of this wrapper will be used as parameters for the strategy method
262
+ (or the next wrapper in line). Keyword arguments can simply be returned as hash.
263
+
264
+ If multiple wrapper classes are defined the before wrapper actions are executed
265
+ in the order the wrapper classes are defined while the after actions are applied
266
+ in reverse order.
267
+
268
+ ### Memory Strategy
269
+
270
+ Although currently not part of the gem a simple memory strategy can be
271
+ implemented in this way:
272
+
273
+ ```ruby
274
+ class MemoryStore
275
+ class << self; attr_accessor :store end
276
+
277
+ def clear
278
+ self.class.store = []
279
+ end
280
+
281
+ private def store
282
+ self.class.store || clear
283
+ end
284
+ end
285
+ ```
286
+
287
+ ## How does it compare to other repository pattern implementations
288
+
289
+ Compared to other gem implementing the repository pattern this gem makes no
290
+ assumptions regarding the interface of your repository or what kind of data
291
+ source is used.
292
+ Some alternative have some interesting features nevertheless:
293
+
294
+ - [Hanami::Repository](https://github.com/hanami/model#repositories) is for one
295
+ closely tied to the the Hanami entities and does not separate the repository
296
+ interface from the implementing strategies. For straight forward mapping of
297
+ entity to data source this might be enough though. Another caveat is that it
298
+ currently only supports SQL data sources.
299
+ - [ROM::Repository](http://rom-rb.org/learn/repositories/) similarly is tied to
300
+ other facilities of ROM like the ROM containers. It also appears to take a
301
+ similar approach as Hanami to custom queries which should not leak to the
302
+ outside application. There is predefined interface for manipulating resources
303
+ through. The addition of `ROM::Changeset` brings an interesting addition to
304
+ the mix which might make it an interesting alternative if using `ROM` fits
305
+ into the applications structure.
306
+
307
+ This gem on the other hand makes absolutely no assumptions about your data
308
+ source or general structure of your code. It can be simply plugged in between
309
+ your business logic and data source to abstract the two. Of cause like the other
310
+ repository pattern implementations strategy details should be hidden from the
311
+ interface. The data source can essentially be anything. A SQL database, a no-SQL
312
+ database, a JSON API or even a gem. Placing a gem behind a repository can be
313
+ useful if you're not yet sure this is the correct or best possible gem,
314
+ the [faraday](https://github.com/lostisland/faraday) gem is essentially doing
315
+ this by giving all the different http libraries a common interface).
316
+
317
+
318
+ ## Goals of this implementation
319
+
320
+ - small core codebase
321
+ - minimal processing overhead - fast method dispatching
322
+ - flexible - all kind of methods should possible to be mediated
323
+ - basic but powerful callbacks/hooks/observer possibilities
324
+
325
+ ## Development
326
+
327
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
328
+ `rake test` to run the tests. You can also run `bin/console` for an interactive
329
+ prompt that will allow you to experiment.
330
+
331
+ To install this gem onto your local machine, run `bundle exec rake install`. To
332
+ release a new version, update the version number in `version.rb`, and then run
333
+ `bundle exec rake release`, which will create a git tag for the version, push
334
+ git commits and tags, and push the `.gem` file
335
+ to [rubygems.org](https://rubygems.org).
336
+
337
+ ## Contributing
338
+
339
+ Bug reports and pull requests are welcome on GitHub at
340
+ https://github.com/andreaseger/receptacle. This project is intended to be a safe,
341
+ welcoming space for collaboration, and contributors are expected to adhere to
342
+ the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
343
+
344
+ ## Attribution
345
+
346
+ [Runtastic][runtastic] is using the repository pattern extensively in its
347
+ backend services and inspired the creation of this library. Nevertheless no code
348
+ developed at [Runtastic][runtastic] was used in this library.
349
+
350
+ ## License
351
+
352
+ The gem is available as open source under the terms of
353
+ the [MIT License](http://opensource.org/licenses/MIT).
354
+
355
+ [runtastic]: https://github.com/runtastic
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+ require 'bundler/gem_tasks'
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.libs << 'test'
7
+ t.libs << 'lib'
8
+ t.test_files = FileList['test/**/test_*.rb']
9
+ end
10
+
11
+ begin
12
+ require 'rubocop/rake_task'
13
+ RuboCop::RakeTask.new
14
+ namespace :rubocop do
15
+ desc 'Install Rubocop as pre-commit hook'
16
+ task :install do
17
+ require 'rubocop_runner'
18
+ RubocopRunner.install
19
+ end
20
+ end
21
+ rescue LoadError
22
+ p 'rubocop not installed'
23
+ end
24
+
25
+ task default: :test
data/bin/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'receptacle'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ require 'pry'
11
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ bundle exec rake rubocop:install