Class: XSD::BaseObject

Inherits:
Object
  • Object
show all
Defined in:
lib/xsd/base_object.rb

Overview

Base object

Constant Summary collapse

NO_ELEMENTS_CONTAINER =

Objects that can not have nested elements

i[annotation simpleType attributeGroup attribute
unique union simpleContent list any anyAttribute key keyref].freeze
NO_ATTRIBUTES_CONTAINER =

Objects that cannot have nested attributes

i[annotation unique anyAttribute all
attribute choice sequence group simpleType facet key keyref].freeze
XML_SCHEMA =

Base XMLSchema namespace

'http://www.w3.org/2001/XMLSchema'
XML_SCHEMA_NS =

Namespace map used for all internal xpath queries

{ 'xs' => XML_SCHEMA }.freeze

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ BaseObject

Returns a new instance of BaseObject.

Raises:



38
39
40
41
42
43
# File 'lib/xsd/base_object.rb', line 38

def initialize(options = {})
  raise Error, "#{self.class}.new expects a hash parameter" unless options.is_a?(Hash)

  @options = options
  @cache   = {}
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method, *args) ⇒ Object (protected)

Lookup for properties

Parameters:

  • method (Symbol)
  • args (Array)


313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# File 'lib/xsd/base_object.rb', line 313

def method_missing(method, *args)
  # check cache first, nil and false results are cached as well
  return @cache[method] if @cache.key?(method)

  # check for property first
  if (property = self.class.properties[method])
    return @cache[method] = property_value(property, method, args)
  end

  # if object has ref it cannot contain any type and children, so proxy call to target object
  if node['ref'] && method != :ref && method != :reference
    return @cache[method] = reference.send(method, *args)
  end

  # then check for linked types
  if (link = self.class.links[method])
    name = link[:property] ? send(link[:property]) : nil
    if name
      return @cache[method] = object_by_name(link[:type], name)
    elsif is_a?(Restriction) && i[base_simple_type base_complex_type].include?(method)
      # handle restriction without base
      return @cache[method] = nil
    end
  end

  # last check for nested objects
  if (child = self.class.children[method])
    result = child[:type].is_a?(Array) ? map_children(child[:type][0]) : map_child(child[:type])
    return @cache[method] = result
  end

  super
end

Class Attribute Details

.children ⇒ Object (readonly)

Returns the value of attribute children.



23
24
25
# File 'lib/xsd/base_object.rb', line 23

def children
  @children
end

Returns the value of attribute links.



23
24
25
# File 'lib/xsd/base_object.rb', line 23

def links
  @links
end

.properties ⇒ Object (readonly)

Returns the value of attribute properties.



23
24
25
# File 'lib/xsd/base_object.rb', line 23

def properties
  @properties
end

Instance Attribute Details

#id ⇒ Object

Optional. Specifies a unique ID for the element property :id, :string

Returns:

  • String



61
62
63
# File 'lib/xsd/base_object.rb', line 61

def id
  node['id']
end

#options ⇒ Object (readonly)

Returns the value of attribute options.



6
7
8
# File 'lib/xsd/base_object.rb', line 6

def options
  @options
end

Class Method Details

.child(name, type, options = {}) ⇒ Object

Define new object child

Parameters:

  • name (Symbol)
  • type (Symbol, Array<Symbol>)
  • options (Hash) (defaults to: {})


292
293
294
295
296
297
# File 'lib/xsd/base_object.rb', line 292

def self.child(name, type, options = {})
  children[to_underscore(name)] = {
    type: type,
    **options
  }
end

Define new object child

Parameters:

  • name (Symbol)
  • type (Symbol)
  • options (Hash) (defaults to: {})


303
304
305
306
307
308
# File 'lib/xsd/base_object.rb', line 303

def self.link(name, type, options = {})
  links[to_underscore(name)] = {
    type: type,
    **options
  }
end

.mapped_name ⇒ Object

Get mapped element name

Returns:

  • Symbol



357
358
359
360
361
362
363
# File 'lib/xsd/base_object.rb', line 357

def self.mapped_name
  @mapped_name ||= begin
    name = self.name.split('::').last
    name[0] = name[0].downcase
    name.to_sym
  end
end

.property(name, type, options = {}, &block) ⇒ Object

Define new object property

