Class: Procedo::XML

Inherits:
Object
  • Object
show all
Defined in:
lib/procedo/xml.rb

Overview

The module parse XML procedures. More documentation on schema can be found on wiki.ekylibre.com Sample:

<?xml version="1.0"?>
<procedures xmlns="http://www.ekylibre.org/XML/2013/procedures">
  <procedure name="sowing" categories="planting" actions="sowing">
    <parameters>
      <parameter name="seeds" type="input" filter="is seed and derives from plant and can grow"
        default-name="{{variant}} - [{{birth_day}}/{{birth_month}}/{{birth_year}}] - [{{derivative_of}}]">
        <handler indicator="population"/>
        <handler indicator="net_mass" unit="kilogram" if="self.net_mass? & self.net_mass(kilogram) > 0"
          to="population" backward="value * self..net_mass(kilogram)" forward="value / self..net_mass(kilogram)"/>
        <handler indicator="mass_area_density" unit="kilogram_per_hectare"
          if="self.net_mass? & self.net_mass(kilogram) > 0 & cultivation.net_surface_area? & cultivation.net_surface_area(hectare) > 0"
          to="population" backward="(value * self..net_mass(kilogram)) / cultivation.net_surface_area(hectare)"
          forward="(value * cultivation.net_surface_area(hectare)) / self..net_mass(kilogram)"/>
      </parameter>
      <tool name="sower" filter="can sow"/>
      <doer name="driver" filter="can drive(equipment) and can move"/>
      <parameter name="tractor" type="tool" filter="can tow(equipment) and can move"/>
      <group name="zone">
        <parameter name="land_parcel" type="target" filter="can store(plant)" default-actor="storage"/>
        <parameter name="cultivation" type="output" variety="derivative-of: seeds"
          filter="is derivative-of: seeds" default-name="{{variant}} [{{birth_month_abbr}}. {{birth_year}}] ({{container}})"
          default-shape=":land_parcel" default-variant="production">
          <handler indicator="shape">
            <converter to="shape" forward="intersection(value, land_parcel.shape)" backward="value"/>
            <converter to="population" forward="area(value) / cultivation..net_surface_area(square_meter)"/>
          </handler>
        </parameter>
      </group>
    </parameters>
  </procedure>
</procedures>

Class Method Summary collapse

Class Method Details

.parse(file) ⇒ Object

Parse an XML procedures file with one or many procedures Returns a list of Procedo::Procedure


44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/procedo/xml.rb', line 44

def parse(file)
  collection = []
  f = File.open(file, 'rb')
  document = Nokogiri::XML(f) do |config|
    config.strict.nonet.noblanks.noent
  end
  f.close
  # Add a better syntax check
  if document.root.namespace.href.to_s == XML_NAMESPACE
    document.root.xpath('xmlns:procedure').each do |element|
      collection << parse_procedure(element)
    end
  else
    Rails.logger.info("File #{path} is not a procedure as defined by #{XML_NAMESPACE}")
  end
  collection
end

.parse_attribute(parameter, element) ⇒ Object

Parse <attribute> of parameter


164
165
166
# File 'lib/procedo/xml.rb', line 164

def parse_attribute(parameter, element)
  parse_setter(parameter, :attribute, element)
end

.parse_computation(item, element) ⇒ Object


193
194
195
196
197
198
199
# File 'lib/procedo/xml.rb', line 193

def parse_computation(item, element)
  expression = element.attr('expr').strip
  destinations = element.attr('to').strip.split(/\s*\,\s*/)
  options = {}
  options[:if] = element.attr('if') if element.has_attribute?('if')
  item.add_computation(expression, destinations, options)
end

.parse_computations(item, element) ⇒ Object


187
188
189
190
191
# File 'lib/procedo/xml.rb', line 187

def parse_computations(item, element)
  element.xpath('xmlns:compute').each do |el|
    parse_computation(item, el)
  end
end

.parse_group_children(procedure, element, options = {}) ⇒ Object

Parse list of children of a <parameter-group> or <parameters> tag


90
91
92
93
94
95
96
97
98
99
100
# File 'lib/procedo/xml.rb', line 90

def parse_group_children(procedure, element, options = {})
  element.children.each do |child|
    if child.name == 'parameter' || Procedo::Procedure::ProductParameter::TYPES.include?(child.name.to_sym)
      parse_parameter(procedure, child, options)
    elsif %w(group parameter-group).include?(child.name)
      parse_parameter_group(procedure, child, options)
    else
      raise "Unexpected child: #{child.name}"
    end
  end
end

.parse_handler(parameter, element) ⇒ Object

Parse <handler> of parameter


146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/procedo/xml.rb', line 146

