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.
- data/.rvmrc +1 -0
- data/CHANGELOG.txt +4 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +41 -0
- data/LICENSE.txt +27 -0
- data/README.txt +188 -0
- data/Rakefile +102 -0
- data/VERSION +1 -0
- data/amee-data-abstraction.gemspec +115 -0
- data/examples/_calculator_form.erb +27 -0
- data/examples/calculation_controller.rb +16 -0
- data/init.rb +4 -0
- data/lib/amee-data-abstraction.rb +30 -0
- data/lib/amee-data-abstraction/calculation.rb +236 -0
- data/lib/amee-data-abstraction/calculation_set.rb +101 -0
- data/lib/amee-data-abstraction/drill.rb +63 -0
- data/lib/amee-data-abstraction/exceptions.rb +47 -0
- data/lib/amee-data-abstraction/input.rb +197 -0
- data/lib/amee-data-abstraction/metadatum.rb +58 -0
- data/lib/amee-data-abstraction/ongoing_calculation.rb +545 -0
- data/lib/amee-data-abstraction/output.rb +16 -0
- data/lib/amee-data-abstraction/profile.rb +108 -0
- data/lib/amee-data-abstraction/prototype_calculation.rb +350 -0
- data/lib/amee-data-abstraction/term.rb +506 -0
- data/lib/amee-data-abstraction/terms_list.rb +150 -0
- data/lib/amee-data-abstraction/usage.rb +90 -0
- data/lib/config/amee_units.rb +129 -0
- data/lib/core-extensions/class.rb +27 -0
- data/lib/core-extensions/hash.rb +43 -0
- data/lib/core-extensions/ordered_hash.rb +21 -0
- data/lib/core-extensions/proc.rb +15 -0
- data/rails/init.rb +32 -0
- data/spec/amee-data-abstraction/calculation_set_spec.rb +54 -0
- data/spec/amee-data-abstraction/calculation_spec.rb +75 -0
- data/spec/amee-data-abstraction/drill_spec.rb +38 -0
- data/spec/amee-data-abstraction/input_spec.rb +77 -0
- data/spec/amee-data-abstraction/metadatum_spec.rb +17 -0
- data/spec/amee-data-abstraction/ongoing_calculation_spec.rb +494 -0
- data/spec/amee-data-abstraction/profile_spec.rb +39 -0
- data/spec/amee-data-abstraction/prototype_calculation_spec.rb +256 -0
- data/spec/amee-data-abstraction/term_spec.rb +385 -0
- data/spec/amee-data-abstraction/terms_list_spec.rb +53 -0
- data/spec/config/amee_units_spec.rb +71 -0
- data/spec/core-extensions/class_spec.rb +25 -0
- data/spec/core-extensions/hash_spec.rb +44 -0
- data/spec/core-extensions/ordered_hash_spec.rb +12 -0
- data/spec/core-extensions/proc_spec.rb +12 -0
- data/spec/fixtures/electricity.rb +35 -0
- data/spec/fixtures/electricity_and_transport.rb +55 -0
- data/spec/fixtures/transport.rb +26 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +244 -0
- 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
|