Parameters:

  • name (Symbol)
  • type (Symbol)
  • options (Hash) (defaults to: {})


279
280
281
282
283
284
285
286
# File 'lib/xsd/base_object.rb', line 279

def self.property(name, type, options = {}, &block)
  properties[to_underscore(name)] = {
    name: name,
    type: type,
    resolve: block,
    **options
  }
end

.to_underscore(string) ⇒ Object



271
272
273
# File 'lib/xsd/base_object.rb', line 271

def self.to_underscore(string)
  string.to_s.gsub(/([^A-Z])([A-Z]+)/, '\1_\2').sub(':', '_').downcase.to_sym
end

Instance Method Details

#[](*args) ⇒ Object

Get element or attribute by path

Returns:

  • Element, Attribute, nil



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/xsd/base_object.rb', line 98

def [](*args)
  result = self

  args.flatten.each do |curname|
    next if result.nil?

    curname = curname.to_s

    if curname[0] == '@'
      result = result.collect_attributes.find { |attr| definition_match?(attr, curname[1..]) }
    else
      result = result.collect_elements.find { |elem| definition_match?(elem, curname) }
    end
  end

  result
end

#collect_attributes ⇒ Object

Get all available attributes on the current stack level



245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/xsd/base_object.rb', line 245

def collect_attributes(*)
  @collect_attributes ||= if NO_ATTRIBUTES_CONTAINER.include?(self.class.mapped_name)
                            []
                          elsif is_a?(Referenced) && ref
                            reference.collect_attributes
                          else
                            # map children recursive
                            map_children(:*).flat_map do |obj|
                              if obj.is_a?(Attribute) || obj.is_a?(AnyAttribute)
                                obj
                              else
                                # get attributes considering references
                                (obj.is_a?(Referenced) && obj.ref ? obj.reference : obj).collect_attributes
                              end
                            end
                          end
end

#collect_elements ⇒ Object

Get all available elements on the current stack level



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/xsd/base_object.rb', line 225

def collect_elements(*)
  @collect_elements ||= if NO_ELEMENTS_CONTAINER.include?(self.class.mapped_name)
                          []
                        elsif is_a?(Referenced) && ref
                          reference.collect_elements
                        else
                          # map children recursive
                          map_children(:*).flat_map do |obj|
                            if obj.is_a?(Element) || obj.is_a?(Any)
                              obj
                            else
                              # get elements considering references
                              (obj.is_a?(Referenced) && obj.ref ? obj.reference : obj).collect_elements
                            end
                          end
                        end
end

#documentation ⇒ Object

Return element documentation



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

def documentation
  @documentation ||= begin
    docs = documentation_for(node)
    if docs.any?
      docs
    elsif is_a?(Referenced) && referenced?
      documentation_for(reference.node)
    else
      []
    end
  end
end

#documentation_for(node) ⇒ Object

Return documentation for specified node

Parameters:

  • node (Nokogiri::XML::Node)


219
220
221
# File 'lib/xsd/base_object.rb', line 219

def documentation_for(node)
  node.xpath('./xs:annotation/xs:documentation/text()', XML_SCHEMA_NS).map { |x| x.text.strip }
end

#get_prefix(name) ⇒ Object

Get namespace prefix from node name

Parameters:

  • name (String, nil) —

    Name to strip from

Returns:

  • String, nil



197
198
199
# File 'lib/xsd/base_object.rb', line 197

def get_prefix(name)
  name&.include?(':') ? name.split(':').first : nil
end

#inspect ⇒ Object

Get object string representation

Returns:

  • String



53
54
55
# File 'lib/xsd/base_object.rb', line 53

def inspect
  "#<#{self.class.name} path=#{node.path}>"
end

#map_child(name) ⇒ Object

Get child object

Parameters:

  • name (Symbol)

Returns:

  • BaseObject, nil



183
184
185
# File 'lib/xsd/base_object.rb', line 183

def map_child(name)
  map_children(name).first
end

#map_children(name) ⇒ Object

Get child objects

Parameters:

  • name (Symbol)


176
177
178
# File 'lib/xsd/base_object.rb', line 176

def map_children(name)
  nodes(name).map { |node| node_to_object(node) }
end

#namespaces ⇒ Object

Get current namespaces

Returns:

  • Hash



