blob: 3a678cb334446108d17834ea98acc66bdec2bd22 [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
require_relative "../../deltacloud/core_ext"
module CIMI
module Model
end
end
# The smarts of converting from XML and JSON into internal objects
class CIMI::Model::Schema
#
# Attributes describe how we extract values from XML/JSON
#
class Attribute
attr_reader :name, :xml_name, :json_name
attr_reader :required
def initialize(name, opts = {})
@name = name
@xml_name = opts[:xml_name] || name.to_s.camelize(true)
@json_name = opts[:json_name] || name.to_s.camelize(true)
@required = opts[:required] || false
end
def from_xml(xml, model)
model[@name] = xml[@xml_name].first if xml.has_key?(@xml_name)
end
def from_json(json, model)
model[@name] = json[@json_name]
end
def to_xml(model, xml)
xml[@xml_name] = [model[@name]] if model[@name]
end
def to_json(model, json)
json[@json_name] = model[@name] if model and model[@name]
end
def convert(value)
value
end
def required?
@required
end
def valid?(value)
!value.nil? and !value.to_s.empty?
end
end
class Scalar < Attribute
def initialize(name, opts)
@text = opts[:text]
if ! [nil, :nested, :direct].include?(@text)
raise "text option for scalar must be :nested or :direct"
end
super(name, opts)
end
def text?; @text; end
def nested_text?; @text == :nested; end
def from_xml(xml, model)
case @text
when :nested then model[@name] = xml[@xml_name].first["content"] if xml[@xml_name]
when :direct then model[@name] = xml["content"]
else model[@name] = xml[@xml_name]
end
end
def to_xml(model, xml)
return unless model
return unless model[@name]
case @text
when :nested then xml[@xml_name] = [{ "content" => model[@name] }]
when :direct then xml["content"] = model[@name]
else xml[@xml_name] = model[@name]
end
end
end
class Struct < Attribute
attr_accessor :schema
def initialize(name, opts={}, &block)
content = opts[:content]
super(name, opts)
if opts[:schema]
if block_given?
raise "Cannot provide :schema option and a block"
end
@schema = opts[:schema]
else
@schema = CIMI::Model::Schema.new
@schema.instance_eval(&block) if block_given?
@schema.scalar(content, :text => :direct, :required => opts[:required]) if content
end
end
def from_xml(xml, model)
xml = xml.has_key?(xml_name) ? xml[xml_name].first : {}
model[name] = convert_from_xml(xml)
end
def from_json(json, model)
json = json.has_key?(json_name) ? json[json_name] : {}
model[name] = convert_from_json(json)
end
def to_xml(model, xml)
conv = convert_to_xml(extract(model))
xml[xml_name] = [conv] unless conv.empty?
end
def to_json(model, json)
conv = convert_to_json(extract(model))
json[json_name] = conv unless conv.empty?
end
def convert_from_xml(xml)
sub = struct.new
@schema.from_xml(xml, sub)
sub
end
def convert_from_json(json)
sub = struct.new
@schema.from_json(json, sub)
sub
end
def convert_to_xml(model)
xml = ::Hash.new
@schema.to_xml(model, xml)
xml
end
def convert_to_json(model)
json = {}
@schema.to_json(model, json)
json
end
def valid?(value)
@schema.required_attributes.all? { |a|
a.valid?(value.send(a.name))
}
end
def convert(value)
if @klass
@klass.new(value || {})
else
super(value)
end
end
private
def struct
if @klass
@klass
else
@struct_class ||= ::Struct.new(nil, *@schema.attribute_names)
end
end
def extract(model)
if model.respond_to?("[]")
model[name] || {}
else
{}
end
end
end
class Ref < CIMI::Model::Schema::Struct
def initialize(name, opts={}, &block)
raise 'The :class attribute must be set' unless opts[:class]
refname = "#{opts[:class].name.split("::").last}Ref"
if CIMI::Model::const_defined?(refname)
@klass = CIMI::Model::const_get(refname)
else
@klass = Class.new(opts[:class]) do |m|
scalar :href
end
CIMI::Model::const_set(refname, @klass)
end
@klass.class_eval { def href?; !href.nil?; end }
opts[:schema] = @klass.schema
super(name, opts, &block)
end
# The 'ref' could be the reference to a CIMI entity or the full CIMI
# entity representation.
#
def valid?(value)
!value.href.nil? || @klass.schema.required_attributes.all? { |a|
a.valid?(value.send(a.name))
}
end
end
class Array < Attribute
attr_accessor :struct
# For an array :funThings, we collect all <funThing/> elements (XmlSimple
# actually does the collecting)
def initialize(name, opts = {}, &block)
unless opts[:xml_name]
opts[:xml_name] = name.to_s.singularize.camelize.uncapitalize
end
if opts[:ref] && block_given?
raise "Provide only one of :ref or a block"
end
super(name, opts)
if opts[:ref]
@struct = Ref.new(name, :class=> opts[:ref])
else
@struct = Struct.new(name, opts, &block)
end
end
def from_xml(xml, model)
model[name] = (xml[xml_name] || []).map { |elt| @struct.convert_from_xml(elt) }
end
def from_json(json, model)
model[name] = (json[json_name] || []).map { |elt| @struct.convert_from_json(elt) }
end
def to_xml(model, xml)
ary = (model[name] || []).map { |elt| @struct.convert_to_xml(elt) }
xml[xml_name] = ary unless ary.empty?
end
def to_json(model, json)
ary = (model[name] || []).map { |elt| @struct.convert_to_json(elt) }
json[json_name] = ary unless ary.empty?
end
end
class Hash < Attribute
def initialize(name, opts = {}, &block)
opts[:json_name] = name.to_s.pluralize unless opts[:json_name]
super(name, opts)
end
def from_xml(xml, model)
model[name] = (xml[xml_name] || []).inject({}) do |result, item|
result[item["key"]] = item["content"]
result
end
end
def from_json(json, model)
model[name] = json[json_name] || {}
end
def to_xml(model, xml)
mapped = extract(model).map { |k, v| { "key" => k, "content" => v } }
xml[xml_name] = mapped unless mapped.empty?
end
def to_json(model, json)
h = extract(model)
json[json_name] = h unless h.empty?
end
private
def extract(model)
if model.respond_to?("[]")
model[name] || {}
else
{}
end
end
end
class Collection < Attribute
def initialize(name, opts = {})
params = {}
params[:scope] = opts.delete(:scope)
super(name, opts)
unless opts[:class]
raise "Specify the class of collection entries using :class"
end
params[:embedded] = true
unless opts[:class].collection_class
opts[:class].collection_class = CIMI::Model::Collection.generate(opts[:class], params)
end
@collection_class = opts[:class].collection_class
end
def from_xml(xml, model)
if xml[xml_name]
model[name] = @collection_class.schema.from_xml(xml[xml_name].first, {})
end
end
def from_json(json, model)
if json[json_name]
model[name] = @collection_class.schema.from_json(json[json_name], {})
end
end
def to_xml(model, xml)
return if model[name].nil?
model[name].prepare
if model[name].entries.empty?
xml[xml_name] = { "href" => model[name].href }
else
xml[xml_name] = @collection_class.schema.to_xml(model[name])
end
end
def to_json(model, json)
return if model[name].nil?
model[name].prepare
if model[name].entries.empty?
json[json_name] = { "href" => model[name].href }
else
json[json_name] = @collection_class.schema.to_json(model[name])
end
end
# Convert a Hash or Array to an instance of the collection class
def convert(value)
if value.is_a?(::Array)
@collection_class.new(:entries => value)
else
@collection_class.new(value || {})
end
end
end
#
# The actual Schema class
#
attr_accessor :attributes
def initialize
@attributes = []
end
def deep_clone
Marshal::load(Marshal.dump(self))
end
def collections
@attributes.select { |a| a.is_a?(Collection) }
end
def convert(name, value)
attr = @attributes.find { |a| a.name == name }
raise "Unknown attribute #{name}" unless attr
attr.convert(value)
end
def from_xml(xml, model = {})
@attributes.freeze
@attributes.each { |attr| attr.from_xml(xml, model) }
model
end
def from_json(json, model = {})
@attributes.freeze
@attributes.each { |attr| attr.from_json(json, model) }
model
end
def to_xml(model, xml = nil)
xml ||= ::Hash.new
@attributes.freeze
model.prepare if model.respond_to?(:prepare)
@attributes.each { |attr| attr.to_xml(model, xml) }
xml
end
#For MachineCollection, copy over the schema of Machine to hold
#each member of the collection - avoid duplicating the schemas
def add_collection_member_array(model)
member_symbol = model.name.split("::").last.underscore.pluralize.to_sym
members = CIMI::Model::Schema::Array.new(member_symbol)
members.struct.schema.attributes = model.schema.attributes
self.attributes << members
end
def to_json(model, json = {})
@attributes.freeze
model.prepare if model.respond_to?(:prepare)
@attributes.each { |attr| attr.to_json(model, json) }
json
end
def attribute_names
@attributes.map { |a| a.name }
end
def required_attributes
@attributes.select { |a| a.required? }
end
#
# The DSL
#
# Requires that the class into which this is included has a
# +add_attributes!+ method
module DSL
def href(*args)
opts = args.extract_opts!
args.each { |arg| struct(arg, opts) { scalar :href, :required => opts[:required] } }
end
def text(*args)
args.expand_opts!(:text => :nested)
scalar(*args)
end
def scalar(*args)
add_attributes!(args, Scalar)
end
def array(name, opts={}, &block)
add_attributes!([name, opts], Array, &block)
end
def struct(name, opts={}, &block)
add_attributes!([name, opts], Struct, &block)
end
def ref(name, opts={})
unless opts[:class]
s = name.to_s.camelize.gsub(/Config$/, "Configuration")
opts[:class] = CIMI::Model::const_get(s)
end
add_attributes!([name, opts], Ref)
end
def hash_map(name)
add_attributes!([name, {}], Hash)
end
def collection(name, opts={})
opts[:scope] = self
add_attributes!([name, opts], Collection)
end
end
include DSL
def add_attributes!(args, attr_klass, &block)
raise "The schema has already been used to convert objects" if @attributes.frozen?
opts = args.extract_opts!
args.each { |arg| @attributes << attr_klass.new(arg, opts, &block) }
end
end