pod4 0.6.2
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 +7 -0
- data/.hgignore +18 -0
- data/.hgtags +19 -0
- data/.rspec +4 -0
- data/.ruby-version +1 -0
- data/Gemfile +3 -0
- data/LICENSE.md +21 -0
- data/README.md +556 -0
- data/Rakefile +30 -0
- data/lib/pod4/alert.rb +87 -0
- data/lib/pod4/basic_model.rb +137 -0
- data/lib/pod4/errors.rb +80 -0
- data/lib/pod4/interface.rb +110 -0
- data/lib/pod4/metaxing.rb +66 -0
- data/lib/pod4/model.rb +347 -0
- data/lib/pod4/nebulous_interface.rb +408 -0
- data/lib/pod4/null_interface.rb +148 -0
- data/lib/pod4/param.rb +29 -0
- data/lib/pod4/pg_interface.rb +460 -0
- data/lib/pod4/sequel_interface.rb +303 -0
- data/lib/pod4/tds_interface.rb +394 -0
- data/lib/pod4/version.rb +3 -0
- data/lib/pod4.rb +54 -0
- data/md/fixme.md +32 -0
- data/md/roadmap.md +69 -0
- data/pod4.gemspec +49 -0
- data/spec/README.md +19 -0
- data/spec/alert_spec.rb +173 -0
- data/spec/basic_model_spec.rb +220 -0
- data/spec/doc_no_pending.rb +5 -0
- data/spec/fixtures/database.rb +13 -0
- data/spec/model_spec.rb +760 -0
- data/spec/nebulous_interface_spec.rb +286 -0
- data/spec/null_interface_spec.rb +153 -0
- data/spec/param_spec.rb +89 -0
- data/spec/pg_interface_spec.rb +452 -0
- data/spec/pod4_spec.rb +88 -0
- data/spec/sequel_interface_spec.rb +466 -0
- data/spec/shared_examples_for_interface.rb +160 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/tds_interface_spec.rb +494 -0
- data/tags +106 -0
- metadata +316 -0
data/lib/pod4/alert.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
module Pod4
|
2
|
+
|
3
|
+
|
4
|
+
##
|
5
|
+
# An Alert is an error, warning or note which might be raised in validation
|
6
|
+
# in the model. They are, however, designed to follow all the way through the
|
7
|
+
# controller to the view; you should use them whenever you want to display a
|
8
|
+
# message on the page.
|
9
|
+
#
|
10
|
+
class Alert
|
11
|
+
|
12
|
+
# Valid values for @type: :error, :warning, :info or :success
|
13
|
+
ALERTTYPES = [:error, :warning, :info, :success]
|
14
|
+
|
15
|
+
# The alert type
|
16
|
+
attr_reader :type
|
17
|
+
|
18
|
+
# The exception attached to the alert, or nil if there isn't one
|
19
|
+
attr_reader :exception
|
20
|
+
|
21
|
+
# The field name associated with the alert, or nil
|
22
|
+
attr_accessor :field
|
23
|
+
|
24
|
+
# The alert message
|
25
|
+
attr_accessor :message
|
26
|
+
|
27
|
+
|
28
|
+
##
|
29
|
+
# A new alert must have a type (error warning info or success); there
|
30
|
+
# should be a message to display, obviously. Note that you can pass an
|
31
|
+
# exception in place of a message, in which case @exception will be set.
|
32
|
+
#
|
33
|
+
# You may optionally specify the name of the field to be highlighted.
|
34
|
+
# Models will give validation alerts a field that corresponds to the model
|
35
|
+
# attribute; but this is not enforced here, and your controller will have
|
36
|
+
# to sort things out if the model is expecting different field names.
|
37
|
+
#
|
38
|
+
def initialize(type, field=nil, message)
|
39
|
+
raise ArgumentError, "unknown alert type" \
|
40
|
+
unless ALERTTYPES.include? type.to_s.to_sym
|
41
|
+
|
42
|
+
@type = type.to_s.to_sym
|
43
|
+
@field = field ? field.to_sym : nil
|
44
|
+
@exception = nil
|
45
|
+
|
46
|
+
if message.kind_of?(Exception)
|
47
|
+
@exception = message.dup
|
48
|
+
@message = @exception.message
|
49
|
+
|
50
|
+
# SwingShift validation exceptions hold the field name
|
51
|
+
@field ||= @exception.field if @exception.respond_to?(:field)
|
52
|
+
|
53
|
+
else
|
54
|
+
@message = message
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
##
|
61
|
+
# An array of Alert is automatically sorted into descending order of
|
62
|
+
# seriousness
|
63
|
+
#
|
64
|
+
def <=>(other)
|
65
|
+
ALERTTYPES.index(self.type) <=> ALERTTYPES.index(other.type)
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
##
|
70
|
+
# Write self to the log
|
71
|
+
#
|
72
|
+
def log(file='')
|
73
|
+
case self.type
|
74
|
+
when :error then Pod4.logger.error(file) { self.message }
|
75
|
+
when :warning then Pod4.logger.warn(file) { self.message }
|
76
|
+
else Pod4.logger.info(file) { self.message }
|
77
|
+
end
|
78
|
+
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
end
|
84
|
+
##
|
85
|
+
|
86
|
+
|
87
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'octothorpe'
|
2
|
+
|
3
|
+
require_relative 'metaxing'
|
4
|
+
require_relative 'errors'
|
5
|
+
require_relative 'alert'
|
6
|
+
|
7
|
+
|
8
|
+
module Pod4
|
9
|
+
|
10
|
+
|
11
|
+
##
|
12
|
+
# The ultimate parent of all models. It has an interface, an id, a status,
|
13
|
+
# and alerts. That's pretty much it.
|
14
|
+
#
|
15
|
+
# This is useful to the user for weirder models -- for example, where the
|
16
|
+
# datasource records and the model instances don't map one-to-one.
|
17
|
+
#
|
18
|
+
# See Pod4::Model for documentation about Models.
|
19
|
+
#
|
20
|
+
class BasicModel
|
21
|
+
extend Metaxing
|
22
|
+
|
23
|
+
|
24
|
+
# The value of the ID field on the record
|
25
|
+
attr_reader :model_id
|
26
|
+
|
27
|
+
# one of Model::STATII
|
28
|
+
attr_reader :model_status
|
29
|
+
|
30
|
+
# Valid values for @model_status: :error :warning :okay :deleted or :empty
|
31
|
+
STATII = %i|error warning okay deleted empty|
|
32
|
+
|
33
|
+
|
34
|
+
class << self
|
35
|
+
|
36
|
+
##
|
37
|
+
# You MUST call this in your model definition to give it an instance of an
|
38
|
+
# interface.
|
39
|
+
#
|
40
|
+
def set_interface(interface)
|
41
|
+
define_class_method(:interface) {interface}
|
42
|
+
end
|
43
|
+
|
44
|
+
def interface
|
45
|
+
raise NotImplemented, "no call to set_interface in the model"
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
##
|
50
|
+
|
51
|
+
|
52
|
+
##
|
53
|
+
# Initialize a model by passing it a unique id value.
|
54
|
+
# Override this to set initial values for your column attributes.
|
55
|
+
#
|
56
|
+
def initialize(id=nil)
|
57
|
+
@model_status = :empty
|
58
|
+
@model_id = id
|
59
|
+
@alerts = []
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
##
|
64
|
+
# Syntactic sugar; same as self.class.interface, which returns the
|
65
|
+
# interface instance.
|
66
|
+
#
|
67
|
+
def interface; self.class.interface; end
|
68
|
+
|
69
|
+
|
70
|
+
##
|
71
|
+
# Return the list of alerts.
|
72
|
+
#
|
73
|
+
# We don't use attr_reader for this because it won't protect an array from
|
74
|
+
# external changes.
|
75
|
+
#
|
76
|
+
def alerts; @alerts.dup; end
|
77
|
+
|
78
|
+
|
79
|
+
##
|
80
|
+
# Clear down the alerts.
|
81
|
+
#
|
82
|
+
# Note that set model_status to :okay. Theoretically it might need to be
|
83
|
+
# :empty or :deleted, but if you are calling clear_alerts before a call to
|
84
|
+
# `read` or after a call to `delete`, then you have more problems than I
|
85
|
+
# can solve.
|
86
|
+
#
|
87
|
+
def clear_alerts
|
88
|
+
@alerts = []
|
89
|
+
@model_status = :okay
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
##
|
94
|
+
# Raise a SwingShift exception for the model if any alerts are status
|
95
|
+
# :error; otherwise do nothing.
|
96
|
+
#
|
97
|
+
# Note the alias of or_die for this method, which means that if you have
|
98
|
+
# kept to the idiom of CRUD methods returning self, then you can steal a
|
99
|
+
# lick from Perl and say:
|
100
|
+
# MyModel.new(14).read.or_die
|
101
|
+
#
|
102
|
+
def raise_exceptions
|
103
|
+
al = @alerts.sort.first
|
104
|
+
raise ValidationError.from_alert(al) if al && al.type == :error
|
105
|
+
self
|
106
|
+
end
|
107
|
+
|
108
|
+
alias :or_die :raise_exceptions
|
109
|
+
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
|
114
|
+
##
|
115
|
+
# Add a Pod4::Alert to the model instance @alerts attribute
|
116
|
+
#
|
117
|
+
# Call this from your validation method.
|
118
|
+
#
|
119
|
+
def add_alert(type, field=nil, message)
|
120
|
+
return if @alerts.any? do |a|
|
121
|
+
a.type == type && a.field == field && a.message = message
|
122
|
+
end
|
123
|
+
|
124
|
+
lert = Alert.new(type, field, message).log(caller.first.split(':').first)
|
125
|
+
@alerts << lert
|
126
|
+
|
127
|
+
st = @alerts.sort.first.type
|
128
|
+
@model_status = st if %i|error warning|.include?(st)
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
end
|
133
|
+
##
|
134
|
+
|
135
|
+
|
136
|
+
end
|
137
|
+
|
data/lib/pod4/errors.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
module Pod4
|
2
|
+
|
3
|
+
|
4
|
+
##
|
5
|
+
# Raised in abstract methods when treated as concrete
|
6
|
+
#
|
7
|
+
class NotImplemented < Exception
|
8
|
+
|
9
|
+
def initialize(msg=nil)
|
10
|
+
super(msg || $! && $!.message)
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
##
|
17
|
+
# Base error class for Swingshift
|
18
|
+
#
|
19
|
+
# Also used for any configuration errors where ArgumentError is not
|
20
|
+
# appropriate.
|
21
|
+
#
|
22
|
+
class Pod4Error < StandardError
|
23
|
+
|
24
|
+
def initialize(msg=nil)
|
25
|
+
super(msg || $! && $!.message)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
##
|
30
|
+
|
31
|
+
|
32
|
+
##
|
33
|
+
# Raised if something goes wrong on the database
|
34
|
+
#
|
35
|
+
class DatabaseError < Pod4Error
|
36
|
+
|
37
|
+
def initialize(msg=nil)
|
38
|
+
super(msg || $! && $!.message)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
##
|
43
|
+
|
44
|
+
|
45
|
+
##
|
46
|
+
# Raised if a Pod4 method runs into problems
|
47
|
+
#
|
48
|
+
# Note, invalid parameters get a Ruby ArgumentError. This is for, eg, an
|
49
|
+
# interface finding that the ID it was given to read does not exist.
|
50
|
+
#
|
51
|
+
class CantContinue < Pod4Error
|
52
|
+
|
53
|
+
def initialize(msg=nil)
|
54
|
+
super(msg || $! && $!.message)
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
##
|
59
|
+
|
60
|
+
|
61
|
+
##
|
62
|
+
# Raised if validation fails (and you wanted an exception...)
|
63
|
+
#
|
64
|
+
class ValidationError < Pod4Error
|
65
|
+
attr_reader :field
|
66
|
+
|
67
|
+
def self.from_alert(alert)
|
68
|
+
self.new(alert.message, alert.field)
|
69
|
+
end
|
70
|
+
|
71
|
+
def initialize(message=nil, field=nil)
|
72
|
+
super(message || $! && $!.message)
|
73
|
+
@field = field.to_s.to_sym
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
end
|
80
|
+
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require_relative 'metaxing'
|
2
|
+
require_relative 'errors'
|
3
|
+
|
4
|
+
|
5
|
+
module Pod4
|
6
|
+
|
7
|
+
|
8
|
+
##
|
9
|
+
# Abstract class, The parent of all interfaces.
|
10
|
+
#
|
11
|
+
# An interface encapsulates whatever method we are using up connect to the
|
12
|
+
# data. Its state is therefore that of the connection, not the DB table or
|
13
|
+
# whatever entity that the data source uses to group data. It raises only
|
14
|
+
# SwingShift errors (wrapping the error it gets inside a SwingShift error).
|
15
|
+
#
|
16
|
+
# We would expect a child of Interface for each data access type
|
17
|
+
# (sequelInterface, NebulousInterface, etc). These children *will not change*
|
18
|
+
# the signatures of the methods below.
|
19
|
+
#
|
20
|
+
# The methods below are the required ones. Interfaces will likely implement
|
21
|
+
# other, interface-specific, ways of accessing data.
|
22
|
+
#
|
23
|
+
# In Normal use, the interface classes will in turn be subclassed as inner
|
24
|
+
# classes within each model, in order to customise them for the specific
|
25
|
+
# entity that they are drawing data from.
|
26
|
+
#
|
27
|
+
# Note that your Interface subclass probably returns an Octothorpe rather
|
28
|
+
# than a Hash, q.v.. (But you should be able to treat the former as if it
|
29
|
+
# were the latter in most cases.)
|
30
|
+
#
|
31
|
+
class Interface
|
32
|
+
extend Metaxing
|
33
|
+
|
34
|
+
|
35
|
+
ACTIONS = [ :list, :create, :read, :update, :delete ]
|
36
|
+
|
37
|
+
##
|
38
|
+
# A field name in the data source, the name of the unique ID field.
|
39
|
+
#
|
40
|
+
def id_fld
|
41
|
+
raise NotImplemented, "Interface needs to define an 'id_fld' method"
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
##
|
46
|
+
# Individual implementations are likely to have very different initialize
|
47
|
+
# methods, which will accept whatever SwingShift object is needed to
|
48
|
+
# contact the data store, eg. the Sequel DB object.
|
49
|
+
#
|
50
|
+
def initialize
|
51
|
+
raise NotImplemented, "Interface needs to define an 'initialize' method"
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
##
|
56
|
+
# List accepts a parameter as selection criteria, and returns an array of
|
57
|
+
# Octothorpes. Exactly what the selection criteria look like will vary from
|
58
|
+
# interface to interface. So will the contents of the return OT, although
|
59
|
+
# it must include the ID field. (Ideally each element of the return array
|
60
|
+
# should follow the same format as the return value for read(). )
|
61
|
+
#
|
62
|
+
# Note that list should ALWAYS return an array; never nil.
|
63
|
+
#
|
64
|
+
def list(selection=nil)
|
65
|
+
raise NotImplemented, "Interface needs to define 'list' method"
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
##
|
70
|
+
# Create accepts a record parameter (Hash or OT, but again, the format of
|
71
|
+
# this will vary) representing a record, and creates the record. Should
|
72
|
+
# return the ID for the new record.
|
73
|
+
#
|
74
|
+
def create(record)
|
75
|
+
raise NotImplemented, "Interface needs to define 'create' method"
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
##
|
80
|
+
# Read accepts an ID, and returns an Octothorpe representing the unique
|
81
|
+
# record for that ID. If there is no record matching the ID then it returns
|
82
|
+
# an empty Octothorpe.
|
83
|
+
#
|
84
|
+
def read(id)
|
85
|
+
raise NotImplemented, "Interface needs to define 'read' method"
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
##
|
90
|
+
# Update accepts an ID and a record parameter. It updates the record on the
|
91
|
+
# data source that matches the ID using the record parameter. It returns
|
92
|
+
# self.
|
93
|
+
#
|
94
|
+
def update(id, record)
|
95
|
+
raise NotImplemented, "Interface needs to define 'update' method"
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
##
|
100
|
+
# delete removes the record with the given ID. returns self.
|
101
|
+
#
|
102
|
+
def delete(id)
|
103
|
+
raise NotImplemented, "Interface needs to define 'delete' method"
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Pod4
|
2
|
+
|
3
|
+
|
4
|
+
##
|
5
|
+
# A little mixin for metaprogramming
|
6
|
+
#
|
7
|
+
module Metaxing
|
8
|
+
|
9
|
+
##
|
10
|
+
# Return the metaclass (eigenclass) of self.
|
11
|
+
#
|
12
|
+
def metaclass
|
13
|
+
class << self; self; end
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
##
|
18
|
+
# Define (or re-define) a class method.
|
19
|
+
#
|
20
|
+
# Example:
|
21
|
+
#
|
22
|
+
# class Foo
|
23
|
+
# extend Metaxing
|
24
|
+
#
|
25
|
+
# class << self
|
26
|
+
# def set_bar(x); define_class_method(:bar) {x}; end
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# class MyFoo < Foo; end
|
31
|
+
#
|
32
|
+
# Foo.set_bar(23)
|
33
|
+
# puts Foo.bar # -> 23
|
34
|
+
# puts MyFoo.bar # -> 23
|
35
|
+
#
|
36
|
+
# MyFoo.set_bar(42)
|
37
|
+
# puts Foo.bar # -> 23
|
38
|
+
# puts MyFoo.bar # -> 42
|
39
|
+
#
|
40
|
+
# This example gives us something different from a class attribute @@bar --
|
41
|
+
# the value of which would be shared between Foo and MyFoo. And different
|
42
|
+
# again from an attribute @bar on class Foo, which wouldn't turn up in
|
43
|
+
# MyFoo at all. This is a value that has inheritance.
|
44
|
+
#
|
45
|
+
# And this example shows pretty much the only metaprogramming trick you
|
46
|
+
# will find me pulling. It's enough to do a hell of a lot.
|
47
|
+
#
|
48
|
+
# Note that you need to be very careful what parameters you pass in order
|
49
|
+
# to preserve this inheritance: if you pass a reference to something on
|
50
|
+
# Foo, you will be sharing it with MyFoo, not just inheriting it. Best to
|
51
|
+
# use local variables or dups.
|
52
|
+
#
|
53
|
+
# ...Well, actually, you aren't getting a method on the class -- these are
|
54
|
+
# defined in the class' immediate ancestor, eg, Object. You're getting a
|
55
|
+
# method on the eigenclass, which Ruby inserts between the class' ancestor
|
56
|
+
# and the class. For all of me I can't see a practical difference when it
|
57
|
+
# comes to defining class methods.
|
58
|
+
#
|
59
|
+
def define_class_method(method, *args, &blk)
|
60
|
+
metaclass.send(:define_method, method, *args, &blk)
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
end
|