amee-data-abstraction 1.0.0

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.
Files changed (53) hide show
  1. data/.rvmrc +1 -0
  2. data/CHANGELOG.txt +4 -0
  3. data/Gemfile +16 -0
  4. data/Gemfile.lock +41 -0
  5. data/LICENSE.txt +27 -0
  6. data/README.txt +188 -0
  7. data/Rakefile +102 -0
  8. data/VERSION +1 -0
  9. data/amee-data-abstraction.gemspec +115 -0
  10. data/examples/_calculator_form.erb +27 -0
  11. data/examples/calculation_controller.rb +16 -0
  12. data/init.rb +4 -0
  13. data/lib/amee-data-abstraction.rb +30 -0
  14. data/lib/amee-data-abstraction/calculation.rb +236 -0
  15. data/lib/amee-data-abstraction/calculation_set.rb +101 -0
  16. data/lib/amee-data-abstraction/drill.rb +63 -0
  17. data/lib/amee-data-abstraction/exceptions.rb +47 -0
  18. data/lib/amee-data-abstraction/input.rb +197 -0
  19. data/lib/amee-data-abstraction/metadatum.rb +58 -0
  20. data/lib/amee-data-abstraction/ongoing_calculation.rb +545 -0
  21. data/lib/amee-data-abstraction/output.rb +16 -0
  22. data/lib/amee-data-abstraction/profile.rb +108 -0
  23. data/lib/amee-data-abstraction/prototype_calculation.rb +350 -0
  24. data/lib/amee-data-abstraction/term.rb +506 -0
  25. data/lib/amee-data-abstraction/terms_list.rb +150 -0
  26. data/lib/amee-data-abstraction/usage.rb +90 -0
  27. data/lib/config/amee_units.rb +129 -0
  28. data/lib/core-extensions/class.rb +27 -0
  29. data/lib/core-extensions/hash.rb +43 -0
  30. data/lib/core-extensions/ordered_hash.rb +21 -0
  31. data/lib/core-extensions/proc.rb +15 -0
  32. data/rails/init.rb +32 -0
  33. data/spec/amee-data-abstraction/calculation_set_spec.rb +54 -0
  34. data/spec/amee-data-abstraction/calculation_spec.rb +75 -0
  35. data/spec/amee-data-abstraction/drill_spec.rb +38 -0
  36. data/spec/amee-data-abstraction/input_spec.rb +77 -0
  37. data/spec/amee-data-abstraction/metadatum_spec.rb +17 -0
  38. data/spec/amee-data-abstraction/ongoing_calculation_spec.rb +494 -0
  39. data/spec/amee-data-abstraction/profile_spec.rb +39 -0
  40. data/spec/amee-data-abstraction/prototype_calculation_spec.rb +256 -0
  41. data/spec/amee-data-abstraction/term_spec.rb +385 -0
  42. data/spec/amee-data-abstraction/terms_list_spec.rb +53 -0
  43. data/spec/config/amee_units_spec.rb +71 -0
  44. data/spec/core-extensions/class_spec.rb +25 -0
  45. data/spec/core-extensions/hash_spec.rb +44 -0
  46. data/spec/core-extensions/ordered_hash_spec.rb +12 -0
  47. data/spec/core-extensions/proc_spec.rb +12 -0
  48. data/spec/fixtures/electricity.rb +35 -0
  49. data/spec/fixtures/electricity_and_transport.rb +55 -0
  50. data/spec/fixtures/transport.rb +26 -0
  51. data/spec/spec.opts +2 -0
  52. data/spec/spec_helper.rb +244 -0
  53. metadata +262 -0