67
68
69
# File 'lib/xsd/base_object.rb', line 67

def namespaces
  node.namespaces || {}
end

#node ⇒ Object

Get current XML node

Returns:

  • Nokogiri::XML::Node



47
48
49
# File 'lib/xsd/base_object.rb', line 47

def node
  options[:node]
end

#node_to_object(node) ⇒ Object

Get reader object for node

Parameters:

  • node (Nokogiri::XML::Node)

Returns:

  • BaseObject

Raises:



143
144
145
146
147
148
149
150
151
152
# File 'lib/xsd/base_object.rb', line 143

def node_to_object(node)
  # check object in cache first
  cached = reader.object_cache[node]
  return cached if cached

  klass = XML::CLASS_MAP[node.name]
  raise Error, "Object class not found for '#{node.name}'" unless klass

  reader.object_cache[node] = klass.new(options.merge(node: node, schema: schema))
end

#nodes(name = :*, deep = false) ⇒ Object

Get child nodes

Parameters:

  • name (Symbol) (defaults to: :*)
  • deep (Boolean) (defaults to: false)

Returns:

  • Nokogiri::XML::NodeSet



75
76
77
# File 'lib/xsd/base_object.rb', line 75

def nodes(name = :*, deep = false)
  node.xpath("./#{deep ? '/' : ''}xs:#{name}", XML_SCHEMA_NS)
end

#object_by_name(node_name, name) ⇒ Object

Search node by name in all available schemas and return its object

Parameters:

  • node_name (Symbol)
  • name (String)

Returns:

  • BaseObject, nil



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/xsd/base_object.rb', line 120

def object_by_name(node_name, name)
  # get prefix and local name
  name_prefix = get_prefix(name)
  name_local  = strip_prefix(name)

  # do not search for built-in types
  return nil if schema.namespace_prefix == name_prefix

  # determine schema for namespace
  search_schemas = schemas_for_namespace(name_prefix)

  # find element in target schema, name is passed as an xpath variable to stay safe with any name contents
  search_schemas.each do |s|
    node = s.node.at_xpath("./xs:#{node_name}[@name=$name]", XML_SCHEMA_NS, name: name_local)
    return s.node_to_object(node) if node
  end

  nil
end

#parent ⇒ Object

Get xml parent object

Returns:

  • BaseObject, nil



156
157
158
159
160
161
162
163
164
165
# File 'lib/xsd/base_object.rb', line 156

def parent
  return @parent if defined?(@parent)

  parent_node = node.respond_to?(:parent) ? node.parent : nil

  # the parent may be a document or a node of a foreign schema (e.g. wsdl:types), there is no object for those
  @parent = if parent_node.is_a?(Nokogiri::XML::Element) && XML::CLASS_MAP.key?(parent_node.name)
              node_to_object(parent_node)
            end
end

#reader ⇒ Object

Get reader instance

Returns:

  • XML



265
266
267
# File 'lib/xsd/base_object.rb', line 265

def reader
  options[:reader]
end

#schema ⇒ Object

Get current schema object

Returns:

  • Schema



169
170
171
# File 'lib/xsd/base_object.rb', line 169

def schema
  options[:schema]
end

#schemas_for_namespace(ns_or_prefix) ⇒ Object

Get schemas by namespace or prefix

Parameters:

  • ns_or_prefix (String, nil)


82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/xsd/base_object.rb', line 82

def schemas_for_namespace(ns_or_prefix)
  # resolve namespace for current node if prefix was provided
  prefix = namespaces[['xmlns', (ns_or_prefix == '' ? nil : ns_or_prefix)].compact.join(':')]
  ns = prefix || ns_or_prefix

  if schema.targets_namespace?(ns)
    [schema, *schema.includes.map(&:imported_schema)]
  elsif (import = schema.import_by_namespace(ns))
    [import.imported_schema]
  else
    raise Error, "Schema not found for namespace '#{ns}' in '#{schema.id || schema.target_namespace}'"
  end
end

#strip_prefix(name) ⇒ Object

Strip namespace prefix from node name

Parameters:

  • name (String, nil) —

    Name to strip from

Returns:

  • String, nil



190
191
192
# File 'lib/xsd/base_object.rb', line 190

def strip_prefix(name)
  name&.include?(':') ? name.split(':').last : name
end