errol 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 +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +220 -0
- data/Rakefile +17 -0
- data/errol.gemspec +26 -0
- data/lib/errol.rb +8 -0
- data/lib/errol/entity.rb +109 -0
- data/lib/errol/inquiry.rb +33 -0
- data/lib/errol/repository.rb +178 -0
- data/lib/errol/version.rb +3 -0
- data/test/entity/access_generation_test.rb +75 -0
- data/test/entity/access_methods_test.rb +32 -0
- data/test/entity/entity_test.rb +35 -0
- data/test/entity/repository_method_test.rb +92 -0
- data/test/inquiry/inquiry_test.rb +57 -0
- data/test/repository/contruction_test.rb +62 -0
- data/test/repository/page_test.rb +163 -0
- data/test/repository/query_test.rb +190 -0
- data/test/test_config.rb +27 -0
- metadata +144 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c43849e71e1ed66ec52d37a22663f6b4d44332e9
|
4
|
+
data.tar.gz: 41200a851e4a32ba4e67a52f7887a4550ed94348
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9ce82562e4c6f32be9824a1547630942750e818a3de7010ed4af9bc9e1cb9e96f1345d64570e047d3d85b239c102075b5b5d1ea85a1e802d9f6789f541937ad7
|
7
|
+
data.tar.gz: 051da105e5b3623b20fd0902cc6ae68a35eee31dda201c4265f6f54241052a8acc7f5974b10654f0c23e7ba6bcf3795d38317824f0f0c6c83bf9145aaf75820a
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Peter Saxton
|
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,220 @@
|
|
1
|
+
# Errol
|
2
|
+
|
3
|
+
**Repository build on top of [sequel](http://sequel.jeremyevans.net/) to store simple data records.** Errol encourages a separation of concerns in the domain model of a system where data is persisted. It uses these extra components to include a default solution for paginating its records.
|
4
|
+
|
5
|
+
They key components are the *Repository* that is responsible for storing and retrieving *Records* in response to various *Inquiries*. Records are not the same as the bloated god objects of active record but should remain simple data stores. The responsibility of a record is representing a database row with domain friendly types, this means serialising data. Where the behaviour that is no longer represented on your model now belongs is dependant on the system. It is often worth having an object that wraps the data record to define extra behaviour as well as methods to save. Errol ships with an entity that can be used as a starting point for these objects.
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'errol'
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install errol
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
Usage is best described by each of the components
|
28
|
+
|
29
|
+
### Inquiry
|
30
|
+
The inquiry object is simply a store of data that was sent with each inquiry plus an optional set of defaults. The repository assumes that a page and page_size is available when using the ability to paginate
|
31
|
+
|
32
|
+
```rb
|
33
|
+
class Posts
|
34
|
+
class Inquiry < Errol::Inquiry
|
35
|
+
default :order, :id
|
36
|
+
default :page, 1
|
37
|
+
default :page_size, 12
|
38
|
+
default :published, true
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
inquiry = Inquiry.new :page => 2
|
43
|
+
inquiry.page
|
44
|
+
# => 2
|
45
|
+
inquiry.published?
|
46
|
+
# => true
|
47
|
+
inquiry.other
|
48
|
+
# => raise DefaultValueUndefined
|
49
|
+
```
|
50
|
+
|
51
|
+
### Records
|
52
|
+
These are simply sequel models. there is no wrapper and you define them as sequel models
|
53
|
+
|
54
|
+
```rb
|
55
|
+
class Post
|
56
|
+
class Record < Sequel::Model(:posts)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
```
|
60
|
+
|
61
|
+
### Repository
|
62
|
+
The repository contains all the records, instances of a repository contain a subset dependant on the inquiry, by default the inquiry will be used to paginate.
|
63
|
+
|
64
|
+
```rb
|
65
|
+
class Posts < Errol::Repository
|
66
|
+
|
67
|
+
class << self
|
68
|
+
# The repository needs to know what records it is storing
|
69
|
+
def record_class
|
70
|
+
Post::Record
|
71
|
+
end
|
72
|
+
|
73
|
+
# The repository needs to know how to build a requirments hash that may be empty into a inquiry containing defaults
|
74
|
+
def inquiry(requirements)
|
75
|
+
Posts::Inquiry.new(requirements)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# This the odd part, the dataset method is called when returning data. This a custom method that allows arbitrarily complex manipulation of data using settings from the inquiry
|
80
|
+
def dataset
|
81
|
+
partial = raw_dataset
|
82
|
+
partial = partial.order(inquiry.order)
|
83
|
+
partial = partial.where(:published) if inquiry.published
|
84
|
+
partial
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
page = Posts.new
|
89
|
+
# First page of published posts ordered by id
|
90
|
+
|
91
|
+
page.first_page?
|
92
|
+
# => true
|
93
|
+
|
94
|
+
page.page_size
|
95
|
+
# => 12
|
96
|
+
|
97
|
+
page.each { |post| puts post }
|
98
|
+
# Outputs each of the posts on the page
|
99
|
+
|
100
|
+
# class methods also use an inquiry to filter data
|
101
|
+
|
102
|
+
Posts.each { |post| puts post }
|
103
|
+
# Outputs each published post in the dataset ordered by id
|
104
|
+
|
105
|
+
Posts.each(:published => false, :order => :created_at) { |post| puts post }
|
106
|
+
# Outputs each post in the database ordered by creation date
|
107
|
+
|
108
|
+
#if you want a filtered dataset which is not paginated pass false as the last argument
|
109
|
+
set = Posts.new({:order => :created_at}, false)
|
110
|
+
```
|
111
|
+
|
112
|
+
### Entity
|
113
|
+
Entities are an optional wrapper around the record objects. My personal criteria as to if can add something to my record is can this object still be represented by an open struct? This allows me to test my entities with Ostructs and see the data thats is pulled or pushed to them. Use a entity for anything else. Say we want a post intro which is the first 20 words of a post. *I often name my entities after the domain object and name space the record below them, this is because I never interact with the data record.*
|
114
|
+
|
115
|
+
```rb
|
116
|
+
# Without Errol::Entity
|
117
|
+
class Post
|
118
|
+
def initialize(record)
|
119
|
+
@record = record
|
120
|
+
end
|
121
|
+
|
122
|
+
attr_reader :record
|
123
|
+
|
124
|
+
def intro
|
125
|
+
body.split[' '][0, 3].join(' ') + '...'
|
126
|
+
end
|
127
|
+
|
128
|
+
def body
|
129
|
+
record.body
|
130
|
+
end
|
131
|
+
|
132
|
+
def title
|
133
|
+
record.title
|
134
|
+
end
|
135
|
+
|
136
|
+
def save
|
137
|
+
Posts.save record
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# With Errol::Entity
|
142
|
+
class Customer < Errorl::Entity
|
143
|
+
repository = Customers
|
144
|
+
entry_accessor :title, :body
|
145
|
+
|
146
|
+
def intro
|
147
|
+
body.split[' '][0, 3].join(' ') + '...'
|
148
|
+
end
|
149
|
+
end
|
150
|
+
```
|
151
|
+
|
152
|
+
If you are working with entities you might not want to have to keep wrapping records. The repository allows you to define a method for sending out and accepting entities.
|
153
|
+
|
154
|
+
```rb
|
155
|
+
class Posts < Errol::Repository
|
156
|
+
class << self
|
157
|
+
def dispatch(record)
|
158
|
+
Post.new(record)
|
159
|
+
end
|
160
|
+
|
161
|
+
def receive(entity)
|
162
|
+
entity.record
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
```
|
167
|
+
|
168
|
+
## Documentation
|
169
|
+
|
170
|
+
### Entity
|
171
|
+
|
172
|
+
**::entry_reader** `entity.entry_reader(*entries) => self`
|
173
|
+
|
174
|
+
defines reader access to entries on the record which is short hand for calling `entity.record.entry`
|
175
|
+
|
176
|
+
**::entry_writer** `entity.entry_writer(*entries) => self`
|
177
|
+
|
178
|
+
defines writer access to entries on the record which is short hand for calling `entity.record.entry = value`
|
179
|
+
|
180
|
+
**::boolean_query** `entity.boolean_query(*entries) => self`
|
181
|
+
|
182
|
+
defines query(appends '?' on method name) to entries on the record which is short hand for calling `!!entity.record.entry`
|
183
|
+
|
184
|
+
**::entry_accessor** `entity.entry_accessor(*entries) => self`
|
185
|
+
|
186
|
+
short hand for entry_reader and entry_writer for entries
|
187
|
+
|
188
|
+
**::boolean_accessor** `entity.boolean_accessor(*entries) => self`
|
189
|
+
|
190
|
+
short hand for boolean_query and entry_writer for entries
|
191
|
+
|
192
|
+
**#set** `entity.set(**attributes) => self`
|
193
|
+
|
194
|
+
sends each item in the hash to a method matching the key with argument of the value
|
195
|
+
|
196
|
+
**#save** `entity.save => self`
|
197
|
+
|
198
|
+
Submits itself to the declared repository for saving
|
199
|
+
|
200
|
+
**#destroy** `entity.destroy => self`
|
201
|
+
|
202
|
+
Submits itself to the declared repository for removal
|
203
|
+
|
204
|
+
**#refresh** `entity.refresh => self`
|
205
|
+
|
206
|
+
Submits itself to the declared repository to be refreshed
|
207
|
+
|
208
|
+
## Contributing
|
209
|
+
|
210
|
+
1. Fork it ( https://github.com/[my-github-username]/errol/fork )
|
211
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
212
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
213
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
214
|
+
5. Create a new Pull Request
|
215
|
+
|
216
|
+
## Upcoming
|
217
|
+
1. separate repository save method to insert and replace
|
218
|
+
2. method missing bang methods call normal method then save
|
219
|
+
3. raise error if entity initialised with nil?
|
220
|
+
4. Protect record access method. Problem is access is require by repository to run sql filtering
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
require 'rake/testtask'
|
4
|
+
test_tasks = Dir['test/*/'].map { |d| File.basename(d) }
|
5
|
+
|
6
|
+
test_tasks.each do |folder|
|
7
|
+
Rake::TestTask.new("test:#{folder}") do |test|
|
8
|
+
test.pattern = "test/#{folder}/**/*_test.rb"
|
9
|
+
test.verbose = true
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
desc "Run application test suite"
|
14
|
+
Rake::TestTask.new("test") do |test|
|
15
|
+
test.pattern = "test/**/*_test.rb"
|
16
|
+
test.verbose = true
|
17
|
+
end
|
data/errol.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'errol/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "errol"
|
8
|
+
spec.version = Errol::VERSION
|
9
|
+
spec.authors = ["Peter Saxton"]
|
10
|
+
spec.email = ["peterhsaxton@gmail.com"]
|
11
|
+
spec.summary = %q{Repository to store and deliver encapsulated records.}
|
12
|
+
spec.description = %q{Based of the Sequel Library to deliver a repository interface to persisted data. Handles pagination by default}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "sequel", "~> 4.19"
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_development_dependency "minitest", "~> 5.4"
|
25
|
+
spec.add_development_dependency "minitest-reporters", "~> 1.0"
|
26
|
+
end
|
data/lib/errol.rb
ADDED
data/lib/errol/entity.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
module Errol
|
2
|
+
class Entity
|
3
|
+
RepositoryUndefined = Class.new(StandardError)
|
4
|
+
NilRecord = Class.new(StandardError)
|
5
|
+
def self.repository=(repository)
|
6
|
+
@repository = repository
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.repository
|
10
|
+
@repository || (raise RepositoryUndefined, "No repository set for #{self.name}")
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.entry_reader(*entries)
|
14
|
+
entries.each do |entry|
|
15
|
+
define_method entry do
|
16
|
+
record.public_send entry
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.entry_writer(*entries)
|
22
|
+
entries.each do |entry|
|
23
|
+
define_method "#{entry}=" do |value|
|
24
|
+
record.public_send "#{entry}=", value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.boolean_query(*entries)
|
30
|
+
entries.each do |entry|
|
31
|
+
define_method "#{entry}?" do
|
32
|
+
!!(record.public_send entry)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.entry_accessor(*entries)
|
38
|
+
entry_reader *entries
|
39
|
+
entry_writer *entries
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.boolean_accessor(*entries)
|
43
|
+
boolean_query *entries
|
44
|
+
entry_writer *entries
|
45
|
+
end
|
46
|
+
|
47
|
+
def initialize(record)
|
48
|
+
# raise NilRecord, "Tried to initialise #{self.class.name} with nil record" if record.nil?
|
49
|
+
@record = record
|
50
|
+
end
|
51
|
+
|
52
|
+
attr_reader :record
|
53
|
+
# protected :record
|
54
|
+
entry_reader :id
|
55
|
+
|
56
|
+
def set(**attributes)
|
57
|
+
attributes.each do |attribute, value|
|
58
|
+
self.public_send "#{attribute}=", value
|
59
|
+
end
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
def set!(*args)
|
64
|
+
set(*args)
|
65
|
+
save
|
66
|
+
end
|
67
|
+
|
68
|
+
def repository
|
69
|
+
self.class.repository
|
70
|
+
end
|
71
|
+
|
72
|
+
def save
|
73
|
+
repository.save self
|
74
|
+
self
|
75
|
+
end
|
76
|
+
|
77
|
+
def destroy
|
78
|
+
repository.remove self
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
def refresh
|
83
|
+
repository.refresh self
|
84
|
+
self
|
85
|
+
end
|
86
|
+
|
87
|
+
def ==(other)
|
88
|
+
other.class == self.class && other.record == record
|
89
|
+
end
|
90
|
+
alias_method :eql?, :==
|
91
|
+
# def method_missing(method_name, *args, &block)
|
92
|
+
# if method_name.to_s =~ /^(.+)!$/
|
93
|
+
# self.public_send($1, *args, &block)
|
94
|
+
# repository.save self
|
95
|
+
# else
|
96
|
+
# super
|
97
|
+
# end
|
98
|
+
# end
|
99
|
+
|
100
|
+
# def respond_to?(method_name)
|
101
|
+
# if method_name.to_s =~ /^(.+)!$/
|
102
|
+
# self.respond_to? $1
|
103
|
+
# else
|
104
|
+
# super
|
105
|
+
# end
|
106
|
+
# end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Errol
|
2
|
+
class Inquiry
|
3
|
+
DefaultValueUndefined = Class.new(StandardError)
|
4
|
+
|
5
|
+
def initialize(options={})
|
6
|
+
options.each do |key, value|
|
7
|
+
defaults[key.to_sym] = value
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def defaults
|
12
|
+
@defaults ||= self.class.defaults.clone
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.default(property, value)
|
16
|
+
defaults[property.to_sym] = value
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.defaults
|
20
|
+
@defaults ||= {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def method_missing(method_name, *args, &block)
|
24
|
+
if method_name.to_s =~ /^(.+)(?:\?)$/
|
25
|
+
!!self.public_send($1, *args, &block)
|
26
|
+
else
|
27
|
+
defaults.fetch(method_name) do |requirement|
|
28
|
+
raise DefaultValueUndefined.new "Inquiry requirement for \"#{requirement}\" has not been set"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|