mn-requirements 0.0.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,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