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.org 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


175
176
177
# File 'lib/procedo/xml.rb', line 175

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

.parse_computation(item, element) ⇒ Object


207
208
209
210
211
212
213
# File 'lib/procedo/xml.rb', line 207

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


201
202
203
204
205
# File 'lib/procedo/xml.rb', line 201

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


106
107
108
109
110
111
112
113
114
115
116
# File 'lib/procedo/xml.rb', line 106

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)
    elsif child.element?
      raise "Unexpected child: #{child.name}"
    end
  end
end

.parse_handler(parameter, element) ⇒ Object

Parse <handler> of parameter


158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/procedo/xml.rb', line 158

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

  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>


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
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/procedo/xml.rb', line 119

def parse_parameter(procedure, element, options = {})
  locals = {}
  name = element.attr('name').to_sym
  if element.name != 'parameter'
    type = element.name.to_sym
    if element.has_attribute?('type')
      raise "'type' attribute is not supported in a <#{element.name}> element"
    end
  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)
      locals[info.underscore.to_sym] = element.attr(info).to_s
    end
  end
  %w[component-of display-status].each do |attribute|
    if element.has_attribute?(attribute)
      locals[attribute.underscore.to_sym] = element.attr(attribute).to_s
    end
  end
  parent = options[:group] || procedure
  parameter = parent.add_product_parameter(name, type, options.merge(locals))
  # 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


216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/procedo/xml.rb', line 216

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# 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)
  options[:maintenance] = (element.attr('maintenance').to_s == 'true')
  options[:deprecated] = (element.attr('deprecated').to_s == 'true')
  options[:hidden] = true if element.attr('hidden').to_s == 'true'

  options[:varieties] = element.attr('varieties')
                               .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


180
181
182
# File 'lib/procedo/xml.rb', line 180

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

.parse_setter(parameter, type, element) ⇒ Object


184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/procedo/xml.rb', line 184

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')

  options[:compute_filter] = element.attr('compute-filter') if element.has_attribute?('compute-filter')
  options[:filter] = element.attr('filter') if element.has_attribute?('filter')
  setter = parameter.send("add_#{type}", name, options)
  parse_computations(setter, element)
end