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.
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
+
@@ -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