dirt-core 2.2.3
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.
- data/Gemfile +8 -0
- data/Gemfile.lock +46 -0
- data/MIT-LICENSE +23 -0
- data/README +14 -0
- data/doc/BlockValidator.html +272 -0
- data/doc/ClosureValidator.html +276 -0
- data/doc/Dirt.html +158 -0
- data/doc/Dirt/FooBar.html +167 -0
- data/doc/Dirt/HerpZerp.html +161 -0
- data/doc/Dirt/HerpZerp/DerpGerp.html +161 -0
- data/doc/Dirt/HerpZerp/DerpGerp/FooBar.html +167 -0
- data/doc/Dirt/MemoryPersister.html +453 -0
- data/doc/Dirt/MemoryRecord.html +157 -0
- data/doc/Dirt/MissingRecordError.html +157 -0
- data/doc/Dirt/Model.html +320 -0
- data/doc/Dirt/NoPersisterError.html +157 -0
- data/doc/Dirt/Relation.html +338 -0
- data/doc/Dirt/Role.html +345 -0
- data/doc/Dirt/TooManyRecordsError.html +157 -0
- data/doc/Dirt/TransactionError.html +157 -0
- data/doc/ExistenceValidator.html +316 -0
- data/doc/FooBar.html +153 -0
- data/doc/Gemfile.html +131 -0
- data/doc/HerpZerp.html +147 -0
- data/doc/HerpZerp/DerpGerp.html +147 -0
- data/doc/HerpZerp/DerpGerp/FooBar.html +153 -0
- data/doc/Persister.html +311 -0
- data/doc/Persisting.html +413 -0
- data/doc/PresenceValidator.html +281 -0
- data/doc/README.html +132 -0
- data/doc/Role.html +309 -0
- data/doc/Scheduler.html +147 -0
- data/doc/SelfExistenceValidator.html +278 -0
- data/doc/TestPersister.html +411 -0
- data/doc/Validating.html +293 -0
- data/doc/created.rid +18 -0
- data/doc/images/add.png +0 -0
- data/doc/images/brick.png +0 -0
- data/doc/images/brick_link.png +0 -0
- data/doc/images/bug.png +0 -0
- data/doc/images/bullet_black.png +0 -0
- data/doc/images/bullet_toggle_minus.png +0 -0
- data/doc/images/bullet_toggle_plus.png +0 -0
- data/doc/images/date.png +0 -0
- data/doc/images/delete.png +0 -0
- data/doc/images/find.png +0 -0
- data/doc/images/loadingAnimation.gif +0 -0
- data/doc/images/macFFBgHack.png +0 -0
- data/doc/images/package.png +0 -0
- data/doc/images/page_green.png +0 -0
- data/doc/images/page_white_text.png +0 -0
- data/doc/images/page_white_width.png +0 -0
- data/doc/images/plugin.png +0 -0
- data/doc/images/ruby.png +0 -0
- data/doc/images/tag_blue.png +0 -0
- data/doc/images/tag_green.png +0 -0
- data/doc/images/transparent.png +0 -0
- data/doc/images/wrench.png +0 -0
- data/doc/images/wrench_orange.png +0 -0
- data/doc/images/zoom.png +0 -0
- data/doc/index.html +110 -0
- data/doc/js/darkfish.js +155 -0
- data/doc/js/jquery.js +18 -0
- data/doc/js/navigation.js +142 -0
- data/doc/js/search.js +94 -0
- data/doc/js/search_index.js +1 -0
- data/doc/js/searcher.js +228 -0
- data/doc/rdoc.css +543 -0
- data/doc/table_of_contents.html +203 -0
- data/lib/dirt/bdd/matchers.rb +10 -0
- data/lib/dirt/bdd/persister_spec_helper.rb +213 -0
- data/lib/dirt/bdd/shared_examples.rb +29 -0
- data/lib/dirt/contexts/context.rb +15 -0
- data/lib/dirt/core.rb +36 -0
- data/lib/dirt/errors/missing_record_error.rb +4 -0
- data/lib/dirt/errors/no_persister_error.rb +4 -0
- data/lib/dirt/errors/too_many_records_error.rb +4 -0
- data/lib/dirt/errors/transaction_error.rb +4 -0
- data/lib/dirt/models/model_behaviour.rb +47 -0
- data/lib/dirt/persisters/memory_persister.rb +86 -0
- data/lib/dirt/persisters/persister.rb +54 -0
- data/lib/dirt/persisters/relation.rb +38 -0
- data/lib/dirt/roles/persisting.rb +73 -0
- data/lib/dirt/roles/role.rb +32 -0
- data/lib/dirt/roles/validating.rb +27 -0
- data/lib/dirt/roles/validation/closure_validator.rb +15 -0
- data/lib/dirt/roles/validation/existence_validator.rb +53 -0
- data/lib/dirt/roles/validation/presence_validator.rb +21 -0
- data/lib/dirt/roles/validation/self_existence_validator.rb +18 -0
- data/spec/isolations/closure_validator_spec.rb +57 -0
- data/spec/isolations/context_spec.rb +54 -0
- data/spec/isolations/existence_validator_spec.rb +151 -0
- data/spec/isolations/memory_persister_spec.rb +175 -0
- data/spec/isolations/model_spec.rb +91 -0
- data/spec/isolations/persister_spec.rb +113 -0
- data/spec/isolations/persisting_spec.rb +266 -0
- data/spec/isolations/presence_validator_spec.rb +63 -0
- data/spec/isolations/role_spec.rb +116 -0
- data/spec/isolations/self_existence_validator_spec.rb +64 -0
- data/spec/isolations/spec_helper.rb +26 -0
- data/spec/isolations/validating_spec.rb +88 -0
- metadata +161 -0
data/lib/dirt/core.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2014 Tenjin Inc.
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
require 'dirt/persisters/persister'
|
25
|
+
require 'dirt/persisters/memory_persister'
|
26
|
+
|
27
|
+
require 'dirt/contexts/context'
|
28
|
+
|
29
|
+
require 'dirt/models/model_behaviour'
|
30
|
+
|
31
|
+
require 'dirt/roles/persisting'
|
32
|
+
require 'dirt/roles/validating'
|
33
|
+
require 'dirt/roles/validation/presence_validator'
|
34
|
+
require 'dirt/roles/validation/existence_validator'
|
35
|
+
require 'dirt/roles/validation/self_existence_validator'
|
36
|
+
require 'dirt/roles/validation/closure_validator'
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Dirt
|
2
|
+
# Common class for all model objects within the system.
|
3
|
+
module ModelBehaviour
|
4
|
+
def self.included(klass)
|
5
|
+
klass.extend ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
# Initializes a new instance of the class. The parameter hash takes whatever properties of the
|
10
|
+
# specific model type as keys.
|
11
|
+
def new(*args)
|
12
|
+
obj = nil
|
13
|
+
begin
|
14
|
+
obj = super
|
15
|
+
rescue ArgumentError
|
16
|
+
obj = super()
|
17
|
+
end
|
18
|
+
obj.update(args[0]) if args[0].is_a? Hash
|
19
|
+
obj
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Updates the properties of this instance. The parameter hash takes whatever properties of the
|
24
|
+
# specific model type as keys.
|
25
|
+
def update(params)
|
26
|
+
params.keys.each do |key|
|
27
|
+
self.send("#{key}=", params[key])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# The hash representation of any model object maps attribute names as symbols to their values, like the following:
|
32
|
+
# {attr1: val1, attr2: val2, ...}
|
33
|
+
def to_hash
|
34
|
+
instance_variables.each_with_object({}) do |var, hash|
|
35
|
+
hash[var[1..-1].to_sym] = instance_variable_get(var)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def ==(other)
|
40
|
+
return instance_variables.all? do |var|
|
41
|
+
instance_variable_get(var) == other.instance_variable_get(var)
|
42
|
+
end
|
43
|
+
|
44
|
+
false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Dirt
|
2
|
+
require 'dirt/errors/missing_record_error'
|
3
|
+
require 'ostruct'
|
4
|
+
|
5
|
+
# An in-memory implementation of persistence.
|
6
|
+
class MemoryPersister
|
7
|
+
def initialize(type, &block)
|
8
|
+
@type = type
|
9
|
+
@next_id = 0
|
10
|
+
@records = {}
|
11
|
+
@new_maker = block
|
12
|
+
end
|
13
|
+
|
14
|
+
def new(*args)
|
15
|
+
raise RuntimeError.new('Cannot create a new instance without a block given to init.') unless @new_maker
|
16
|
+
@new_maker.yield(*args)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Saves the record to the array either under the given id, or a new one if none is provided,
|
20
|
+
def save(data, id=nil)
|
21
|
+
assert_exists(id)
|
22
|
+
|
23
|
+
id ||= @next_id += 1
|
24
|
+
|
25
|
+
@records[id] = data
|
26
|
+
|
27
|
+
MemoryRecord.new(data.to_hash.merge(id: id))
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns the record with the given id.
|
31
|
+
def load(id)
|
32
|
+
assert_exists(id)
|
33
|
+
|
34
|
+
record = @records[id]
|
35
|
+
|
36
|
+
record ? MemoryRecord.new(record.to_hash.merge(id: id)) : nil
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns the list of all records.
|
40
|
+
def all
|
41
|
+
@records.collect do |id, r|
|
42
|
+
MemoryRecord.new(r.to_hash.merge(id: id))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Removes the record with the given id.
|
47
|
+
def delete(id)
|
48
|
+
if @records.has_key?(id)
|
49
|
+
MemoryRecord.new(@records.delete(id).to_hash.merge(id: id))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# determines whether a record exists with the given id
|
54
|
+
def exists?(id)
|
55
|
+
@records[id] != nil
|
56
|
+
end
|
57
|
+
|
58
|
+
def find(params)
|
59
|
+
where(params).first
|
60
|
+
end
|
61
|
+
|
62
|
+
def where(params)
|
63
|
+
matches = @records.select do |id, record|
|
64
|
+
same_id = (params.delete(:id) || id) == id
|
65
|
+
|
66
|
+
params.all? { |attr, val| record.send(attr) == val } && same_id
|
67
|
+
end.collect do |id, record|
|
68
|
+
MemoryRecord.new(record.to_hash.merge(id: id))
|
69
|
+
end
|
70
|
+
|
71
|
+
Relation.new(matches)
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
def assert_exists(id)
|
76
|
+
raise MissingRecordError.new("That #{@type} (id: #{id}) does not exist.") if id && !@records[id]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class MemoryRecord < OpenStruct
|
81
|
+
alias_method :to_hash, :marshal_dump
|
82
|
+
end
|
83
|
+
|
84
|
+
# This must be at the bottom to work around the circular dependency with Relation.
|
85
|
+
require 'dirt/persisters/relation'
|
86
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'active_support/all'
|
2
|
+
require 'dirt/errors/transaction_error'
|
3
|
+
require 'dirt/errors/no_persister_error'
|
4
|
+
|
5
|
+
# This Persister registry is the source for all persisters in the system. Register persisters here with
|
6
|
+
# #for.
|
7
|
+
#
|
8
|
+
# == Persisters
|
9
|
+
# All persisters must respond to the following:
|
10
|
+
#
|
11
|
+
# * +all+ Returns all records.
|
12
|
+
# * +find(id)+ Returns the record with the given id.
|
13
|
+
# * +exists?(id)+ Determines whether a record exists with the given id
|
14
|
+
# * +save(data)+ Saves a new record.
|
15
|
+
# * +save(data, id)+ Saves a record at the given id.
|
16
|
+
# * +transaction(&block)+ Rolls back changes brought about during :yeild, and re-raises a TransactionError iff that error is raised in the yeild.
|
17
|
+
# * delete(id) Deletes the record with the given id.
|
18
|
+
class Persister
|
19
|
+
# If persister is supplied, then this sets the persister for the given class,
|
20
|
+
# otherwise it returns the previously set persister for that class.
|
21
|
+
# If the first argument is a class, it is converted to a symbol.
|
22
|
+
def self.for(klass, persister=nil)
|
23
|
+
if klass.is_a? Class
|
24
|
+
klass = klass.to_s.demodulize.underscore.to_sym
|
25
|
+
end
|
26
|
+
|
27
|
+
@@persisters ||= {}
|
28
|
+
@@persisters[klass] ||= persister if persister
|
29
|
+
|
30
|
+
@@persisters[klass] || raise(Dirt::NoPersisterError.new("There is no persister for \"#{klass.to_s.pluralize}\"."))
|
31
|
+
end
|
32
|
+
|
33
|
+
# Forgets about all previously saved persisters.
|
34
|
+
def self.clear
|
35
|
+
@@persisters = nil
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
def self.transaction(persister_list=[], &block)
|
40
|
+
begin
|
41
|
+
if persister_list.size < 1
|
42
|
+
yeild
|
43
|
+
elsif persister_list.size == 1
|
44
|
+
persister_list.first.transaction &block
|
45
|
+
else
|
46
|
+
self.transaction(persister_list[1..(persister_list.size-1)]) do
|
47
|
+
persister_list.first.transaction &block
|
48
|
+
end
|
49
|
+
end
|
50
|
+
rescue Dirt::TransactionError => e
|
51
|
+
{errors: e.message.split("\n")}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Dirt
|
2
|
+
require 'dirt/persisters/memory_persister'
|
3
|
+
|
4
|
+
class Relation < MemoryPersister
|
5
|
+
@records = []
|
6
|
+
|
7
|
+
def initialize(records)
|
8
|
+
@records = records
|
9
|
+
end
|
10
|
+
|
11
|
+
def first
|
12
|
+
@records.first
|
13
|
+
|
14
|
+
#match = @records.first
|
15
|
+
#match ? match : nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def collect(&block)
|
19
|
+
@records.collect(&block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def each(&block)
|
23
|
+
@records.each(&block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def empty?
|
27
|
+
@records.empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
def all?(&block)
|
31
|
+
every = true
|
32
|
+
|
33
|
+
@records.each do |record|
|
34
|
+
every &= yield record
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'dirt/roles/role'
|
2
|
+
require 'dirt/persisters/persister'
|
3
|
+
|
4
|
+
# Role for adding persistability to model objects.
|
5
|
+
class Persisting < Dirt::Role
|
6
|
+
attr_reader :id
|
7
|
+
|
8
|
+
def initialize(decorated, id=nil)
|
9
|
+
super(decorated)
|
10
|
+
@id = id
|
11
|
+
end
|
12
|
+
|
13
|
+
# Saves the decorated object with the appropriate persister.
|
14
|
+
def save()
|
15
|
+
saved = persister.save(@decorated, @id)
|
16
|
+
|
17
|
+
if saved
|
18
|
+
@id = saved.id
|
19
|
+
self
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Loads persisted data into the decorated object
|
24
|
+
def load(id)
|
25
|
+
assert_exists(id)
|
26
|
+
|
27
|
+
loaded = persister.load(id)
|
28
|
+
|
29
|
+
chameleonize(loaded)
|
30
|
+
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
# Loads from the first record that matches the given attributes.
|
35
|
+
def load_by(attrs)
|
36
|
+
record = Persister.for(@decorated.class).find(attrs)
|
37
|
+
|
38
|
+
unless record
|
39
|
+
raise Dirt::MissingRecordError.new("No record matches #{attrs.collect { |pair| pair.join(' == ') }.join(', ')}.")
|
40
|
+
end
|
41
|
+
|
42
|
+
chameleonize(record)
|
43
|
+
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
# Removes the decorated object from the appropriate persister.
|
48
|
+
def delete(id)
|
49
|
+
assert_exists(id)
|
50
|
+
|
51
|
+
persister.delete(id)
|
52
|
+
end
|
53
|
+
|
54
|
+
def ==(other)
|
55
|
+
super && other.id == @id
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
def chameleonize(record)
|
60
|
+
@id = record.id
|
61
|
+
@decorated.update(record.to_hash.except(:id))
|
62
|
+
end
|
63
|
+
|
64
|
+
def persister
|
65
|
+
Persister.for(@decorated.class)
|
66
|
+
end
|
67
|
+
|
68
|
+
def assert_exists(id)
|
69
|
+
unless persister.exists?(id)
|
70
|
+
raise Dirt::MissingRecordError.new("That #{@decorated.class.to_s.demodulize.downcase} (id: #{id || 'nil'}) does not exist.")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Dirt
|
2
|
+
# Roles are part of the DCI system design paradigm. They decorate a model object to extend its
|
3
|
+
# behaviour for the context of a single interaction.
|
4
|
+
class Role
|
5
|
+
# Takes the decorated object.
|
6
|
+
def initialize(decorated)
|
7
|
+
@decorated = decorated
|
8
|
+
end
|
9
|
+
|
10
|
+
# Attempts to run the missing method on the decorated object before exploding as normal,
|
11
|
+
# with a light tingling sensation.
|
12
|
+
def method_missing(method, *args, &block)
|
13
|
+
# if @decorated.respond_to?(method)
|
14
|
+
@decorated.send(method, *args, &block)
|
15
|
+
# else
|
16
|
+
# super
|
17
|
+
# end
|
18
|
+
end
|
19
|
+
|
20
|
+
def class
|
21
|
+
@decorated.class
|
22
|
+
end
|
23
|
+
|
24
|
+
def respond_to?(method, privates = false)
|
25
|
+
super || @decorated.respond_to?(method, privates)
|
26
|
+
end
|
27
|
+
|
28
|
+
def ==(other)
|
29
|
+
other && (other.instance_variable_get(:@decorated) == @decorated || other == @decorated)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# A Role for testing whether the wrapped object is valid, using the list of validators provided in the constructor,
|
2
|
+
#
|
3
|
+
# == Validators
|
4
|
+
# Each validator must respond to
|
5
|
+
# * +valid?+ to return whether that validator passes, and
|
6
|
+
# * +error_message+ to return any errors found when +valid?+ is +false+.
|
7
|
+
class Validating < Dirt::Role
|
8
|
+
# Takes the decorated object and then the list of validators. See Role for more on decoration.
|
9
|
+
def initialize(decorated, validators)
|
10
|
+
super(decorated)
|
11
|
+
@validators = validators
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns +true+ _iff_ every validator passes.
|
15
|
+
def valid?
|
16
|
+
@validators.all? do |v|
|
17
|
+
v.valid?(@decorated)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns a list of the string messages returned from the validators that have failed.
|
22
|
+
def errors
|
23
|
+
@validators.collect do |v|
|
24
|
+
v.error_message(@decorated) unless v.valid?(@decorated)
|
25
|
+
end.reject { |m| m.nil? }
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Takes a block, and uses it to determine validity,
|
2
|
+
class ClosureValidator
|
3
|
+
def initialize(block, message_block)
|
4
|
+
@block = block
|
5
|
+
@message_block = message_block
|
6
|
+
end
|
7
|
+
|
8
|
+
def valid?(validated)
|
9
|
+
@block.call(validated)
|
10
|
+
end
|
11
|
+
|
12
|
+
def error_message(validated)
|
13
|
+
@message_block.call(validated)
|
14
|
+
end
|
15
|
+
end
|