mn-requirements 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,89 @@
1
+ module Metanorma
2
+ class Requirements
3
+ class Modspec < Default
4
+ def requirement_type_cleanup(reqt)
5
+ reqt["type"] = case reqt["type"]
6
+ when "requirement", "recommendation", "permission"
7
+ "general"
8
+ when "requirements_class" then "class"
9
+ when "conformance_test" then "verification"
10
+ when "conformance_class" then "conformanceclass"
11
+ when "abstract_test" then "abstracttest"
12
+ else reqt["type"]
13
+ end
14
+ end
15
+
16
+ def requirement_metadata_component_tags
17
+ %w(test-purpose test-method test-method-type conditions part description
18
+ reference step requirement permission recommendation)
19
+ end
20
+
21
+ def requirement_metadata1(reqt, dlist, ins)
22
+ ins1 = super
23
+ dlist.xpath("./dt").each do |e|
24
+ tag = e&.text&.gsub(/ /, "-")&.downcase
25
+ next unless requirement_metadata_component_tags.include? tag
26
+
27
+ ins1.next = requirement_metadata1_component(e, tag)
28
+ ins1 = ins1.next
29
+ end
30
+ end
31
+
32
+ def requirement_metadata1_component(term, tag)
33
+ val = term.at("./following::dd")
34
+ val.name = tag
35
+ val.xpath("./dl").each do |d|
36
+ requirement_metadata1(val, d, d)
37
+ d.remove
38
+ end
39
+ if REQS.include?(term.text) && !val.text.empty?
40
+ val["label"] = val.text.strip
41
+ val.children.remove
42
+ end
43
+ val
44
+ end
45
+
46
+ # separate from default model requirement_metadata_cleanup,
47
+ # which extracts model:: ogc into reqt["model"]
48
+ def requirement_metadata_cleanup(reqt)
49
+ super
50
+ requirement_metadata_to_component(reqt)
51
+ requirement_metadata_to_requirement(reqt)
52
+ requirement_subparts_to_blocks(reqt)
53
+ requirement_target_identifiers(reqt)
54
+ end
55
+
56
+ def requirement_target_identifiers(reqt)
57
+ reqt.xpath("./classification[tag = 'target']/value[link]").each do |v|
58
+ v.children = v.at("./link/@target").text
59
+ end
60
+ end
61
+
62
+ def requirement_metadata_to_component(reqt)
63
+ reqt.xpath(".//test-method | .//test-purpose | .//conditions | "\
64
+ ".//part | .//test-method-type | .//step | .//reference")
65
+ .each do |c|
66
+ c["class"] = c.name
67
+ c.name = "component"
68
+ end
69
+ end
70
+
71
+ def requirement_metadata_to_requirement(reqt)
72
+ reqt.xpath("./requirement | ./permission | ./recommendation")
73
+ .each do |c|
74
+ c["id"] = Metanorma::Utils::anchor_or_uuid
75
+ c["model"] = reqt["model"] # all requirements must have a model
76
+ end
77
+ end
78
+
79
+ def requirement_subparts_to_blocks(reqt)
80
+ reqt.xpath(".//component | .//description").each do |c|
81
+ next if %w(p ol ul dl table component description)
82
+ .include?(c&.elements&.first&.name)
83
+
84
+ c.children = "<p>#{c.children.to_xml}</p>"
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,238 @@
1
+ require_relative "xrefs"
2
+ require_relative "reqt_label"
3
+
4
+ module Metanorma
5
+ class Requirements
6
+ class Modspec < Default
7
+ def requirement_render1(node)
8
+ requirement_table_cleanup(super)
9
+ end
10
+
11
+ def recommendation_base(node, klass)
12
+ out = node.document.create_element("table")
13
+ out.default_namespace = node.namespace.href
14
+ %w(id keep-with-next keep-lines-together unnumbered).each do |x|
15
+ out[x] = node[x] if node[x]
16
+ end
17
+ out["class"] = klass
18
+ out["type"] = recommend_class(node)
19
+ recommendation_component_labels(node)
20
+ out
21
+ end
22
+
23
+ def recommendation_component_labels(node)
24
+ node.xpath(ns("./component[@class = 'part']")).each_with_index do |c, i|
25
+ c["label"] = (i + "A".ord).chr.to_s
26
+ end
27
+ node.xpath(ns("./component[not(@class = 'part')]")).each do |c|
28
+ c["label"] = recommend_component_label(c)
29
+ end
30
+ end
31
+
32
+ def recommendation_header(recommend, out)
33
+ h = out.add_child("<thead><tr><th scope='colgroup' colspan='2'>"\
34
+ "</th></tr></thead>").first
35
+ recommendation_name(recommend, h.at(ns(".//th")))
36
+ out
37
+ end
38
+
39
+ def recommendation_name(node, out)
40
+ b = out.add_child("<p class='#{recommend_name_class(node)}'></p>").first
41
+ name = node.at(ns("./name")) and name.children.each do |n|
42
+ b << n
43
+ end
44
+ title = node.at(ns("./title"))
45
+ return unless title &&
46
+ node.ancestors("requirement, recommendation, permission").empty?
47
+
48
+ b << l10n(": ") if name
49
+ title.children.each { |n| b << n }
50
+ end
51
+
52
+ def recommendation_attributes(node, out)
53
+ ins = out.add_child("<tbody></tbody>").first
54
+ recommend_title(node, ins)
55
+ recommendation_attributes1(node).each do |i|
56
+ ins.add_child("<tr><td>#{i[0]}</td><td>#{i[1]}</td></tr>")
57
+ end
58
+ ins
59
+ end
60
+
61
+ def recommend_title(node, out)
62
+ label = node.at(ns("./identifier")) or return
63
+ b = out.add_child("<tr><td colspan='2'><p></p></td></tr>")
64
+ p = b.at(ns(".//p"))
65
+ p["class"] = "RecommendationLabel"
66
+ p << label.children.to_xml
67
+ end
68
+
69
+ def recommendation_attributes1(node)
70
+ ret = recommendation_attributes1_head(node, [])
71
+ node.xpath(ns("./classification")).each do |c|
72
+ line = recommendation_attr_keyvalue(c, "tag",
73
+ "value") and ret << line
74
+ end
75
+ ret
76
+ end
77
+
78
+ def recommendation_attributes1_head(node, head)
79
+ oblig = node["obligation"] and head << ["Obligation", oblig]
80
+ subj = node.at(ns("./subject"))&.children and
81
+ head << [rec_subj(node), subj]
82
+ node.xpath(ns("./classification[tag = 'target']/value")).each do |v|
83
+ xref = recommendation_id(node.document, v.text) and head << [
84
+ rec_target(node), xref
85
+ ]
86
+ end
87
+ %w(general class).include?(node["type"]) and
88
+ xref = recommendation_link(node.document,
89
+ node.at(ns("./identifier"))&.text) and
90
+ head << ["Conformance test", xref]
91
+ recommendation_attributes1_dependencies(node, head)
92
+ end
93
+
94
+ def recommendation_attributes1_dependencies(node, head)
95
+ node.xpath(ns("./inherit")).each do |i|
96
+ head << ["Dependency",
97
+ recommendation_id(node.document, i.children.to_xml)]
98
+ end
99
+ node.xpath(ns("./classification[tag = 'indirect-dependency']/value"))
100
+ .each do |v|
101
+ xref = recommendation_id(node.document, v.children.to_xml) and
102
+ head << ["Indirect Dependency", xref]
103
+ end
104
+ head
105
+ end
106
+
107
+ def recommendation_steps(node)
108
+ node.elements.each { |e| recommendation_steps(e) }
109
+ return node unless node.at(ns("./component[@class = 'step']"))
110
+
111
+ d = node.at(ns("./component[@class = 'step']"))
112
+ d = d.replace("<ol class='steps'><li>#{d.children.to_xml}</li></ol>")
113
+ .first
114
+ node.xpath(ns("./component[@class = 'step']")).each do |f|
115
+ f = f.replace("<li>#{f.children.to_xml}</li>").first
116
+ d << f
117
+ end
118
+ node
119
+ end
120
+
121
+ def recommendation_attributes1_component(node, out)
122
+ node = recommendation_steps(node)
123
+ out << "<tr><td>#{node['label']}</td><td>#{node.children}</td></tr>"
124
+ out
125
+ end
126
+
127
+ def recommendation_attr_keyvalue(node, key, value)
128
+ tag = node.at(ns("./#{key}"))
129
+ value = node.at(ns("./#{value}"))
130
+ (tag && value && !%w(target
131
+ indirect-dependency).include?(tag.text)) or
132
+ return nil
133
+ [tag.text.capitalize, value.children]
134
+ end
135
+
136
+ def reqt_component_type(node)
137
+ klass = node.name
138
+ klass == "component" and klass = node["class"]
139
+ "requirement-#{klass}"
140
+ end
141
+
142
+ def preserve_in_nested_table?(node)
143
+ %w(recommendation requirement permission
144
+ table ol dl ul).include?(node.name)
145
+ end
146
+
147
+ def requirement_component_parse(node, out)
148
+ return out if node["exclude"] == "true"
149
+
150
+ node.elements.size == 1 && node.first_element_child.name == "dl" and
151
+ return reqt_dl(node.first_element_child, out)
152
+ node.name == "component" and
153
+ return recommendation_attributes1_component(node, out)
154
+ out.add_child("<tr><td colspan='2'></td></tr>").first
155
+ .at(ns(".//td")) <<
156
+ (preserve_in_nested_table?(node) ? node : node.children)
157
+ out
158
+ end
159
+
160
+ def reqt_dl(node, out)
161
+ node.xpath(ns("./dt")).each do |dt|
162
+ dd = dt.next_element
163
+ dd&.name == "dd" or next
164
+ out.add_child("<tr><td>#{dt.children.to_xml}</td>"\
165
+ "<td>#{dd.children.to_xml}</td></tr>")
166
+ end
167
+ out
168
+ end
169
+
170
+ def requirement_table_cleanup(table)
171
+ return table unless table["type"] == "recommendclass"
172
+
173
+ table.xpath(ns("./tbody/tr/td/table")).each do |t|
174
+ t.xpath(ns("./thead | ./tbody |./tfoot")).each do |x|
175
+ x.replace(x.children)
176
+ end
177
+ (x = t.at(ns("./tr/th[@colspan = '2']"))) &&
178
+ (y = t.at(ns("./tr/td[@colspan = '2']"))) and
179
+ requirement_table_cleanup1(x, y)
180
+ t.parent.parent.replace(t.children)
181
+ end
182
+ table
183
+ end
184
+
185
+ # table nested in table: merge label and caption into a single row
186
+ def requirement_table_cleanup1(outer, inner)
187
+ outer.delete("colspan")
188
+ outer.delete("scope")
189
+ inner.delete("colspan")
190
+ inner.delete("scope")
191
+ outer.name = "td"
192
+ p = outer.at(ns("./p[@class = 'RecommendationTitle']")) and
193
+ p.delete("class")
194
+ outer.parent << inner.dup
195
+ inner.parent.remove
196
+ end
197
+
198
+ def rec_subj(node)
199
+ case node["type"]
200
+ when "class" then "Target type"
201
+ else "Subject"
202
+ end
203
+ end
204
+
205
+ def rec_target(node)
206
+ case node["type"]
207
+ when "class" then "Target type"
208
+ when "conformanceclass" then "Requirements class"
209
+ when "verification", "abstracttest" then "Requirement"
210
+ else "Target"
211
+ end
212
+ end
213
+
214
+ def recommend_class(node)
215
+ case node["type"]
216
+ when "verification", "abstracttest" then "recommendtest"
217
+ when "class", "conformanceclass" then "recommendclass"
218
+ else "recommend"
219
+ end
220
+ end
221
+
222
+ def recommend_name_class(node)
223
+ if %w(verification abstracttest).include?(node["type"])
224
+ "RecommendationTestTitle"
225
+ else "RecommendationTitle"
226
+ end
227
+ end
228
+
229
+ def recommend_component_label(node)
230
+ case node["class"]
231
+ when "test-purpose" then "Test purpose"
232
+ when "test-method" then "Test method"
233
+ else Metanorma::Utils.strict_capitalize_first(node["class"])
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,9 @@
1
+ require_relative "cleanup"
2
+ require_relative "isodoc"
3
+
4
+ module Metanorma
5
+ class Requirements
6
+ class Modspec < Default
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,80 @@
1
+ module Metanorma
2
+ class Requirements
3
+ class Modspec < Default
4
+ def recommendation_label(elem, type, xrefs)
5
+ label = elem.at(ns("./identifier"))&.text
6
+ if inject_crossreference_reqt?(elem, label)
7
+ number = xrefs.anchor(reqtlabels(elem.document, label), :xref, false)
8
+ number.nil? ? type : number
9
+ else
10
+ type = recommendation_class_label(elem)
11
+ super
12
+ end
13
+ end
14
+
15
+ def reqtlabels(doc, label)
16
+ @reqtlabels ||= doc
17
+ .xpath(ns("//requirement | //recommendation | //permission"))
18
+ .each_with_object({}) do |r, m|
19
+ l = r.at(ns("./label"))&.text and m[l] = r["id"]
20
+ end
21
+ @reqtlabels[label]
22
+ end
23
+
24
+ # embedded reqts xref to top level reqts via label lookup
25
+ def inject_crossreference_reqt?(node, label)
26
+ !node.ancestors("requirement, recommendation, permission").empty? &&
27
+ reqtlabels(node.document, label)
28
+ end
29
+
30
+ def recommendation_class_label(node)
31
+ case node["type"]
32
+ when "verification" then @labels["#{node.name}test"]
33
+ when "class" then @labels["#{node.name}class"]
34
+ when "abstracttest" then @labels["abstracttest"]
35
+ when "conformanceclass" then @labels["conformanceclass"]
36
+ else
37
+ case node.name
38
+ when "recommendation" then @labels["recommendation"]
39
+ when "requirement" then @labels["requirement"]
40
+ when "permission" then @labels["permission"]
41
+ end
42
+ end
43
+ end
44
+
45
+ def reqt_ids(docxml)
46
+ docxml.xpath(ns("//requirement | //recommendation | //permission"))
47
+ .each_with_object({}) do |r, m|
48
+ id = r.at(ns("./identifier")) or next
49
+ m[id.text] = r["id"]
50
+ end
51
+ end
52
+
53
+ def reqt_links(docxml)
54
+ docxml.xpath(ns("//requirement | //recommendation | //permission"))
55
+ .each_with_object({}) do |r, m|
56
+ next unless %w(conformanceclass
57
+ verification).include?(r["type"])
58
+
59
+ subj = r.at(ns("./classification[tag = 'target']/value"))
60
+ id = r.at(ns("./identifier"))
61
+ next unless subj && id
62
+
63
+ m[subj.text] = { lbl: id.text, id: r["id"] }
64
+ end
65
+ end
66
+
67
+ def recommendation_link(docxml, ident)
68
+ @reqt_links ||= reqt_links(docxml)
69
+ test = @reqt_links[ident&.strip] or return nil
70
+ "<xref target='#{test[:id]}'>#{test[:lbl]}</xref>"
71
+ end
72
+
73
+ def recommendation_id(docxml, ident)
74
+ @reqt_ids ||= reqt_ids(docxml)
75
+ test = @reqt_ids[ident&.strip] or return ident&.strip
76
+ "<xref target='#{test}'>#{ident.strip}</xref>"
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,66 @@
1
+ module Metanorma
2
+ class Requirements
3
+ class Modspec < Default
4
+ def req_class_paths
5
+ [
6
+ { klass: "permissionclass", label: "permissionclass",
7
+ xpath: "permission[@type = 'class']" },
8
+ { klass: "requirementclass", label: "requirementclass",
9
+ xpath: "requirement[@type = 'class']" },
10
+ { klass: "recommendationclass", label: "recommendationclass",
11
+ xpath: "recommendation[@type = 'class']" },
12
+ { klass: "permissiontest", label: "permissiontest",
13
+ xpath: "permission[@type = 'verification']" },
14
+ { klass: "recommendationtest", label: "recommendationtest",
15
+ xpath: "recommendation[@type = 'verification']" },
16
+ { klass: "requirementtest", label: "requirementtest",
17
+ xpath: "requirement[@type = 'verification']" },
18
+ { klass: "abstracttest", label: "abstracttest",
19
+ xpath: "permission[@type = 'abstracttest']" },
20
+ { klass: "abstracttest", label: "abstracttest",
21
+ xpath: "requirement[@type = 'abstracttest']" },
22
+ { klass: "abstracttest", label: "abstracttest",
23
+ xpath: "recommendation[@type = 'abstracttest']" },
24
+ { klass: "conformanceclass", label: "conformanceclass",
25
+ xpath: "permission[@type = 'conformanceclass']" },
26
+ { klass: "conformanceclass", label: "conformanceclass",
27
+ xpath: "requirement[@type = 'conformanceclass']" },
28
+ { klass: "conformanceclass", label: "conformanceclass",
29
+ xpath: "recommendation[@type = 'conformanceclass']" },
30
+ { klass: "permission", label: "permission",
31
+ xpath: "permission[not(@type = 'verification' or @type = 'class' "\
32
+ "or @type = 'abstracttest' or @type = 'conformanceclass')]" },
33
+ { klass: "recommendation", label: "recommendation",
34
+ xpath: "recommendation[not(@type = 'verification' or "\
35
+ "@type = 'class' or @type = 'abstracttest' or "\
36
+ "@type = 'conformanceclass')]" },
37
+ { klass: "requirement", label: "requirement",
38
+ xpath: "requirement[not(@type = 'verification' or @type = 'class' "\
39
+ "or @type = 'abstracttest' or @type = 'conformanceclass')]" },
40
+ ]
41
+ end
42
+
43
+ def req_nested_class_paths
44
+ req_class_paths
45
+ end
46
+
47
+ def permission_parts(block, block_id, label, klass)
48
+ block.xpath(ns("./component[@class = 'part']"))
49
+ .each_with_index.with_object([]) do |(c, i), m|
50
+ next if c["id"].nil? || c["id"].empty?
51
+
52
+ m << { id: c["id"], number: l10n("#{block_id} #{(i + 'A'.ord).chr}"),
53
+ elem: c, label: label, klass: klass }
54
+ end
55
+ end
56
+
57
+ def postprocess_anchor_struct(block, anchor)
58
+ super
59
+ if l = block.at(ns("./identifier"))&.text
60
+ anchor[:xref] += l10n(": ") + "<tt>#{l}</tt>"
61
+ end
62
+ anchor
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,94 @@
1
+ require_relative "../default/default"
2
+ require_relative "../modspec/modspec"
3
+ require "isodoc-i18n"
4
+
5
+ module Metanorma
6
+ class Requirements
7
+ attr_accessor :i18n, :labels
8
+
9
+ def initialize(options)
10
+ @default = options[:default]
11
+ @i18n = ::IsoDoc::I18n.new(options[:lang] || "en",
12
+ options[:script] || "Latn")
13
+ @labels = options[:labels]
14
+ @models = {}
15
+ model_names.each { |k| @models[k] = create(k) }
16
+ end
17
+
18
+ def model_names
19
+ %i[default ogc]
20
+ end
21
+
22
+ # all roles that can be assigned to an example to make it a reqt,
23
+ # across all models (because the model may not be an attribute but
24
+ # embedded in the definition list). Mapped to obligation
25
+ # TODO may need to make it conditional on model
26
+ def requirement_roles
27
+ {
28
+ recommendation: "recommendation",
29
+ requirement: "requirement",
30
+ permission: "permission",
31
+ requirements_class: "requirement",
32
+ conformance_test: "requirement",
33
+ conformance_class: "requirement",
34
+ abstract_test: "requirement",
35
+ }
36
+ end
37
+
38
+ def create(type)
39
+ case type
40
+ when :modspec, :ogc
41
+ Metanorma::Requirements::Modspec.new(parent: self)
42
+ else Metanorma::Requirements::Default.new(parent: self)
43
+ end
44
+ end
45
+
46
+ def model(type)
47
+ @models[type&.to_sym] || @models[@default]
48
+ end
49
+
50
+ REQRECPER = "//requirement | //recommendation | //permission".freeze
51
+
52
+ # all cleanup steps by all possible models are included here, and each model
53
+ # can skip a given step. This class iterates through the entire document,
54
+ # and picks the model for each requirement; then that model's method is
55
+ # applied to that particular requirement instance
56
+ def requirement_cleanup(xmldoc)
57
+ requirement_metadata_cleanup(xmldoc)
58
+ requirement_type_cleanup(xmldoc)
59
+ requirement_inherit_cleanup(xmldoc)
60
+ requirement_descriptions_cleanup(xmldoc)
61
+ requirement_identifier_cleanup(xmldoc)
62
+ end
63
+
64
+ def requirement_type_cleanup(xmldoc)
65
+ xmldoc.xpath(REQRECPER).each do |r|
66
+ model(r["model"]).requirement_type_cleanup(r)
67
+ end
68
+ end
69
+
70
+ def requirement_metadata_cleanup(xmldoc)
71
+ xmldoc.xpath(REQRECPER).each do |r|
72
+ model(r["model"]).requirement_metadata_cleanup(r)
73
+ end
74
+ end
75
+
76
+ def requirement_inherit_cleanup(xmldoc)
77
+ xmldoc.xpath(REQRECPER).each do |r|
78
+ model(r["model"]).requirement_inherit_cleanup(r)
79
+ end
80
+ end
81
+
82
+ def requirement_descriptions_cleanup(xmldoc)
83
+ xmldoc.xpath(REQRECPER).each do |r|
84
+ model(r["model"]).requirement_descriptions_cleanup(r)
85
+ end
86
+ end
87
+
88
+ def requirement_identifier_cleanup(xmldoc)
89
+ xmldoc.xpath(REQRECPER).each do |r|
90
+ model(r["model"]).requirement_identifier_cleanup(r)
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,5 @@
1
+ module Metanorma
2
+ class Requirements
3
+ VERSION = "0.0.1".freeze
4
+ end
5
+ end
@@ -0,0 +1,2 @@
1
+ require_relative "metanorma/requirements/selector"
2
+ require_relative "metanorma/requirements/version"
@@ -0,0 +1,42 @@
1
+ lib = File.expand_path("lib", __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "metanorma/requirements/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "mn-requirements"
7
+ spec.version = Metanorma::Requirements::VERSION
8
+ spec.authors = ["Ribose Inc."]
9
+ spec.email = ["open.source@ribose.com"]
10
+
11
+ spec.summary = "Requirements processing and rendering according to different models"
12
+ spec.description = <<~DESCRIPTION
13
+ Requirements processing and rendering according to different models
14
+ DESCRIPTION
15
+
16
+ spec.homepage = "https://github.com/metanorma/mn-requirements"
17
+ spec.license = "BSD-2-Clause"
18
+
19
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
20
+ f.match(%r{^(test|spec|features)/})
21
+ end
22
+ spec.bindir = "exe"
23
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.require_paths = ["lib"]
25
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
26
+
27
+ spec.add_dependency "isodoc-i18n", "~> 1.0.0"
28
+ spec.add_dependency "metanorma-utils", "~> 1.3.0"
29
+
30
+ spec.add_development_dependency "debug"
31
+ spec.add_development_dependency "equivalent-xml", "~> 0.6"
32
+ spec.add_development_dependency "guard", "~> 2.14"
33
+ spec.add_development_dependency "guard-rspec", "~> 4.7"
34
+ spec.add_development_dependency "isodoc", "~> 2"
35
+ spec.add_development_dependency "metanorma-standoc", "~> 2"
36
+ spec.add_development_dependency "rake", "~> 13.0"
37
+ spec.add_development_dependency "rspec", "~> 3.6"
38
+ spec.add_development_dependency "rubocop", "~> 1.5.2"
39
+ spec.add_development_dependency "sassc", "2.4.0"
40
+ spec.add_development_dependency "simplecov", "~> 0.15"
41
+ spec.add_development_dependency "timecop", "~> 0.9"
42
+ end