lib/edi/formater.rb
module Edi
module Formatter
def self_record
self
end
def to_params(options = {})
to_hash(options).to_params(options)
end
def to_edi(options = {})
options.reverse_merge!(field_separator: '*',
segment_separator: :new_line,
seg_sep_suppress: '<<seg. sep.>>',
inline_field_separator: ':')
output = record_to_edi(data_type = (model = self_record.orm_model).data_type, options, model.schema, self_record)
seg_sep = options[:segment_separator] == :new_line ? "\r\n" : options[:segment_separator].to_s
output.join(seg_sep)
end
def default_hash(options = {})
prepare_options(options)
max_entries = options[:max_entries].to_i
max_entries = nil if max_entries.zero?
hash = record_to_hash(self_record, options, options.delete(:reference), nil, max_entries, options[:viewport].presence)
options.delete(:stack)
hash = { self_record.orm_model.data_type.slug => hash } if options[:include_root]
hash
end
def to_hash(options = {})
default_hash(options)
end
def to_json(options = {})
hash = to_hash(options)
options[:pretty] ? JSON.pretty_generate(hash) : hash.to_json
end
def share_hash(options = {})
if self_record.class.respond_to?(:share_options)
options =
begin
options.reverse_merge(self_record.class.share_options)
rescue Exception
options
end
else
options = options.reverse_merge(
ignore: [:id],
include_blanks: true,
protected: true,
polymorphic: true
)
end
to_hash(options)
end
def share_json(options = {})
hash = share_hash(options)
options[:pretty] ? JSON.pretty_generate(hash) : hash.to_json
end
def to_xml_element(options = {})
prepare_options(options)
unless (xml_doc = options[:xml_doc])
options[:xml_doc] = xml_doc = Nokogiri::XML::Document.new
end
element = record_to_xml_element(data_type = self_record.orm_model.data_type, self_record.orm_model.schema, self_record, xml_doc, nil, options, namespaces = {})
namespaces.each do |ns, xmlns|
if xmlns.empty?
element['xmlns'] = ns unless ns.blank?
else
element["xmlns:#{xmlns}"] = ns
end
end
element
end
def to_xml(options = {})
element = to_xml_element(options)
options[:xml_doc] << element
options[:xml_doc].to_xml(options)
end
alias_method :inspect_json, :to_hash
private
def prepare_options(options)
include_id = options[:include_id]
[:ignore, :only, :embedding, :inspecting, :including].each do |option|
value = (options[option] || [])
value = [value] unless value.is_a?(Enumerable)
value = value.select { |p| p.is_a?(Symbol) || p.is_a?(String) }.collect(&:to_sym)
options[option] = value
include_id ||= (value.include?(:id) || value.include?(:_id)) if include_id.nil? && option != :ignore
end
[:only].each { |option| options.delete(option) if options[option].empty? }
options[:stack] = []
options[:include_id] = include_id.respond_to?(:call) ? include_id : include_id.to_b
if (viewport = options[:viewport])
if viewport.is_a?(Hash)
viewport.stringify_keys!
else
viewport =
begin
JSON.parse(viewport.to_s)
rescue
parse_viewport(viewport.to_s)
end
end
options[:viewport] = viewport
end
options.keys.each do |option|
if option.is_a?(String)
options[option.to_sym] = options.delete(option)
end
end
end
def parse_viewport(value)
value = value.to_s.split(' ').map do |seq|
seq.chars.inject([]) do |a, char|
case char
when '{', '}'
a << char
else
case (last = a.pop)
when '{', '}'
a << last
a << char
else
a << (last || '') + char
end
end
a
end
end.flatten
first = value.shift
if first == '{'
hash = {}
parse_viewport_seq(value, hash)
unless value.empty?
raise "Unexpected token '#{value.shift}'"
end
hash
else
raise "Unexpected token '#{first}'"
end
end
def parse_viewport_seq(seq, hash)
previous_token = nil
until seq.empty?
case (token = seq.shift)
when '{'
if previous_token
hash[previous_token] = token_hash = {}
parse_viewport_seq(seq, token_hash)
else
raise "Unexpected token '{'"
end
when '}'
break
else
hash[token] = true
previous_token = token
end
end
end
def split_name(name)
name = (tokens = name.split(':')).pop
[tokens.join(':'), name]
end
def ns_prefix_for(ns, namespaces, preferred = nil)
letters = true
ns = preferred || ns.split(':').last.split('/').last.underscore.split('_').collect { |token| (letters &&= token[0] =~ /[[:alpha:]]/) ? token[0] : '' }.join
ns = 'ns' if ns.blank?
if namespaces.values.include?(ns)
i = 1
while namespaces.values.include?(nns = ns + i.to_s)
i += 1
end
ns = nns
end
ns
end
def record_to_xml_element(data_type, schema, record, xml_doc, enclosed_property_name, options, namespaces)
return unless record
if Cenit::Utility.json_object?(record)
return Nokogiri::XML({ enclosed_property_name => record }.to_xml(dasherize: false)).root.first_element_child
end
if schema['xml'] && (xmlnss = schema['xml']['xmlns']).is_a?(Hash)
xmlnss.each do |ns, xmlns|
namespaces[ns] = ns_prefix_for(ns, namespaces, xmlns) unless namespaces.has_key?(ns)
end
end
element_name = schema['edi']['segment'] if schema['edi']
element_name ||= enclosed_property_name || record.orm_model.data_type.name
ns, element_name = split_name(element_name)
xmlns = ''
unless ns.empty? || (xmlns = namespaces[ns])
xmlns = namespaces[ns] =
if namespaces.values.include?('')
ns_prefix_for(ns, namespaces)
else
''
end
end
element_name = xmlns.empty? ? element_name : xmlns + ':' + element_name
required = schema['required'] || []
attr = {}
elements = []
content = nil
content_property = nil
record.orm_model.properties_schemas.each do |property_name, property_schema|
property_schema = data_type.merge_schema(property_schema)
name = property_schema['edi']['segment'] if property_schema['edi']
name ||= property_name
property_model = record.orm_model.property_model(property_name)
if (inspecting = options[:inspecting].present?) #TODO Factorize for all format formatting
next unless (property_model || inspecting.include?(name.to_sym))
else
next if property_schema['virtual'] ||
((property_schema['edi'] || {})['discard'] && !(included_anyway = options[:including_discards])) ||
options[:ignore].include?(name.to_sym) ||
(options[:only] && !options[:only].include?(name.to_sym) && !included_anyway)
end
case property_schema['type']
when 'array'
property_value = record.send(property_name)
xml_opts = property_schema['xml'] || {}
if xml_opts['attribute']
property_value = property_value && property_value.collect(&:to_s).join(' ')
attr[name] = property_value if !property_value.blank? || options[:with_blanks] || required.include?(property_name)
elsif xml_opts['simple_type']
elements << (e = xml_doc.create_element(name))
e << property_value && property_value.collect(&:to_s).join(' ')
elsif property_model&.modelable?
property_schema = data_type.merge_schema(property_schema['items'] || {})
json_objects = []
property_value.each do |sub_record|
if Cenit::Utility.json_object?(sub_record)
json_objects << sub_record
else
elements << record_to_xml_element(data_type, property_schema, sub_record, xml_doc, nil, options, namespaces)
end
end if property_value
unless json_objects.empty?
elements << Nokogiri::XML({ property_name => json_objects }.to_xml(dasherize: false)).root.first_element_child
end
else
elements << Nokogiri::XML({ name => property_value }.to_xml(dasherize: false)).root.first_element_child
end
when 'object'
if property_model&.modelable?
elements << record_to_xml_element(data_type, property_schema, record.send(property_name), xml_doc, nil, options, namespaces)
else
elements << Nokogiri::XML({ name => record.send(property_name) }.to_xml(dasherize: false)).root.first_element_child
end
else
value = property_schema['default'] if (value = record.send(property_name)).nil?
unless value.nil?
xml_opts = property_schema['xml'] || {}
if xml_opts['attribute']
attr[name] = value if !value.nil? || options[:with_blanks] || required.include?(property_name)
elsif xml_opts['content']
if content.nil?
content = value
content_property = property_name
else
raise Exception.new("More than one content property found: '#{content_property}' and '#{property_name}'")
end
else
elements << Nokogiri::XML({ name => value }.to_xml(dasherize: false)).root.first_element_child
end
end
end
end
element = xml_doc.create_element(element_name, attr)
if elements.empty?
content =
case content
when NilClass
[]
when Hash
Nokogiri::XML(content.to_xml).root.element_children
else
[json_value(content, options, schema).to_s]
end
content.each { |e| element << e }
else
raise Exception.new("Incompatible content property ('#{content_property}') in presence of complex content") if content_property
elements.each { |e| element << e if e }
end
element
end
def record_to_hash(record, options, referenced, enclosed_model, max_entries, viewport)
return record if Cenit::Utility.json_object?(record)
model =
begin
record.orm_model
rescue
nil
end
return nil unless model
schema = model.schema
key_properties = schema['referenced_by']&.dup || (options[:with_references] && ['_id']) || []
json =
if viewport.nil? && key_properties.present?
if referenced
{ '_reference' => true }
else
{ '_primary' => key_properties }
end
else
referenced = false
{}
end
if !referenced && options[:stack].include?(record)
result = { '_reference' => true }
do_store(result, 'id', record.id, {})
return result
end
options[:stack] << record
if (include_id = options[:include_id]).respond_to?(:call)
include_id = include_id.call(record)
end
if include_id || (viewport && (viewport['id'] || viewport['_id']))
entries = do_store(json, options[:raw_properties] ? '_id' : 'id', record.id, options)
max_entries -= entries if max_entries
end
content_property = nil
model.stored_properties_on(record).each do |property_name|
break if max_entries && max_entries < 1
if (protected = (model.schema['protected'] || []).include?(property_name)) && options[:protected]
key_properties.delete(property_name)
next
end
property_schema = model.property_schema(property_name) || {}
property_model = model.property_model(property_name)
name = property_name
if !options[:raw_properties] && property_schema['edi']
name = property_schema['edi']['segment'] || name
end
if property_schema['type'] != 'object' && (schema['properties'].size == 1 || (property_schema['xml'] && property_schema['xml']['content']))
content_property = name
end
can_be_referenced = !(options[:embedding_all] || options[:embedding].include?(name.to_sym))
if viewport.is_a?(Hash)
next unless viewport[name]
elsif (inspecting = options[:inspecting].present?)
unless (property_model || options[:inspecting].include?(name.to_sym)) && (!referenced || key_properties.include?(property_name))
key_properties.delete(property_name)
next
end
else
if property_schema['virtual'] ||
((property_schema['edi'] || {})['discard'] && !(included_anyway = options[:including_discards] || options[:including].include?(property_name.to_sym))) ||
(can_be_referenced && referenced && !key_properties.include?(property_name)) ||
options[:ignore].include?(name.to_sym) ||
(options[:only].present? && options[:only].exclude?(name.to_sym) && !included_anyway)
key_properties.delete(property_name)
next
end
end
property_viewport = viewport.is_a?(Hash) && viewport[name]
unless property_viewport.nil?
property_viewport = nil unless property_viewport.is_a?(Hash)
end
if name != property_name
key_properties.each_with_index do |p, i|
if p == property_name
key_properties[i] = name
break
end
end
end
case property_schema['type']
when 'array'
referenced_items = can_be_referenced && property_schema['referenced'] && !property_schema['export_embedded']
if (value = record.send(property_name)) && !value.try(:null?)
next if max_entries && value.size > max_entries
sub_max_entries = max_entries && (max_entries - value.size)
sub_max_entries = 1 unless sub_max_entries.nil? || sub_max_entries.positive?
new_value = []
value.each do |sub_record|
next if inspecting && (scope = options[:inspect_scope]) && !scope.include?(sub_record)
new_value << record_to_hash(
sub_record,
options,
referenced_items,
property_model,
sub_max_entries,
property_viewport
)
end
else
new_value = nil
sub_max_entries = max_entries
end
do_store(json, name, new_value, options, property_schema, key_properties.include?(property_name))
max_entries = sub_max_entries
when 'object'
sub_record = record.send(property_name)
next if inspecting && (scope = options[:inspect_scope]) && !scope.include?(sub_record)
value = record_to_hash(
sub_record,
options,
can_be_referenced && property_schema['referenced'] && !property_schema['export_embedded'],
property_model,
max_entries && max_entries - 1,
property_viewport
)
entries = do_store(json, name, value, options, property_schema, key_properties.include?(property_name))
max_entries -= entries if max_entries
else
begin
if (value = record.send(property_name)).nil?
value = (protected ? nil : record[property_name])
end
rescue
value = nil
end
if value.nil?
value = property_schema['default']
end
entries = do_store(json, name, value, options, property_schema, key_properties.include?(property_name)) #TODO Default values should came from record attributes
max_entries -= entries if max_entries
end
end
if (options[:inspecting].include?(:_type) ||
options[:including].include?(:_type) ||
(enclosed_model && !record.orm_model.eql?(enclosed_model)) ||
(options[:polymorphic] && record.orm_model.hereditary?)) && !json['_reference'] && !options[:ignore].include?(:_type) && (!options[:only] || options[:only].include?(:_type))
json['_type'] = model.to_s
end
options[:stack].pop
if content_property && json.size == 1 && options[:inline_content] && json.has_key?(content_property) && !json[content_property].is_a?(Hash)
json[content_property]
else
if json.key?('id') || json.key?('_id')
json.delete('_primary')
elsif key_properties.include?('id') || key_properties.include?('_id')
key_properties.delete('id')
key_properties.delete('_id')
json.delete('_primary') if key_properties.empty?
end
json
end
end
def do_store(json, key, value, options, schema = {}, store_anyway = false)
if options[:nqnames]
key = key.to_s.split(':').last
end
if value.nil?
if store_anyway || options[:include_null]
k = json.key?(key) ? 0 : 1
json[key] = nil
k
else
0
end
elsif value.is_a?(Array) || value.is_a?(Hash)
if store_anyway || value.present? || options[:include_blanks] || options[:include_empty]
k = json.key?(key) ? 0 : value.size
json[key] = value
k
else
0
end
else
value = value.to_s if [BSON::ObjectId, Symbol].any? { |klass| value.is_a?(klass) }
value = json_value(value, options, schema)
if store_anyway || !(value.nil? || value.try(:empty?)) || options[:include_blanks] #TODO String blanks!
k = json.key?(key) ? 0 : 1
json[key] = value
k
else
0
end
end
end
def json_value(value, options, schema)
case value
when Time, Date, DateTime
value = value.to_time
if schema && schema['format'] == 'time'
value.strftime('%H:%M:%S.%L%:z')
else
value.iso8601
end
when Class, Module
value.to_s
else
if Cenit::Utility.json_object?(value)
value
else
options = options.dup
if (hash = value.try(:to_hash, options))
hash
elsif (json = value.try(:to_json, options))
JSON.parse(json.to_s) rescue json.to_s
else
value.to_s
end
end
end
end
def record_to_edi(data_type, options, schema, record, enclosed_property_name = nil)
output = []
return output unless record
field_sep = options[:field_separator]
segment =
if (edi_options = schema['edi'] || {})['virtual'] || options[:skip_segment_tag]
''
else
edi_options['segment'] ||
if (record_data_type = record.orm_model.data_type) != data_type
record_data_type.name
else
enclosed_property_name || data_type.name
end
end
schema['properties'].each do |property_name, property_schema|
property_schema = data_type.merge_schema(property_schema)
next if property_schema['edi'] && property_schema['edi']['discard']
if (property_model = record.orm_model.property_model(property_name)) && property_model.modelable?
if property_schema['type'] == 'array'
if (sub_values = record.send(property_name))
property_schema = data_type.merge_schema(property_schema['items'])
sub_values.each do |sub_record|
output.concat(record_to_edi(data_type, options, property_schema, sub_record, property_name))
end
end
else
if (sub_record = record.send(property_name))
if property_schema['edi'] && property_schema['edi']['inline']
value = []
property_model.properties_schemas.each do |sub_property_name, sub_property_schema|
value << edi_value(sub_record, sub_property_name, sub_property_schema, sub_record.orm_model.property_model(sub_property_name), options)
end
segment +=
if field_sep == :by_fixed_length
value.join
else
while value.last.blank?
value.pop
end
field_sep + value.join(options[:inline_field_separator])
end
else
output.concat(record_to_edi(data_type, options, property_schema, sub_record, property_name))
end
end
end
else
value = edi_value(record, property_name, property_schema, property_model, options)
segment +=
if field_sep == :by_fixed_length
value
else
field_sep + value
end
end
end
while segment.end_with?(field_sep)
segment = segment.chomp(field_sep)
end
if options[:skip_segment_tag] && segment.start_with?(field_sep)
segment[0] = ''
end
output.unshift(segment) unless edi_options['virtual']
output
end
def edi_value(record, property_name, property_schema, property_model, options)
if (value = record[property_name]).nil?
value = property_schema['default'] || ''
end
value = property_model.to_string(value) if property_model
value =
if (segment_sep = options[:segment_separator]) == :new_line
value.to_s.gsub(/(\n|\r|\r\n)+/, options[:seg_sep_suppress])
else
value.to_s.gsub(segment_sep, options[:seg_sep_suppress])
end
if options[:field_separator] == :by_fixed_length
if (max_len = property_schema['maxLength']) && (auto_fill = property_schema['auto_fill'])
if auto_fill[0] == 'R'
value += auto_fill[1] until value.length == max_len
else #should be 'L'
value = auto_fill[1] + value until value.length == max_len
end
end
end
value
end
end
end
class Hash
def to_params(options = {})
unsafe = options[:unsafe]
sort.map do |k, values|
if values.is_a?(Array)
values << nil if values.empty?
values.sort.collect do |v|
[escape(k, unsafe), escape(v, unsafe)] * '='
end
elsif values.is_a?(Hash)
normalize_nested_query(values, k, unsafe)
else
[escape(k, unsafe), escape(values, unsafe)] * '='
end
end * '&'
end
private
def normalize_nested_query(value, prefix, unsafe)
case value
when Array
value.map do |v|
normalize_nested_query(v, "#{prefix}[]", unsafe)
end.flatten.sort
when Hash
value.map do |k, v|
normalize_nested_query(v, prefix ? "#{prefix}[#{k}]" : k, unsafe)
end.flatten.sort
else
[escape(prefix, unsafe), escape(value, unsafe)] * '='
end
end
def escape(value, unsafe)
URI::escape(value.to_s, unsafe)
rescue ArgumentError
URI::escape(value.to_s.force_encoding(Encoding::UTF_8), unsafe)
end
end