@@ -0,0 +1,47 @@
1
+ # Copyright (C) 2011 AMEE UK Ltd. - http://www.amee.com
2
+ # Released as Open Source Software under the BSD 3-Clause license. See LICENSE.txt for details.
3
+
4
+ # :title: Module: AMEE::DataAbstraction::Exceptions
5
+
6
+ module AMEE
7
+ module DataAbstraction
8
+ module Exceptions
9
+ # Throw this exception when there is a general syntax error in a DSL block.
10
+ class DSL < Exception; end
11
+
12
+ # Throw this exception when user specifies a suggested UI for a term which
13
+ # is not supported.
14
+ class InvalidInterface < Exception ; end
15
+
16
+ # Throw this exception when user tries to set a value for a term with a
17
+ #read-only value.
18
+ class FixedValueInterference < Exception; end
19
+
20
+ # Throw this exception when someone tries to access a term which is not
21
+ # defined for a calculation.
22
+ class NoSuchTerm < Exception; end
23
+
24
+ # Throw this exception when trying to create a PI for a calculation which
25
+ # already has a corresponding PI.
26
+ class AlreadyHaveProfileItem < Exception; end
27
+
28
+ # Throw this exception when a locally stored calculation and the information
29
+ # on the AMEE server have got out of sync.
30
+ class Syncronization < Exception; end
31
+
32
+ # Throw this exception if something went wrong making a profile item.
33
+ class DidNotCreateProfileItem < Exception; end
34
+
35
+ # Throw this exception when someone tries to specify a "usage" twice when
36
+ # defining a PrototypeCalculation using the DSL.
37
+ class TwoUsages < Exception; end
38
+
39
+ # Throw this exception when an invalid value is set for a term
40
+ class ChoiceValidation < Exception; end
41
+
42
+ # Throw this exception when inappropriate units are set for a term.
43
+ class InvalidUnits < Exception; end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,197 @@
1
+ # Copyright (C) 2011 AMEE UK Ltd. - http://www.amee.com
2
+ # Released as Open Source Software under the BSD 3-Clause license. See LICENSE.txt for details.
3
+
4
+ # :title: Class: AMEE::DataAbstraction::Input
5
+
6
+ module AMEE
7
+ module DataAbstraction
8
+
9
+ # Subclass of <tt>Term</tt> providing methods and attributes appropriate for
10
+ # representing calculation inputs specifically
11
+ #
12
+ class Input < Term
13
+
14
+ # Returns the valid choices for this input
15
+ # (Abstract, implemented only for subclasses of input.)
16
+ def choices
17
+ raise NotImplementedError
18
+ end
19
+
20
+ # Returns an ppropriate data structure for a rails select list form helper.
21
+ def options_for_select
22
+ [[nil,nil]]+choices.map{|x|[x.underscore.humanize,x] unless x.nil? }.compact
23
+ end
24
+
25
+ # Initialization of <i>Input</i> objects follows that of the parent
26
+ # <i>Term</i> class.
27
+ #
28
+ def initialize(options={},&block)
29
+ @validation = nil
30
+ validation_message {"#{name} is invalid."}
31
+ super
32
+ end
33
+
34
+ # Configures the value of <tt>self</tt> to be fixed to <tt>val</tt>, i.e.
35
+ # the value is read-only.
36
+ #
37
+ def fixed val
38
+ value(val)
39
+ @fixed=true
40
+ @optional=false
41
+ end
42
+
43
+ # Block to define custom complaint message for an invalid value.
44
+ def validation_message(&block)
45
+ @validation_block=block
46
+ end
47
+
48
+ # Set a default validation message appropriate for input terms which have
49
+ # a list of choices.
50
+ def choice_validation_message
51
+ validation_message {"#{name} is invalid because #{value} is not one of #{choices.join(', ')}."}
52
+ end
53
+
54
+ # Represents the value of <tt>self</tt>. Set a value by passing an argument.
55
+ # Retrieve a value by calling without an argument, e.g.,
56
+ #
57
+ # my_term.value 12345
58
+ #
59
+ # my_term.value #=> 12345
60
+ #
61
+ # If <tt>self</tt> is configured to have a fixed value and a value is passed
62
+ # which does not correspond to the fixed value, a <i>FixedValueInterference</i>
63
+ # exception is raised.
64
+ #
65
+ def value(*args)
66
+ unless args.empty?
67
+ if args.first.to_s != @value.to_s
68
+ raise Exceptions::FixedValueInterference if fixed?
69
+ parent.dirty! if parent and parent.is_a? OngoingCalculation
70
+ end
71
+ end
72
+ super
73
+ end
74
+
75
+ # Represents a custom object, symbol or pattern (to be called via ===) to
76
+ # determine the whether value set for <tt>self</tt> should be considered
77
+ # acceptable. The following symbols are acceptable :numeric, :date or
78
+ # :datetime If validation is specified using a <i>Proc</i> object, the term
79
+ # value should be initialized as the block variable. E.g.,
80
+ #
81
+ # my_input.validation 20
82
+ #
83
+ # my_input.valid? #=> true
84
+ #
85
+ # my_input.value 'some string'
86
+ # my_input.valid? #=> false
87
+ #
88
+ # my_input.value 21
89
+ # my_input.valid? #=> false
90
+ #
91
+ # my_input.value 20
92
+ # my_input.valid? #=> true
93
+ #
94
+ # ---
95
+ #
96
+ # my_input.validation lambda{ |value| value.is_a? Numeric }
97
+ #
98
+ # my_input.valid? #=> true
99
+ #
100
+ # my_input.value 'some string'
101
+ # my_input.valid? #=> false
102
+ #
103
+ # my_input.value 12345
104
+ # my_input.valid? #=> true
105
+ #
106
+ # ---
107
+ #
108
+ # my_input.validation :numeric
109
+ #
110
+ # my_input.valid? #=> false
111
+ #
112
+ # my_input.value 21
113
+ # my_input.valid? #=> true
114
+ #
115
+ # my_input.value "20"
116
+ # my_input.valid? #=> true
117
+ #
118
+ # my_input.value "e"
119
+ # my_input.valid? #=> false
120
+ def validation(*args)
121
+ unless args.empty?
122
+ if args.first.is_a?(Symbol)
123
+ @validation = case args.first
124
+ when :numeric then lambda{|v| v.is_a?(Fixnum) || v.is_a?(Integer) || v.is_a?(Float) || v.is_a?(BigDecimal) || (v.is_a?(String) && v.match(/^[\d\.]+$/))}
125
+ when :date then lambda{|v| v.is_a?(Date) || v.is_a?(DateTime) || Date.parse(v) rescue nil}
126
+ when :datetime then lambda{|v| v.is_a?(Time) || v.is_a?(DateTime) || DateTime.parse(v) rescue nil}
127
+ end
128
+ else
129
+ @validation = args.first
130
+ end
131
+ end
132
+ @validation
133
+ end
134
+
135
+ # Returns true if <tt>self</tt> is configured to contain a fixed (read-only)
136
+ # value
137
+ #
138
+ def fixed?
139
+ @fixed
140
+ end
141
+
142
+ # Returns <tt>true</tt> if the value of <tt>self</tt> does not need to be
143
+ # specified for the parent calculation to calculate a result. Otherwise,
144
+ # returns <tt>false</tt>
145
+ #
146
+ def optional?(usage=nil)
147
+ @optional
148
+ end
149
+
150
+ # Returns <tt>true</tt> if the value of <tt>self</tt> is required in order
151
+ # for the parent calculation to calculate a result. Otherwise, returns
152
+ # <tt>false</tt>
153
+ #
154
+ def compulsory?(usage=nil)
155
+ !optional?(usage)
156
+ end
157
+
158
+ # Check that the value of <tt>self</tt> is valid. If invalid, and is defined
159
+ # as part of a calculation, add the invalidity message to the parent
160
+ # calculation's error list. Otherwise, raise a <i>ChoiceValidation</i>
161
+ # exception.
162
+ #
163
+ def validate!
164
+ # Typically, you just wipe yourself if supplied value not valid, but
165
+ # deriving classes might want to raise an exception
166
+ #
167
+ invalid unless fixed? || valid?
168
+ end
169
+
170
+ # Declare the calculation invalid, reporting to the parent calculation or
171
+ # raising an exception, as appropriate.
172
+ #
173
+ def invalid
174
+ if parent
175
+ parent.invalid(label,instance_eval(&@validation_block))
176
+ else
177
+ raise AMEE::DataAbstraction::Exceptions::ChoiceValidation.new(instance_eval(&@validation_block))
178
+ end
179
+ end
180
+
181
+ # Returns <tt>true</tt> if the UI element of <tt>self</tt> is disabled.
182
+ # Otherwise, returns <tt>false</tt>.
183
+ #
184
+ def disabled?
185
+ super || fixed?
186
+ end
187
+
188
+ protected
189
+ # Returns <tt>true</tt> if the value set for <tt>self</tt> is either blank
190
+ # or passes custom validation criteria. Otherwise, returns <tt>false</tt>.
191
+ #
192
+ def valid?
193
+ validation.blank? || validation === @value_before_cast
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,58 @@
1
+ # Copyright (C) 2011 AMEE UK Ltd. - http://www.amee.com
2
+ # Released as Open Source Software under the BSD 3-Clause license. See LICENSE.txt for details.
3
+
4
+ # :title: Class: AMEE::DataAbstraction::Metadatum
5
+
6
+ module AMEE
7
+ module DataAbstraction
8
+
9
+ # Subclass of <tt>Input</tt> providing methods and attributes appropriate for
10
+ # representing arbitrary metadata which does not correspond to any AMEE profile
11
+ # item value or drill.
12
+ #
13
+ class Metadatum < Input
14
+
15
+ # Initialization of <i>Metadatum</i> objects follows that of the parent
16
+ # <i>Input</i> class. The <tt>interface</tt> attribute of <tt>self</tt> is
17
+ # set to <tt>:drop_down</tt> by default, but can be manually configured if
18
+ # required.
19
+ #
20
+ def initialize(options={},&block)
21
+ super
22
+ interface :drop_down unless interface
23
+ end
24
+
25
+ # Represents a list of acceptable choices for the value of <tt>self</tt>.
26
+ # Set the list of choices by passing an argument. Retrieve the choices by
27
+ # calling without an argument, e.g.,
28
+ #
29
+ # my_metadatum.choices 'London', 'New York', 'Tokyo'
30
+ #
31
+ # my_metadatum.choices #=> [ 'London',
32
+ # 'New York',
33
+ # 'Tokyo' ]
34
+ #
35
+ # A single value of <tt>nil</tt> represents an unrestricted value
36
+ # .
37
+ attr_property :choices
38
+
39
+ # Returns <tt>false</tt> as all metadatum are arbitrarily defined and
40
+ # therefore not directly involved in any AMEE calculation.
41
+ #
42
+ def compulsory?(usage=nil)
43
+ false
44
+ end
45
+
46
+ # Returns <tt>true</tt> if the value set for <tt>self</tt> is valid. If
47
+ # <tt>self</tt> contains neither a custom validation pattern nor any
48
+ # defined choices, <tt>true</tt> is returned. Otherwise, validity depends
49
+ # on the custom validation being successful (if present) and the the value
50
+ # of <tt>self</tt> matching one of the entries in the <tt>choices</tt>
51
+ # attribute (if defined). Otherwise, returns <tt>false</tt>.
52
+ #
53
+ def valid?
54
+ super && (choices.blank? || choices.include?(value))
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,545 @@
1
+ # Copyright (C) 2011 AMEE UK Ltd. - http://www.amee.com
2
+ # Released as Open Source Software under the BSD 3-Clause license. See LICENSE.txt for details.
3
+
4
+ # :title: Class: AMEE::DataAbstraction::OngoingCalculation
5
+
6
+ module AMEE
7
+ module DataAbstraction
8
+
9
+ # Instances of the <i>OngoingCalculation</i> class represent actual
10
+ # calculations made via the AMEE platform.
11
+ #
12
+ # The class inherits from the <i>Calculation</i> class and is therefore
13
+ # primarly characterised by the <tt>label</tt>, <tt>name</tt>, and <tt>path</tt>
14
+ # attributes, as well as an associated instance of the <tt>TermsList</tt>
15
+ # class which represents each of the values (input, outputs, metdata) involved
16
+ # in the calculation.
17
+ #
18
+ # Instances of <i>OngoingCalcualtion</i> are typically instantiated from an
19
+ # instance of <i>PrototypeCalculation</i> using the <tt>#begin_calculation</tt>
20
+ # method, e.g.
21
+ #
22
+ # my_prototype.begin_calculation #=> <AMEE::DataAbstraction::OngoingCalculation ...>
23
+ #
24
+ # In this case, the new instance inherits all of the attributes and terms
25
+ # defined on the <i>PrototypeCalculation</i> template.
26
+ #
27
+ # In contrast to instances of <i>PrototypeCalculation</i>, instances of the
28
+ # <i>OngoingCalculation</i> class will typically have their term values
29
+ # explicitly set according to the specific calculation scenario being
30
+ # represented. Other term attributes, e.g. units, may also be modified on a
31
+ # calculation-by-calculation basis. These values and other attributes form
32
+ # (at least some of) the data which is passed to the AMEE platform in order
33
+ # to make caluclations.
34
+ #
35
+ class OngoingCalculation < Calculation
36
+
37
+ public
38
+
39
+ # String representing the AMEE platform profile UID assocaited with <tt>self</tt>
40
+ attr_accessor :profile_uid
41
+
42
+ # String representing the AMEE platform profile item UID assocaited with <tt>self</tt>
43
+ attr_accessor :profile_item_uid
44
+
45
+ # Hash of invalidity messages. Keys are represent the labels of terms
46
+ # assocaited with <tt>self</tt>. Values are string error message reports associated
47
+ # with the keyed term.
48
+ #
49
+ attr_accessor :invalidity_messages
50
+
51
+ # Construct an Ongoing Calculation. Should be called only via
52
+ # <tt>PrototypeCalculation#begin_calculation</tt>. Not intended for external
53
+ # use.
54
+ #
55
+ def initialize
56
+ super
57
+ dirty!
58
+ reset_invalidity_messages
59
+ end
60
+
61
+ # Returns true if the value of a term associated with <tt>self</tt> has been changed
62
+ # since the calculation was last synchronized with the AMEE platform.
63
+ # Otherwise, return false.
64
+ #
65
+ def dirty?
66
+ @dirty
67
+ end
68
+
69
+ # Declare that the calculation is dirty, i.e. that changes to term values
70
+ # have been made since <tt>self</tt> was last synchronized with the AMEE platform,
71
+ # in which case a synchonization with with AMEE must occur for <tt>self</tt> to be
72
+ # valid.
73
+ #
74
+ def dirty!
75
+ @dirty=true
76
+ end
77
+
78
+ # Declare that the calculation is not dirty, and need not be sent to AMEE
79
+ # for results to be valid.
80
+ #
81
+ def clean!
82
+ @dirty=false
83
+ end
84
+
85
+ # Returns true if all compulsory terms are set, i.e. ahve non-nil values.
86
+ # This inidicates that <tt>self</tt> is ready to be sent to the AMEE platform for
87
+ # outputs to be calculated
88
+ #
89
+ def satisfied?
90
+ inputs.compulsory.unset.empty?
91
+ end
92
+
93
+ # Mass assignment of (one or more) term attributes (value, unit, per_unit)
94
+ # based on data defined in <tt>choice</tt>. <tt>choice</tt> should be
95
+ # a hash with keys representing the labels of terms which are to be updated.
96
+ # Hash values can represent either the the value to be assigned explicitly,
97
+ # or, alternatively, a hash representing any or all of the term value, unit
98
+ # and per_unit attributes (keyed as :value, :unit and :per_unit).
99
+ #
100
+ # Unit attributes can be represented by any form which is accepted by the
101
+ # <i>Quantify::Unit#for</i> method (either an instance of
102
+ # <i>Quantify::Unit::Base</i> (or subclass) or a symbolized or string
103
+ # representation of the a unit symbol, name or label).
104
+ #
105
+ # Nil values are ignored. Term attributes can be intentionally blanked by
106
+ # passing a blank string as the respective hash value.
107
+ #
108
+ # Examples of options hash which modify only term values:
109
+ #
110
+ # options = { :type => 'van' }
111
+ #
112
+ # options = { :type => 'van',
113
+ # :distance => 100 }
114
+ #
115
+ # options = { :type => 'van',
116
+ # :distance => "" }
117
+ #
118
+ # Examples of options hash which modify other term attributes:
119
+ #
120
+ # options = { :type => 'van',
121
+ # :distance => { :value => 100 }}
122
+ #
123
+ # options = { :type => 'van',
124
+ # :distance => { :value => 100,
125
+ # :unit => :mi }}
126
+ #
127
+ # options = { :type => 'van',
128
+ # :distance => { :value => 100,
129
+ # :unit => 'feet' }}
130
+ #
131
+ # my_distance_unit = <Quantify::Unit::NonSI:0xb71cac48 @label="mi" ... >
132
+ # my_time_unit = <Quantify::Unit::NonSI:0xb71c67b0 @label="h" ... >
133
+ #
134
+ # options = { :type => 'van',
135
+ # :distance => { :value => 100,
136
+ # :unit => my_unit,
137
+ # :per_unit => my_time_unit }}
138
+ #
139
+ # my_calculation.choose_without_validation!(options)
140
+ #
141
+ # Do not attempt to check that the values specified are acceptable.
142
+ #
143
+ def choose_without_validation!(choice)
144
+ # Make sure choice keys are symbols since they are mapped to term labels
145
+ # Uses extension methods for Hash defined in /core_extensions
146
+ choice.recursive_symbolize_keys!
147
+
148
+ new_profile_uid= choice.delete(:profile_uid)
149
+ self.profile_uid=new_profile_uid if new_profile_uid
150
+ new_profile_item_uid= choice.delete(:profile_item_uid)
151
+ self.profile_item_uid=new_profile_item_uid if new_profile_item_uid
152
+ choice.each do |k,v|
153
+ next unless self[k]
154
+ if v.is_a? Hash
155
+ # <tt>if has_key?</tt> clause included so that single attributes can
156
+ # be updated without nullifying others if their values are not
157
+ # explicitly passed. Intentional blanking of values is enabled by
158
+ # passing nil or "".
159
+ #
160
+ self[k].value v[:value] if v.has_key?(:value)
161
+ self[k].unit v[:unit] if v.has_key?(:unit)
162
+ self[k].per_unit v[:per_unit] if v.has_key?(:per_unit)
163
+ else
164
+ self[k].value v
165
+ end
166
+ end
167
+ end
168
+
169
+ # Similar to <tt>#choose_without_validation!</tt> but performs validation
170
+ # on the modified input terms and raises a <i>ChoiceValidation</i>
171
+ # exception if any of the values supplied is invalid
172
+ #
173
+ def choose!(choice)
174
+ choose_without_validation!(choice)
175
+ validate!
176
+ raise AMEE::DataAbstraction::Exceptions::ChoiceValidation.new(invalidity_messages) unless
177
+ invalidity_messages.empty?
178
+ end
179
+
180
+ # Similar to <tt>#choose!</tt> but returns <tt>false</tt> if any term
181
+ # attributes are invalid, rather than raising an exception. Returns
182
+ # <tt>true</tt> if validation is successful.
183
+ #
184
+ def choose(choice)
185
+ begin
186
+ choose!(choice)
187
+ return true
188
+ rescue AMEE::DataAbstraction::Exceptions::ChoiceValidation
189
+ return false
190
+ end
191
+ end
192
+
193
+ # Synchonizes the current term values and attributes with the AMEE platform
194
+ # if <tt>self</tt> is <tt>dirty?</tt>, and subsequently calls <tt>clean!</tt>
195
+ # on <tt>self</tt>
196
+ #
197
+ def calculate!
198
+ return unless dirty?
199
+ syncronize_with_amee
200
+ clean!
201
+ end
202
+
203
+ # Check that the values set for all terms are acceptable, and raise a
204
+ # <i>ChoiceValidation</i> exception if not. Error messages are available
205
+ # via the <tt>self.invalidity_messages</tt> hash.
206
+ #
207
+ def validate!
208
+ return unless dirty?
209
+ reset_invalidity_messages
210
+ inputs.each do |d|
211
+ d.validate! unless d.unset?
212
+ end
213
+ autodrill
214
+ end
215
+
216
+ # Declare that the term labelled by <tt>label</tt> has an unnaceptable value
217
+ # and load the <tt>message</tt> into the <tt>invalidity_messages</tt> hash.
218
+ #
219
+ def invalid(label,message)
220
+ @invalidity_messages[label]=message
221
+ end
222
+
223
+ # Set the values of any invalid terms to nil. Can be called following any
224
+ # of the <tt>#choose...</tt> methods so that invalid terms resulting from
225
+ # modification of a previously valid calculation can be cleared. This is
226
+ # particularly useful in cases where drill down choices have been changed
227
+ # thus invalidating the choices for subsequent drills.
228
+ #
229
+ def clear_invalid_terms!
230
+ terms.select do |term|
231
+ invalidity_messages.keys.include?(term.label)
232
+ end.each do |term|
233
+ term.value nil
234
+ end
235
+ reset_invalidity_messages
236
+ end
237
+
238
+ private
239
+
240
+ # Empty the hash of error messages for term choices.
241
+ def reset_invalidity_messages
242
+ @invalidity_messages={}
243
+ end
244
+
245
+ # Obtain from the AMEE platform the results of a calculation, and set these
246
+ # to the output terms of <tt>self</tt>.
247
+ #
248
+ def load_outputs
249
+ outputs.each do |output|
250
+ res=nil
251
+ if output.path==:default
252
+ res= profile_item.amounts.find{|x| x[:default] == true}
253
+ else
254
+ res= profile_item.amounts.find{|x| x[:type] == output.path}
255
+ end
256
+ if res
257
+ output.value res[:value]
258
+ output.unit res[:unit]
259
+ output.per_unit res[:per_unit]
260
+ else
261
+ # If no platform result, then no outputs should be set. Added so that
262
+ # nullifying a compulsory PIV wipes output value, otherwise previous
263
+ # output values persist after PIVs have been removed.
264
+ #
265
+ output.value nil
266
+ end
267
+ end
268
+ end
269
+
270
+ # Load any metadata stored in the AMEE profile which can be used to set
271
+ # metadata for <tt>self</tt>. Not implemented in AMEE yet.
272
+ #
273
+ def load_metadata
274
+ end
275
+
276
+ # If a profile item exists for <tt>self</tt>, load the corresponding values
277
+ # and units for any unset <i>Profile</i> terms (profile item values) from
278
+ # the AMEE platform
279
+ #
280
+ def load_profile_item_values
281
+ return unless profile_item
282
+ profiles.unset.each do |term|
283
+ ameeval=profile_item.values.find { |value| value[:path] == term.path }
284
+ term.value ameeval[:value]
285
+ term.unit ameeval[:unit]
286
+ term.per_unit ameeval[:per_unit]
287
+ end
288
+ end
289
+
290
+ # Load drill values from the AMEE platform. If the remote drills selections
291
+ # are different than locally set values, raise a <i>Syncronization</i>
292
+ # exception.
293
+ #
294
+ # If an exception is raised, typical practice would be to delete the profile
295
+ # item associated with <tt>self</tt> and create a new one with the current
296
+ # selection of drill down choices (see, for example, the
297
+ # <tt>#synchronize_with_amee</tt> method
298
+ #
299
+ def load_drills
300
+ return unless profile_item
301
+ drills.each do |term|
302
+ ameeval=data_item.value(term.path)
303
+ raise Exceptions::Syncronization if term.set? && ameeval!=term.value
304
+ term.value ameeval
305
+ end
306
+ end
307
+
308
+ # Dispatch the calculation to AMEE. If necessary, delete an out of date
309
+ # AMEE profile item and create a new one. Fetch any values which are stored
310
+ # in the AMEE platform and not stored locally. Send any values stored locally
311
+ # and not stored in the AMEE platform. Fetch calculation results from the
312
+ # AMEE platform and update outputs assocaited with <tt>self</tt>
313
+ #
314
+ def syncronize_with_amee
315
+ new_memoize_pass
316
+ find_profile
317
+ load_profile_item_values
318
+ begin
319
+ load_drills
320
+ rescue Exceptions::Syncronization
321
+ delete_profile_item
322
+ end
323
+ load_metadata
324
+ # We could create an unsatisfied PI, and just check drilled? here
325
+ if satisfied?
326
+ if profile_item
327
+ set_profile_item_values
328
+ else
329
+ create_profile_item
330
+ end
331
+ load_outputs
332
+ end
333
+ rescue AMEE::UnknownError
334
+ # Tidy up, only if we created a "Bad" profile item.
335
+ # Need to check this condition
336
+ delete_profile_item
337
+ raise DidNotCreateProfileItem
338
+ end
339
+
340
+ # Returns a <i>String</i> representation of drill down choices appropriate for
341
+ # submitting to the AMEE platform. An optional hash argument can be provided,
342
+ # with the key <tt>:before</tt> in order to specify the drill down choice
343
+ # to which the representation is required, e.g.
344
+ #
345
+ # my_calc.drill_options #=> "type=van&fuel=petrol&size=2.0+litres"
346
+ #
347
+ # my_calc.drill_options(:before => :size) #=> "type=van&fuel=petrol"
348
+ #
349
+ def drill_options(options={})
350
+ to=options.delete(:before)
351
+ drills_to_use=to ? drills.before(to).set : drills.set
352
+ drills_to_use.map{|x| "#{CGI.escape(x.path)}=#{CGI.escape(x.value)}"}.join("&")
353
+ end
354
+
355
+ # Returns a <i>Hash</i> representation of <i>Profile</i> term attributes
356
+ # appropriate for submitting to the AMEE platform via the AMEE rubygem.
357
+ #
358
+ def profile_options
359
+ result={}
360
+ profiles.set.each do |piv|
361
+ result[piv.path]=piv.value
362
+ result["#{piv.path}Unit"]=piv.unit.label unless piv.unit.nil?
363
+ result["#{piv.path}PerUnit"]=piv.per_unit.label unless piv.per_unit.nil?
364
+ end
365
+ if contents[:start_date] && !contents[:start_date].value.blank?
366
+ result[:start_date] = contents[:start_date].value
367
+ end
368
+ if contents[:end_date] && !contents[:end_date].value.blank?
369
+ result[:end_date] = contents[:end_date].value
370
+ end
371
+ return result
372
+ end
373
+
374
+ # Returns a <i>Hash</i> of options for profile item GET requests to the AMEE
375
+ # platform.
376
+ #
377
+ # This is where is where return unit convertion requests can be handled
378
+ # if/when theseare implemented.
379
+ #
380
+ def get_options
381
+ # Specify unit options here based on the contents
382
+ # getopts={}
383
+ # getopts[:returnUnit] = params[:unit] if params[:unit]
384
+ # getopts[:returnPerUnit] = params[:perUnit] if params[:perUnit]
385
+ return {}
386
+ end
387
+
388
+ # Return the <i>AMEE::Profile::Profile</i> object under which the AMEE profile
389
+ # item associated with <tt>self</tt> belongs and and update
390
+ # <tt>self.profile_uid</tt> to make the appropriate reference.
391
+ #
392
+ def find_profile
393
+ unless self.profile_uid
394
+ prof ||= AMEE::Profile::ProfileList.new(connection).first
395
+ prof ||= AMEE::Profile::Profile.create(connection)
396
+ self.profile_uid=prof.uid
397
+ end
398
+ end
399
+
400
+ # Generate a unique name for the profile item assocaited with <tt>self</tt>.
401
+ # This is required in order to make similarly drilled profile items within
402
+ # the same profile distinguishable.
403
+ #
404
+ # This is random at present but could be improved to generate more meaningful
405
+ # name by interrogating metadata according to specifications of an
406
+ # organisational model.
407
+ #
408
+ def amee_name
409
+ UUIDTools::UUID.timestamp_create
410
+ end
411
+
412
+
413
+ # Create a profile item in the AMEE platform to be associated with
414
+ # <tt>self</tt>. Raises <i>AlreadyHaveProfileItem</i> exception if a
415
+ # profile item value is already associated with <tt>self</tt>
416
+ #
417
+ def create_profile_item
418
+ raise Exceptions::AlreadyHaveProfileItem unless profile_item_uid.blank?
419
+ location = AMEE::Profile::Item.create(profile_category,
420
+ # call <tt>#data_item_uid</tt> on drill object rather than <tt>self</tt>
421
+ # since there exists no profile item value yet
422
+ amee_drill.data_item_uid,
423
+ profile_options.merge(:get_item=>false,:name=>amee_name))
424
+ self.profile_item_uid=location.split('/').last
425
+ end
426
+
427
+ # Methods which should be memoized once per interaction with AMEE to minimise
428
+ # API calls. These require wiping at every pass, because otherwise, they might
429
+ # change, e.g. if metadata changes change the profile
430
+ #
431
+ MemoizedProfileInformation=[:profile_item,:data_item,:profile_category]
432
+
433
+ # Clear the memoized values.
434
+ def new_memoize_pass
435
+ MemoizedProfileInformation.each do |prop|
436
+ instance_variable_set("@#{prop.to_s}",nil)
437
+ end
438
+ end
439
+
440
+ # Return the <i>AMEE::Profile::Item</i> object associated with self. If
441
+ # not set, instantiates via the AMEE platform and assigns to <tt>self</tt>
442
+ #
443
+ def profile_item
444
+ @profile_item||=AMEE::Profile::Item.get(connection, profile_item_path, get_options) unless profile_item_uid.blank?
445
+ end
446
+
447
+ # Update the associated profile item in the AMEE platform with the current
448
+ # <i>Profile</i> term values and attributes
449
+ #
450
+ def set_profile_item_values
451
+ AMEE::Profile::Item.update(connection,profile_item_path,
452
+ profile_options.merge(:get_item=>false))
453
+ #Clear the memoised profile item, to reload with updated values
454
+ @profile_item=nil
455
+ end
456
+
457
+ # Delete the profile item which is associated with <tt>self</tt> from the
458
+ # AMEE platform and nullify the local references (i.e.
459
+ # <tt>@profile_item</tt> and <tt>#profile_item_uid</tt>)
460
+ #
461
+ def delete_profile_item
462
+ AMEE::Profile::Item.delete(connection,profile_item_path)
463
+ self.profile_item_uid=false
464
+ @profile_item=nil
465
+ end
466
+
467
+ # Returns a string representing the AMEE platform path to the profile
468
+ # category associated with <tt>self</self>
469
+ #
470
+ def profile_category_path
471
+ "/profiles/#{profile_uid}#{path}"
472
+ end
473
+
474
+ # Returns a string representing the AMEE platform path to the profile
475
+ # item associated with <tt>self</self>
476
+ #
477
+ def profile_item_path
478
+ "#{profile_category_path}/#{profile_item_uid}"
479
+ end
480
+
481
+ # Returns a string representing the AMEE platform path to the data item
482
+ # associated with <tt>self</self>
483
+ #
484
+ def data_item_path
485
+ "/data#{path}/#{data_item_uid}"
486
+ end
487
+
488
+ # Returns a string representing the AMEE platform UID for the data item
489
+ # associated with <tt>self</self>
490
+ #
491
+ def data_item_uid
492
+ profile_item.data_item_uid
493
+ end
494
+
495
+ # Return the <i>AMEE::Data::Item</i> object associated with self. If
496
+ # not set, instantiates via the AMEE platform and assigns to <tt>self</tt>
497
+ #
498
+ def data_item
499
+ @data_item||=AMEE::Data::Item.get(connection, data_item_path, get_options)
500
+ end
501
+
502
+ # Return the <i>AMEE::Profile::Category</i> object associated with self. If
503
+ # not set, instantiates via the AMEE platform and assigns to <tt>self</tt>
504
+ #
505
+ def profile_category
506
+ @profile_category||=AMEE::Profile::Category.get(connection, profile_category_path)
507
+ end
508
+
509
+ # Automatically set the value of a drill term if there is only one choice
510
+ def autodrill
511
+
512
+ picks=amee_drill.selections
513
+ picks.each do |path,value|
514
+ # If drill term does not exist, initialize a dummy instance.
515
+ #
516
+ # This is useful in those cases where some drills selections are unecessary
517
+ # (i.e. not all choices require selection for data items to be uniquely
518
+ # identified) and removes the need to explicitly specify the blank drills
519
+ # in configuration. This doesn't matter if calculations are auto configured.
520
+ #
521
+ if drill = drill_by_path(path)
522
+ drill.value value
523
+ else
524
+ drills << Drill.new {path path; value value}
525
+ end
526
+ end
527
+ end
528
+
529
+ public
530
+
531
+ # Instantiate an <tt>AMEE::Data::DrillDown</tt> object representing the
532
+ # drill down sequence defined by the drill terms associated with
533
+ # <tt>self</tt>. As with <tt>#drill_options</tt>, An optional hash argument
534
+ # can be provided, with the key <tt>:before</tt> in order to specify the
535
+ # drill down choice to which the representation is required, e.g.
536
+ #
537
+ # my_calc.amee_drill(:before => :size)
538
+ #
539
+ def amee_drill(options={})
540
+ AMEE::Data::DrillDown.get(connection,"/data#{path}/drill?#{drill_options(options)}")
541
+ end
542
+
543
+ end
544
+ end
545
+ end