dirt-core 2.2.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|