chef-gyoku 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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