def parse_handler(parameter, element)
  # Extract attributes from XML element
  options = %w(forward backward indicator unit to name if).each_with_object({}) do |attr, hash|
    hash[attr.to_sym] = element.attr(attr) if element.has_attribute?(attr)
    hash
  end

  options[:indicator] ||= options[:name]
  name = options.delete(:name) || options[:indicator]

  handler = parameter.add_handler(name, options)
  # Converters
  if element.xpath('xmlns:converter').any?
    Rails.logger.warn "Converters are no more supported (in #{parameter.procedure_name}/#{parameter.name})"
  end
end

.parse_parameter(procedure, element, options = {}) ⇒ Object

Parse <parameter>


103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/procedo/xml.rb', line 103

def parse_parameter(procedure, element, options = {})
  name = element.attr('name').to_sym
  if element.name != 'parameter'
    type = element.name.to_sym
    raise "'type' attribute is not supported in a <#{element.name}> element" if element.has_attribute?('type')
  else
    type = element.attr('type').underscore.to_sym
  end
  raise "No type given for #{name} parameter" unless type
  %w(filter cardinality).each do |info|
    if element.has_attribute?(info)
      options[info.underscore.to_sym] = element.attr(info).to_s
    end
  end
  options[:output] = {}
  %w(name variety derivative-of variant).each do |attribute|
    if element.has_attribute?(attribute)
      options[:output][attribute.underscore.to_sym] = element.attr(attribute).to_s
    end
  end
  options[:output][:default] = {}
  %w(name variety derivative-of variant).each do |attribute|
    if element.has_attribute?("default-#{attribute}")
      options[:output][:default][attribute.underscore.to_sym] = element.attr("default-#{attribute}").to_s
    end
  end
  parent = options[:group] || procedure
  parameter = parent.add_product_parameter(name, type, options)
  # Handlers
  element.xpath('xmlns:handler').each do |el|
    parse_handler(parameter, el)
  end
  # Attributes
  element.xpath('xmlns:attribute').each do |el|
    parse_attribute(parameter, el)
  end
  # Readings
  element.xpath('xmlns:reading').each do |el|
    parse_reading(parameter, el)
  end
end

.parse_parameter_group(procedure, element, options = {}) ⇒ Object

Parse <parameter-group> element


202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/procedo/xml.rb', line 202

def parse_parameter_group(procedure, element, options = {})
  unless element.has_attribute?('name')
    raise Procedo::Errors::MissingAttribute, "Missing name for parameter-group in #{procedure.name} at line #{element.line}"
  end
  name = element.attr('name').to_sym
  options = {}
  if element.has_attribute?('cardinality')
    options[:cardinality] = element.attr('cardinality').to_s
  end
  parent = options[:group] || procedure
  group = parent.add_group_parameter(name, options)
  parse_group_children(procedure, element, group: group)
end

.parse_procedure(element) ⇒ Object

Parse a DOM element of and XML procedure corresponding to a <procedure>


63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/procedo/xml.rb', line 63

def parse_procedure(element)
  # Create procedure
  name = element.attr('name').to_sym
  options = {}
  options[:required] = true if element.attr('required').to_s == 'true'
  options[:categories] = element.attr('categories').to_s.split(/[\s\,]+/).map(&:to_sym)
  options[:mandatory_actions] = element.attr('actions').to_s.split(/[\s\,]+/).map(&:to_sym)
  options[:optional_actions] = element.attr('optional-actions').to_s.split(/[\s\,]+/).map(&:to_sym)

  procedure = Procedo::Procedure.new(name, options)

  # Adds parameters
  parameters_count = element.xpath('xmlns:parameters').count
  if parameters_count > 1
    raise "Too many <parameters> markup in #{procedure.name}. Only one accepted."
  elsif parameters_count < 1
    raise "No <parameters> markup in #{procedure.name}. One is needed."
  end
  parse_group_children(procedure, element.xpath('xmlns:parameters').first)

  # Check procedure validity
  # procedure.check!

  procedure
end

.parse_reading(parameter, element) ⇒ Object

Parse <reading> of parameter


169
170
171
# File 'lib/procedo/xml.rb', line 169

def parse_reading(parameter, element)
  parse_setter(parameter, :reading, element)
end

.parse_setter(parameter, type, element) ⇒ Object


173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/procedo/xml.rb', line 173

def parse_setter(parameter, type, element)
  name = element.attr('name')
  options = {}
  if element.has_attribute?('value')
    options[:default_value] = element.attr('value')
    options[:hidden] = true
  elsif element.has_attribute?('default-value')
    options[:default_value] = element.attr('default-value')
  end
  options[:if] = element.attr('if') if element.has_attribute?('if')
  setter = parameter.send("add_#{type}", name, options)
  parse_computations(setter, element)
end