chef-gyoku 1.4.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,96 @@
1
+ module Gyoku
2
+ module Array
3
+ module_function
4
+
5
+ NESTED_ELEMENT_NAME = "element"
6
+
7
+ # Builds XML and prettifies it if +pretty_print+ option is set to +true+
8
+ def to_xml(array, key, escape_xml = true, attributes = {}, options = {})
9
+ xml = build_xml(array, key, escape_xml, attributes, options)
10
+
11
+ if options[:pretty_print] && options[:unwrap]
12
+ Prettifier.prettify(xml, options)
13
+ else
14
+ xml
15
+ end
16
+ end
17
+
18
+ # Translates a given +array+ to XML. Accepts the XML +key+ to add the elements to,
19
+ # whether to +escape_xml+ and an optional Hash of +attributes+.
20
+ def self.build_xml(array, key, escape_xml = true, attributes = {}, options = {})
21
+ self_closing = options.delete(:self_closing)
22
+ unwrap = unwrap?(options.fetch(:unwrap, false), key)
23
+
24
+ iterate_with_xml array, key, attributes, options do |xml, item, attrs, index|
25
+ if self_closing
26
+ xml.tag!(key, attrs)
27
+ else
28
+ case item
29
+ when ::Hash
30
+ if unwrap
31
+ xml << Hash.to_xml(item, options)
32
+ else
33
+ xml.tag!(key, attrs) { xml << Hash.build_xml(item, options) }
34
+ end
35
+ when ::Array
36
+ xml.tag!(key, attrs) { xml << Array.build_xml(item, NESTED_ELEMENT_NAME) }
37
+ when NilClass
38
+ xml.tag!(key, "xsi:nil" => "true")
39
+ else
40
+ xml.tag!(key, attrs) { xml << XMLValue.create(item, escape_xml) }
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ # Iterates over a given +array+ with a Hash of +attributes+ and yields a builder +xml+
47
+ # instance, the current +item+, any XML +attributes+ and the current +index+.
48
+ def iterate_with_xml(array, key, attributes, options, &block)
49
+ xml = Builder::XmlMarkup.new
50
+ unwrap = unwrap?(options.fetch(:unwrap, false), key)
51
+
52
+ if unwrap
53
+ xml.tag!(key, attributes) { iterate_array(xml, array, attributes, &block) }
54
+ else
55
+ iterate_array(xml, array, attributes, &block)
56
+ end
57
+
58
+ xml.target!
59
+ end
60
+ private_class_method :iterate_with_xml
61
+
62
+ # Iterates over a given +array+ with a Hash of +attributes+ and yields a builder +xml+
63
+ # instance, the current +item+, any XML +attributes+ and the current +index+.
64
+ def iterate_array(xml, array, attributes, &block)
65
+ array.each_with_index do |item, index|
66
+ attrs = if item.respond_to?(:keys)
67
+ item.each_with_object({}) do |v, st|
68
+ k = v[0].to_s
69
+ st[k[1..]] = v[1].to_s if Hash.explicit_attribute?(k)
70
+ end
71
+ else
72
+ {}
73
+ end
74
+ yield xml, item, tag_attributes(attributes, index).merge(attrs), index
75
+ end
76
+ end
77
+ private_class_method :iterate_array
78
+
79
+ # Takes a Hash of +attributes+ and the +index+ for which to return attributes
80
+ # for duplicate tags.
81
+ def tag_attributes(attributes, index)
82
+ return {} if attributes.empty?
83
+
84
+ attributes.inject({}) do |hash, (key, value)|
85
+ value = value[index] if value.is_a? ::Array
86
+ value ? hash.merge(key => value) : hash
87
+ end
88
+ end
89
+ private_class_method :tag_attributes
90
+
91
+ def unwrap?(unwrap, key)
92
+ unwrap.is_a?(::Array) ? unwrap.include?(key.to_sym) : unwrap
93
+ end
94
+ private_class_method :unwrap?
95
+ end
96
+ end
data/lib/gyoku/hash.rb ADDED
@@ -0,0 +1,105 @@
1
+ require "builder"
2
+ require "gyoku/prettifier"
3
+ require "gyoku/array"
4
+ require "gyoku/xml_key"
5
+ require "gyoku/xml_value"
6
+
7
+ module Gyoku
8
+ module Hash
9
+ module_function
10
+
11
+ # Builds XML and prettifies it if +pretty_print+ option is set to +true+
12
+ def to_xml(hash, options = {})
13
+ xml = build_xml(hash, options)
14
+
15
+ if options[:pretty_print]
16
+ Prettifier.prettify(xml, options)
17
+ else
18
+ xml
19
+ end
20
+ end
21
+
22
+ # Translates a given +hash+ with +options+ to XML.
23
+ def build_xml(hash, options = {})
24
+ iterate_with_xml hash do |xml, key, value, attributes|
25
+ self_closing = key.to_s[-1, 1] == "/"
26
+ escape_xml = key.to_s[-1, 1] != "!"
27
+ xml_key = XMLKey.create key, options
28
+
29
+ if :content! === key
30
+ xml << XMLValue.create(value, escape_xml, options)
31
+ elsif ::Array === value
32
+ xml << Array.build_xml(value, xml_key, escape_xml, attributes, options.merge(self_closing: self_closing))
33
+ elsif ::Hash === value
34
+ xml.tag!(xml_key, attributes) { xml << build_xml(value, options) }
35
+ elsif self_closing
36
+ xml.tag!(xml_key, attributes)
37
+ elsif NilClass === value
38
+ xml.tag!(xml_key, "xsi:nil" => "true")
39
+ else
40
+ xml.tag!(xml_key, attributes) { xml << XMLValue.create(value, escape_xml, options) }
41
+ end
42
+ end
43
+ end
44
+
45
+ def explicit_attribute?(key)
46
+ key.to_s.start_with?("@")
47
+ end
48
+
49
+ # Iterates over a given +hash+ and yields a builder +xml+ instance, the current
50
+ # Hash +key+ and any XML +attributes+.
51
+ #
52
+ # Keys beginning with "@" are treated as explicit attributes for their container.
53
+ # You can use both :attributes! and "@" keys to specify attributes.
54
+ # In the event of a conflict, the "@" key takes precedence.
55
+ def iterate_with_xml(hash)
56
+ xml = Builder::XmlMarkup.new
57
+ attributes = hash[:attributes!] || {}
58
+ hash_without_attributes = hash.except(:attributes!)
59
+
60
+ order(hash_without_attributes).each do |key|
61
+ node_attr = attributes[key] || {}
62
+ # node_attr must be kind of ActiveSupport::HashWithIndifferentAccess
63
+ node_attr = node_attr.map { |k, v| [k.to_s, v] }.to_h
64
+ node_value = hash[key].respond_to?(:keys) ? hash[key].clone : hash[key]
65
+
66
+ if node_value.respond_to?(:keys)
67
+ explicit_keys = node_value.keys.select { |k| explicit_attribute?(k) }
68
+ explicit_attr = {}
69
+ explicit_keys.each { |k| explicit_attr[k.to_s[1..]] = node_value[k] }
70
+ node_attr.merge!(explicit_attr)
71
+ explicit_keys.each { |k| node_value.delete(k) }
72
+
73
+ tmp_node_value = node_value.delete(:content!)
74
+ node_value = tmp_node_value unless tmp_node_value.nil?
75
+ node_value = "" if node_value.respond_to?(:empty?) && node_value.empty?
76
+ end
77
+
78
+ yield xml, key, node_value, node_attr
79
+ end
80
+
81
+ xml.target!
82
+ end
83
+ private_class_method :iterate_with_xml
84
+
85
+ # Deletes and returns an Array of keys stored under the :order! key of a given +hash+.
86
+ # Defaults to return the actual keys of the Hash if no :order! key could be found.
87
+ # Raises an ArgumentError in case the :order! Array does not match the Hash keys.
88
+ def order(hash)
89
+ order = hash[:order!] || hash.delete("order!")
90
+ hash_without_order = hash.except(:order!)
91
+ order = hash_without_order.keys unless order.is_a? ::Array
92
+
93
+ # Ignore Explicit Attributes
94
+ orderable = order.delete_if { |k| explicit_attribute?(k) }
95
+ hashable = hash_without_order.keys.select { |k| !explicit_attribute?(k) }
96
+
97
+ missing, spurious = hashable - orderable, orderable - hashable
98
+ raise ArgumentError, "Missing elements in :order! #{missing.inspect}" unless missing.empty?
99
+ raise ArgumentError, "Spurious elements in :order! #{spurious.inspect}" unless spurious.empty?
100
+
101
+ order
102
+ end
103
+ private_class_method :order
104
+ end
105
+ end
@@ -0,0 +1,29 @@
1
+ require "rexml/document"
2
+
3
+ module Gyoku
4
+ class Prettifier
5
+ DEFAULT_INDENT = 2
6
+ DEFAULT_COMPACT = true
7
+
8
+ attr_accessor :indent, :compact
9
+
10
+ def self.prettify(xml, options = {})
11
+ new(options).prettify(xml)
12
+ end
13
+
14
+ def initialize(options = {})
15
+ @indent = options[:indent] || DEFAULT_INDENT
16
+ @compact = options[:compact].nil? ? DEFAULT_COMPACT : options[:compact]
17
+ end
18
+
19
+ # Adds intendations and newlines to +xml+ to make it more readable
20
+ def prettify(xml)
21
+ result = ""
22
+ formatter = REXML::Formatters::Pretty.new indent
23
+ formatter.compact = compact
24
+ doc = REXML::Document.new xml
25
+ formatter.write doc, result
26
+ result
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,3 @@
1
+ module Gyoku
2
+ VERSION = "1.4.1"
3
+ end
@@ -0,0 +1,67 @@
1
+ module Gyoku
2
+ module XMLKey
3
+ class << self
4
+ CAMELCASE = lambda { |key| key.gsub(/\/(.?)/) { |m| "::#{m[-1].upcase}" }.gsub(/(?:^|_)(.)/) { |m| m[-1].upcase } }
5
+ LOWER_CAMELCASE = lambda { |key| key[0].chr.downcase + CAMELCASE.call(key)[1..] }
6
+ UPCASE = lambda { |key| key.upcase }
7
+
8
+ FORMULAS = {
9
+ lower_camelcase: lambda { |key| LOWER_CAMELCASE.call(key) },
10
+ camelcase: lambda { |key| CAMELCASE.call(key) },
11
+ upcase: lambda { |key| UPCASE.call(key) },
12
+ none: lambda { |key| key }
13
+ }
14
+
15
+ # Converts a given +object+ with +options+ to an XML key.
16
+ def create(key, options = {})
17
+ xml_key = chop_special_characters key.to_s
18
+
19
+ if unqualified = unqualify?(xml_key)
20
+ xml_key = xml_key.split(":").last
21
+ end
22
+
23
+ xml_key = key_converter(options, xml_key).call(xml_key) if Symbol === key
24
+
25
+ if !unqualified && qualify?(options) && !xml_key.include?(":")
26
+ xml_key = "#{options[:namespace]}:#{xml_key}"
27
+ end
28
+
29
+ xml_key
30
+ end
31
+
32
+ private
33
+
34
+ # Returns the formula for converting Symbol keys.
35
+ def key_converter(options, xml_key)
36
+ return options[:key_converter] if options[:key_converter].is_a? Proc
37
+
38
+ defined_key = options[:key_to_convert]
39
+ key_converter = if !defined_key.nil? && (defined_key == xml_key)
40
+ options[:key_converter]
41
+ elsif !defined_key.nil?
42
+ :lower_camelcase
43
+ elsif options[:except] == xml_key
44
+ :lower_camelcase
45
+ else
46
+ options[:key_converter] || :lower_camelcase
47
+ end
48
+ FORMULAS[key_converter]
49
+ end
50
+
51
+ # Chops special characters from the end of a given +string+.
52
+ def chop_special_characters(string)
53
+ ["!", "/"].include?(string[-1, 1]) ? string.chop : string
54
+ end
55
+
56
+ # Returns whether to remove the namespace from a given +key+.
57
+ def unqualify?(key)
58
+ key[0, 1] == ":"
59
+ end
60
+
61
+ # Returns whether to namespace all keys (elementFormDefault).
62
+ def qualify?(options)
63
+ options[:element_form_default] == :qualified && options[:namespace]
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,41 @@
1
+ require "cgi"
2
+ require "date"
3
+
4
+ module Gyoku
5
+ module XMLValue
6
+ class << self
7
+ # xs:date format
8
+ XS_DATE_FORMAT = "%Y-%m-%d"
9
+
10
+ # xs:time format
11
+ XS_TIME_FORMAT = "%H:%M:%S"
12
+
13
+ # xs:dateTime format
14
+ XS_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S%Z"
15
+
16
+ # Converts a given +object+ to an XML value.
17
+ def create(object, escape_xml = true, options = {})
18
+ case object
19
+ when Time
20
+ object.strftime XS_TIME_FORMAT
21
+ when DateTime
22
+ object.strftime XS_DATETIME_FORMAT
23
+ when Date
24
+ object.strftime XS_DATE_FORMAT
25
+ when String
26
+ escape_xml ? CGI.escapeHTML(object) : object
27
+ when ::Hash
28
+ Gyoku::Hash.to_xml(object, options)
29
+ else
30
+ if object.respond_to?(:to_datetime)
31
+ create object.to_datetime
32
+ elsif object.respond_to?(:call)
33
+ create object.call
34
+ else
35
+ object.to_s
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
data/lib/gyoku.rb ADDED
@@ -0,0 +1,14 @@
1
+ require "gyoku/version"
2
+ require "gyoku/hash"
3
+
4
+ module Gyoku
5
+ # Converts a given Hash +key+ with +options+ into an XML tag.
6
+ def self.xml_tag(key, options = {})
7
+ XMLKey.create(key, options)
8
+ end
9
+
10
+ # Translates a given +hash+ with +options+ to XML.
11
+ def self.xml(hash, options = {})
12
+ Hash.to_xml hash.dup, options
13
+ end
14
+ end
@@ -0,0 +1,121 @@
1
+ require "spec_helper"
2
+
3
+ describe Gyoku::Array do
4
+ describe ".to_xml" do
5
+ it "returns the XML for an Array of Hashes" do
6
+ array = [{name: "adam"}, {name: "eve"}]
7
+ result = "<user><name>adam</name></user><user><name>eve</name></user>"
8
+
9
+ expect(to_xml(array, "user")).to eq(result)
10
+ end
11
+
12
+ it "returns the XML for an Array of Hashes unwrapped" do
13
+ array = [{name: "adam"}, {name: "eve"}]
14
+ result = "<user><name>adam</name><name>eve</name></user>"
15
+
16
+ expect(to_xml(array, "user", true, {}, unwrap: true)).to eq(result)
17
+ end
18
+
19
+ it "returns the XML for an Array of different Objects" do
20
+ array = [:symbol, "string", 123]
21
+ result = "<value>symbol</value><value>string</value><value>123</value>"
22
+
23
+ expect(to_xml(array, "value")).to eq(result)
24
+ end
25
+
26
+ it "defaults to escape special characters" do
27
+ array = ["<tag />", "adam & eve"]
28
+ result = "<value>&lt;tag /&gt;</value><value>adam &amp; eve</value>"
29
+
30
+ expect(to_xml(array, "value")).to eq(result)
31
+ end
32
+
33
+ it "does not escape special characters when told to" do
34
+ array = ["<tag />", "adam & eve"]
35
+ result = "<value><tag /></value><value>adam & eve</value>"
36
+
37
+ expect(to_xml(array, "value", false)).to eq(result)
38
+ end
39
+
40
+ it "adds attributes to a given tag" do
41
+ array = ["adam", "eve"]
42
+ result = '<value active="true">adam</value><value active="true">eve</value>'
43
+
44
+ expect(to_xml(array, "value", :escape_xml, active: true)).to eq(result)
45
+ end
46
+
47
+ it "adds attributes to tags when :unwrap is true" do
48
+ array = [{item: "abc"}]
49
+ key = "items"
50
+ escape_xml = :escape_xml
51
+ attributes = {"amount" => "1"}
52
+ options = {unwrap: true}
53
+ result = "<items amount=\"1\"><item>abc</item></items>"
54
+
55
+ expect(to_xml(array, key, escape_xml, attributes, options)).to eq result
56
+ end
57
+
58
+ it "adds attributes to duplicate tags" do
59
+ array = ["adam", "eve"]
60
+ result = '<value id="1">adam</value><value id="2">eve</value>'
61
+
62
+ expect(to_xml(array, "value", :escape_xml, id: [1, 2])).to eq(result)
63
+ end
64
+
65
+ it "skips attribute for element without attributes if there are fewer attributes than elements" do
66
+ array = ["adam", "eve", "serpent"]
67
+ result = '<value id="1">adam</value><value id="2">eve</value><value>serpent</value>'
68
+
69
+ expect(to_xml(array, "value", :escape_xml, id: [1, 2])).to eq(result)
70
+ end
71
+
72
+ it "handles nested Arrays" do
73
+ array = [["one", "two"]]
74
+ result = "<value><element>one</element><element>two</element></value>"
75
+
76
+ expect(to_xml(array, "value")).to eq(result)
77
+ end
78
+
79
+ context "when :pretty_print option is set to true" do
80
+ context "when :unwrap option is set to true" do
81
+ it "returns prettified xml" do
82
+ array = ["one", "two", {"three" => "four"}]
83
+ options = {pretty_print: true, unwrap: true}
84
+ result = "<test>\n <test>one</test>\n <test>two</test>\n <three>four</three>\n</test>"
85
+ expect(to_xml(array, "test", true, {}, options)).to eq(result)
86
+ end
87
+
88
+ context "when :indent option is specified" do
89
+ it "returns prettified xml with specified indent" do
90
+ array = ["one", "two", {"three" => "four"}]
91
+ options = {pretty_print: true, indent: 3, unwrap: true}
92
+ result = "<test>\n <test>one</test>\n <test>two</test>\n <three>four</three>\n</test>"
93
+ expect(to_xml(array, "test", true, {}, options)).to eq(result)
94
+ end
95
+ end
96
+
97
+ context "when :compact option is specified" do
98
+ it "returns prettified xml with specified compact mode" do
99
+ array = ["one", {"two" => "three"}]
100
+ options = {pretty_print: true, compact: false, unwrap: true}
101
+ result = "<test>\n <test>\n one\n </test>\n <two>\n three \n </two>\n</test>"
102
+ expect(to_xml(array, "test", true, {}, options)).to eq(result)
103
+ end
104
+ end
105
+ end
106
+
107
+ context "when :unwrap option is not set" do
108
+ it "returns non-prettified xml" do
109
+ array = ["one", "two", {"three" => "four"}]
110
+ options = {pretty_print: true}
111
+ result = "<test>one</test><test>two</test><test><three>four</three></test>"
112
+ expect(to_xml(array, "test", true, {}, options)).to eq(result)
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ def to_xml(*args)
119
+ Gyoku::Array.to_xml(*args)
120
+ end
121
+ end