brief 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +5 -13
  2. data/Gemfile +4 -0
  3. data/Gemfile.lock +81 -0
  4. data/Guardfile +5 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +71 -0
  7. data/Rakefile +11 -0
  8. data/bin/brief +35 -0
  9. data/brief-0.0.1.gem +0 -0
  10. data/brief.gemspec +33 -0
  11. data/examples/project_overview.md +23 -0
  12. data/lib/brief/cli/commands/config.rb +40 -0
  13. data/lib/brief/cli/commands/publish.rb +27 -0
  14. data/lib/brief/cli/commands/write.rb +26 -0
  15. data/lib/brief/configuration.rb +134 -0
  16. data/lib/brief/document.rb +68 -0
  17. data/lib/brief/dsl.rb +0 -0
  18. data/lib/brief/formatters/base.rb +12 -0
  19. data/lib/brief/formatters/github_milestone_rollup.rb +52 -0
  20. data/lib/brief/git.rb +19 -0
  21. data/lib/brief/github/wiki.rb +9 -0
  22. data/lib/brief/github.rb +78 -0
  23. data/lib/brief/github_client/authentication.rb +32 -0
  24. data/lib/brief/github_client/client.rb +86 -0
  25. data/lib/brief/github_client/commands.rb +5 -0
  26. data/lib/brief/github_client/issue_labels.rb +65 -0
  27. data/lib/brief/github_client/issues.rb +22 -0
  28. data/lib/brief/github_client/milestone_issues.rb +13 -0
  29. data/lib/brief/github_client/organization_activity.rb +9 -0
  30. data/lib/brief/github_client/organization_issues.rb +13 -0
  31. data/lib/brief/github_client/organization_repositories.rb +20 -0
  32. data/lib/brief/github_client/organization_users.rb +9 -0
  33. data/lib/brief/github_client/repository_events.rb +8 -0
  34. data/lib/brief/github_client/repository_issue_events.rb +9 -0
  35. data/lib/brief/github_client/repository_issues.rb +8 -0
  36. data/lib/brief/github_client/repository_labels.rb +18 -0
  37. data/lib/brief/github_client/repository_milestones.rb +9 -0
  38. data/lib/brief/github_client/request.rb +181 -0
  39. data/lib/brief/github_client/request_wrapper.rb +121 -0
  40. data/lib/brief/github_client/response_object.rb +50 -0
  41. data/lib/brief/github_client/single_repository.rb +9 -0
  42. data/lib/brief/github_client/user_activity.rb +16 -0
  43. data/lib/brief/github_client/user_gists.rb +9 -0
  44. data/lib/brief/github_client/user_info.rb +9 -0
  45. data/lib/brief/github_client/user_issues.rb +13 -0
  46. data/lib/brief/github_client/user_organizations.rb +9 -0
  47. data/lib/brief/github_client/user_repositories.rb +9 -0
  48. data/lib/brief/github_client.rb +43 -0
  49. data/lib/brief/handlers/base.rb +62 -0
  50. data/lib/brief/handlers/github_issue.rb +41 -0
  51. data/lib/brief/handlers/github_milestone.rb +37 -0
  52. data/lib/brief/handlers/github_wiki.rb +11 -0
  53. data/lib/brief/line.rb +69 -0
  54. data/lib/brief/parser.rb +354 -0
  55. data/lib/brief/publisher/handler_manager.rb +47 -0
  56. data/lib/brief/publisher.rb +142 -0
  57. data/lib/brief/tree.rb +42 -0
  58. data/lib/brief/version.rb +9 -0
  59. data/lib/brief.rb +56 -0
  60. data/lib/core_ext.rb +37 -0
  61. data/spec/fixtures/front_end_tutorial.md +33 -0
  62. data/spec/fixtures/generated/project_overview.json +0 -0
  63. data/spec/fixtures/generator_dsl_example.rb +22 -0
  64. data/spec/fixtures/project_overview.md +48 -0
  65. data/spec/fixtures/sample.md +19 -0
  66. data/spec/lib/brief/document_spec.rb +35 -0
  67. data/spec/lib/brief/dsl_spec.rb +21 -0
  68. data/spec/lib/brief/line_spec.rb +11 -0
  69. data/spec/lib/brief/parser_spec.rb +12 -0
  70. data/spec/spec_helper.rb +25 -0
  71. metadata +231 -9
@@ -0,0 +1,354 @@
1
+ # This is my first attempt at breaking down raw markdown
2
+ # into a tree / hierarchy, so that it can be used to do other
3
+ # things / create other entities.
4
+ #
5
+ module Brief
6
+ class Parser
7
+ attr_accessor :content, :options, :raw_tree, :checksum
8
+
9
+ # Regexes
10
+ HeadingRegex = /^#+/
11
+
12
+ # Exceptions
13
+ MissingHeadings = Class.new(Exception)
14
+ HeadingsNotUnique = Class.new(Exception)
15
+
16
+ def initialize(content="",options={})
17
+ @options = options.dup
18
+ @content = content
19
+ @checksum = options.fetch(:checksum, Digest::MD5.hexdigest(content))
20
+
21
+ scan
22
+ parse
23
+ end
24
+
25
+ def tree
26
+ @tree ||= Brief::Tree.new(tree_nodes.dup.freeze)
27
+ end
28
+
29
+ def items(sorted=true)
30
+ sorted ? tree.items.sort_by(&:sort_index) : tree.items
31
+ end
32
+
33
+ def headings_at_level level, options={}
34
+ as_objects = options.fetch(:as_objects, false)
35
+ items = heading_lines.select {|line| line.level == level }.sort_by(&:sort_index)
36
+ as_objects ? items : items.map(&:content)
37
+ end
38
+
39
+ def next_sibling_of(heading_line)
40
+ if heading_line.is_a?(String)
41
+ heading_line = heading_lines.detect {|h| h.content == heading_line }
42
+ end
43
+
44
+ heading_lines.detect {|l| l.level <= heading_line.level && l.number > heading_line.number }
45
+ end
46
+
47
+ def body_content_for(heading_line)
48
+ if heading_line.is_a?(String)
49
+ heading_line = heading_lines.detect {|h| h.content == heading_line }
50
+ end
51
+
52
+ min = heading_line.number + 1
53
+ max = next_sibling_of(heading_line).try(:number).try(:-, 1) || last_line_number
54
+
55
+ parsed_lines.select {|p| p.number.between?(min,max) }.map(&:raw).map(&:strip).join("")
56
+ end
57
+
58
+ def lines_between_boundaries *bounds
59
+ bounds.map do |range|
60
+ a = range.min - 1
61
+ b = range.max - a
62
+ parsed_lines.slice(a, b).map(&:raw)
63
+ end
64
+ end
65
+
66
+ def last_line_number
67
+ parsed_lines.last.try(:number) || raw_lines.length + 1
68
+ end
69
+
70
+ def parser
71
+ self
72
+ end
73
+
74
+ def extract_frontmatter!
75
+ if raw_lines.any? {|l| l.match(/```settings/) }
76
+ markers = identify_codemarkers
77
+
78
+ if raw_lines[markers.first].to_s.match(/```settings/)
79
+ length = markers[1] - markers[0]
80
+ @front_matter = raw_lines.slice(markers[0] + 1, length - 1).join
81
+ remove_raw_lines(markers[0], markers[1])
82
+ @content = raw_lines.join.gsub(/^\n|\n$/,'')
83
+ end
84
+ end
85
+ end
86
+
87
+ def front_matter
88
+ YAML.load(@front_matter) rescue {}
89
+ end
90
+
91
+ def identify_codemarkers
92
+ markers = []
93
+ raw_lines.each_with_index do |line, index|
94
+ if line.match(Brief::Line::CodeBlockRegex)
95
+ markers << index
96
+ end
97
+ end
98
+
99
+ markers
100
+ end
101
+
102
+ def scan
103
+ extract_frontmatter!
104
+
105
+ @code_markers = identify_codemarkers
106
+ @heading_markers = identify_heading_markers
107
+
108
+ @scanned = true
109
+ end
110
+
111
+ def identify_heading_markers
112
+ heading_markers = []
113
+ raw_lines.each_with_index do |line, index|
114
+ if line.match(Brief::Line::HeadingRegex) && !is_line_inside_code_region?(index)
115
+ heading_markers << index
116
+ end
117
+ end
118
+ heading_markers
119
+ end
120
+
121
+ def heading_boundaries
122
+ @heading_markers && @heading_markers.each_slice(2).map do |slice|
123
+ Range.new slice.first + 1, slice.last + 1
124
+ end
125
+ end
126
+
127
+ def code_boundaries
128
+ @code_markers && @code_markers.each_slice(2).map do |slice|
129
+ Range.new slice.first + 1, slice.last + 1
130
+ end
131
+ end
132
+
133
+ def valid?
134
+ @validated.presence || validate
135
+ end
136
+
137
+ def validate
138
+ scan unless @scanned
139
+
140
+ unless heading_lines.length > 0
141
+ raise MissingHeadings, 'A Brief must include some headings'
142
+ end
143
+
144
+ if heading_lines.uniq.length < heading_lines.length
145
+ raise HeadingsNotUnique, 'Headings inside a brief must be unique'
146
+ end
147
+
148
+ @validated = true
149
+ end
150
+
151
+ # This syntax isn't right, but it works.
152
+ # TODO: Research the best way to do this.
153
+ # middleman had an example of using the &method(:method_name)
154
+ # to treat a method definition as a proc
155
+ def tree_visitor node
156
+ subheadings = headings_under(node, as_objects: true)
157
+ children = subheadings.map(&method(:tree_visitor))
158
+ id = "#{ checksum }_#{ node.sort_index.join('_') }"
159
+
160
+ children.each_with_index do |child, index|
161
+ child[:heading_index] = index
162
+ end
163
+
164
+ base = {
165
+ id: id,
166
+ level: node.level,
167
+ children: children,
168
+ title: node.content,
169
+ sort_index: node.sort_index,
170
+ line_number: node.line_number
171
+ }
172
+
173
+ base.merge! content: body_content_for(node).to_s.strip if node.heading?
174
+
175
+ base
176
+ end
177
+
178
+ def tree_nodes
179
+ @tree_nodes ||= begin
180
+ nodes = []
181
+
182
+ headings_at_level(highest_level, as_objects: true).each_with_index do |node, index|
183
+ element = tree_visitor(node)
184
+ element[:heading_index] = index
185
+ nodes << element
186
+ end
187
+
188
+ nodes.map {|node| Hashie::Mash.new(node) }.flatten
189
+ end
190
+ end
191
+
192
+ def elements
193
+ @elements ||= tree.elements
194
+ end
195
+
196
+ def maximum_level
197
+ elements.map(&:level).max
198
+ end
199
+
200
+ def line_at(number)
201
+ parsed_lines.detect {|line| line.number == number }
202
+ end
203
+
204
+ def index_of(line)
205
+ lines.index(line)
206
+ end
207
+
208
+ def next_heading_after heading, options={}
209
+ heading_lines.detect {|line| line.number > heading.number }
210
+ end
211
+
212
+ def highest_level
213
+ level_boundaries.last
214
+ end
215
+
216
+ def lowest_level
217
+ level_boundaries.first
218
+ end
219
+
220
+ def level_boundaries
221
+ levels = heading_lines.map(&:level).uniq
222
+ [levels.max, levels.min]
223
+ end
224
+
225
+ def heading_lines
226
+ parsed_lines.select do |line|
227
+ line.heading?
228
+ end
229
+ end
230
+
231
+ def is_line_inside_code_region?(line_number)
232
+ code_boundaries.any? {|range| range.include?(line_number) }
233
+ end
234
+
235
+ def headings_after(heading_line)
236
+ if heading_line.is_a?(String)
237
+ heading_line = heading_lines.detect {|h| h.content == heading_line }
238
+ end
239
+
240
+ heading_lines.select {|line| line.number > heading_line.number }
241
+ end
242
+
243
+ def headings_under(heading_line, options={})
244
+ matches = []
245
+ continue = true
246
+
247
+ include_subtree = options.fetch(:all, false)
248
+ as_objects = options.fetch(:as_objects, false)
249
+
250
+ if heading_line.is_a?(String)
251
+ heading_line = heading_lines.detect {|h| h.content == heading_line }
252
+ end
253
+
254
+ headings_after(heading_line).each do |line|
255
+ continue = false if line.level <= heading_line.level
256
+ if continue && line.level > heading_line.level
257
+ matches << line unless (!include_subtree && line.level - heading_line.level > 1)
258
+ end
259
+ end
260
+
261
+ as_objects ? matches : matches.map(&:content)
262
+ end
263
+
264
+ def content_lines_under(heading_line, options={})
265
+ reject_blank = !options.fetch(:include_blank, true)
266
+ as_objects = options.fetch(:as_objects, false)
267
+
268
+ if heading_line.is_a?(String)
269
+ heading_line = heading_lines.detect {|h| h.content == heading_line }
270
+ end
271
+
272
+ return [] unless heading_line.respond_to?(:number)
273
+
274
+ min = heading_line.number
275
+ max = next_heading_after(heading_line).try(:number) || last_line_number
276
+
277
+ matches = content_lines.select do |line|
278
+ line.content? && line.number.between?(min, max)
279
+ end
280
+
281
+ matches.reject! {|m| m.content.blank? } if reject_blank
282
+
283
+ as_objects ? matches : matches.map(&:content)
284
+ end
285
+
286
+ def content_lines
287
+ parsed_lines.select(&:content?)
288
+ end
289
+
290
+ def code_block_markers
291
+ parsed_lines.select {|l| l.type == "code_block_marker" }
292
+ end
293
+
294
+ def raw_lines
295
+ @raw_lines ||= @content.lines.to_a
296
+ end
297
+
298
+ # I really need to get better with arrays
299
+ def remove_raw_lines start_index, end_index
300
+ original = @raw_lines.dup
301
+ copy = []
302
+
303
+ original.each_with_index do |line, index|
304
+ copy.push(line) unless index.between? start_index, end_index
305
+ end
306
+
307
+ @raw_lines = copy
308
+ end
309
+
310
+ def stripped_lines
311
+ @stripped ||= raw_lines.map(&:strip)
312
+ end
313
+
314
+ def parsed_lines
315
+ @parsed_lines
316
+ end
317
+
318
+ def parse clear=false
319
+ @parsed_lines = nil if clear
320
+
321
+ return @parsed_lines if @parsed_lines
322
+
323
+ parsed = []
324
+
325
+ raw = raw_lines
326
+
327
+ stripped_lines.each_with_index do |line,index|
328
+ is_code = code_boundaries.any? {|bounds| bounds.include?(index) }
329
+ line = Brief::Line.new(line, index, is_code)
330
+ line.raw = raw[index]
331
+ parsed << line
332
+ end
333
+
334
+ @parsed_lines = parsed
335
+ end
336
+
337
+ def code_blocks_by_language
338
+ return @code_samples if @code_samples
339
+
340
+ bounds = Array(parser && parser.send(:code_boundaries))
341
+ sections = parser.lines_between_boundaries(*bounds)
342
+
343
+ @code_samples = sections.inject({}) do |memo,section|
344
+ marker = section.first.split('```').last
345
+ language = marker.nil? ? :text : marker.strip
346
+
347
+ section.shift && section.pop
348
+ memo[language.to_sym] ||= []
349
+ memo[language.to_sym] << section.join("").sub(/^\s+/,'')
350
+ memo
351
+ end
352
+ end
353
+ end
354
+ end
@@ -0,0 +1,47 @@
1
+ module Brief
2
+ class Publisher::HandlerManager
3
+
4
+ attr_reader :publisher, :document
5
+
6
+ def initialize(publisher, document)
7
+ @publisher = publisher
8
+ @document = document
9
+ end
10
+
11
+ def parser
12
+ @parser ||= document.parser
13
+ end
14
+
15
+ def handlers
16
+ find_parent = lambda do |parent_id|
17
+ parser.tree.find(parent_id)
18
+ end
19
+
20
+ get_manager = -> { self }
21
+
22
+ @handlers ||= publisher.order.inject({}) do |memo, level|
23
+ config = publisher.config_for_level(level)
24
+ klass_name = config.publish.klass
25
+ klass = klass_name.nil? ? Brief::Handlers::Base : (klass_name.to_s.constantize rescue Brief::Handlers::Base)
26
+
27
+ memo[level] = parser.tree.level(level).map do |element|
28
+ obj = klass.new(element)
29
+
30
+ obj.parent(&find_parent)
31
+ obj.get_manager(&get_manager)
32
+
33
+ obj
34
+ end
35
+
36
+ memo
37
+ end
38
+ end
39
+
40
+ def run
41
+ publisher.order.each do |level|
42
+ objects = handlers[level]
43
+ objects.each(&:handle!)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,142 @@
1
+ module Brief
2
+ class Publisher
3
+
4
+ class << self
5
+ attr_accessor :defined, :aliases
6
+ end
7
+
8
+ self.defined ||= {}
9
+ self.aliases ||= {}
10
+
11
+ def self.find query
12
+ query = query.join(" ") if query.respond_to?(:to_ary)
13
+
14
+ if defined[query].nil? && aliases.has_key?(query)
15
+ return find(aliases[query])
16
+ end
17
+
18
+ result = defined[query]
19
+
20
+ result
21
+ end
22
+
23
+ def self.publisher publisher_name
24
+ defined[key]
25
+ end
26
+
27
+ def self.define publisher_name, &config
28
+ obj = defined[publisher_name] ||= Brief::Publisher.new(publisher_name)
29
+ obj.instance_eval(&config) if block_given?
30
+
31
+ aliases[publisher_name.downcase] ||= obj.name
32
+
33
+ obj.aliases.each do |alias_name|
34
+ aliases[alias_name.to_s] ||= obj.name
35
+ end
36
+
37
+ obj
38
+ end
39
+
40
+ def process document
41
+ @manager = Brief::Publisher::HandlerManager.new(self, document).run
42
+ end
43
+
44
+ attr_accessor :command_alias, :sample, :name, :options
45
+
46
+ def initialize(name, options={})
47
+ @name = name
48
+ @options = options
49
+ @command_alias = options[:command_alias]
50
+
51
+ @before_blocks = {}
52
+ end
53
+
54
+ def before event_name, &block
55
+ (@before_blocks[event_name.to_sym] ||= []).push(block)
56
+ end
57
+
58
+ def run_before_hooks event_name, &success
59
+ results = Array(@before_blocks[event_name.to_sym]).map do |hook|
60
+ hook.call if hook.respond_to?(:call)
61
+ end
62
+
63
+ unless results.any? {|r| r == false }
64
+ instance_eval(&success) if block_given?
65
+ end
66
+ end
67
+
68
+ def required_inputs
69
+ @options[:required_inputs] ||= {}
70
+ end
71
+
72
+ def requires_input field, options={}
73
+ cfg = required_inputs.fetch(field.to_sym) { options }
74
+ cfg.reverse_merge(options)
75
+ end
76
+
77
+ def levels &block
78
+ @state = :levels
79
+ instance_eval(&block) if block_given?
80
+ end
81
+
82
+ def sample sample_markdown=nil
83
+ options[:sample] = "#{sample_markdown}".gsub(/^\s+/,'') if sample_markdown
84
+ options[:sample]
85
+ end
86
+
87
+ def aliases *values
88
+ @command_aliases ||= []
89
+ @command_aliases += values
90
+ @command_aliases.uniq
91
+ end
92
+
93
+ def maximum_level
94
+ @maximum_level
95
+ end
96
+
97
+ def current_level
98
+ @current_level || 1
99
+ end
100
+
101
+ def order *levels
102
+ @order = levels if levels && levels.length > 0
103
+ @order || maximum_level.times.map {|n| n + 1 }
104
+ end
105
+
106
+ def level level_number, &block
107
+ @state = :level
108
+ @maximum_level = [@current_level || 0, level_number].compact.map(&:to_i).max
109
+ @current_level = level_number
110
+ instance_eval(&block) if block_given?
111
+ end
112
+
113
+ def level_config
114
+ @level_config ||= Hashie::Mash.new({})
115
+ end
116
+
117
+ def config_for_level level
118
+ level_config["level_#{level}".to_sym] ||= {}
119
+ end
120
+
121
+ def current_level_config
122
+ config_for_level(current_level)
123
+ end
124
+
125
+ def desc description
126
+ current_level_config[:description] = description
127
+ end
128
+
129
+ def define_handler handler, options={}
130
+ if options.is_a?(String)
131
+ options = {klass: options.classify}
132
+ end
133
+
134
+ current_level_config[handler] = options
135
+ end
136
+
137
+ def replaces_items_from_level n, options={}
138
+ current_level_config[:replace_options] = options
139
+ end
140
+
141
+ end
142
+ end
data/lib/brief/tree.rb ADDED
@@ -0,0 +1,42 @@
1
+ module Brief
2
+ class Tree
3
+ def initialize(nodes)
4
+ @nodes = nodes
5
+ end
6
+
7
+ def find element_by_id
8
+ elements.detect {|el| el.id == element_by_id }
9
+ end
10
+
11
+ def elements
12
+ @elements ||= begin
13
+ @nodes.map do |node|
14
+ only = node.except(:children)
15
+
16
+ visitor = lambda do |child|
17
+ c_only = child.except(:children)
18
+ g_children = Array(child[:children])
19
+ g_children = nil if g_children.length == 0
20
+
21
+ g_children.each {|gchild| gchild.parent_id = c_only.id}
22
+
23
+ [c_only, g_children].compact
24
+ end
25
+
26
+ children = Array(node[:children]).map(&visitor).flatten
27
+ children.each {|child| child.parent_id ||= only.id }
28
+
29
+ children = nil if children.length == 0
30
+
31
+ [only, children].compact
32
+ end
33
+
34
+ end.flatten
35
+ end
36
+
37
+ def level level=1
38
+ elements.select {|el| el.level == level }
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,9 @@
1
+ module Brief
2
+ MAJOR = 0
3
+ MINOR = 0
4
+ REVISION = 1
5
+
6
+ Version = "#{ MAJOR }.#{ MINOR }.#{ REVISION }"
7
+
8
+ VERSION = Version
9
+ end
data/lib/brief.rb ADDED
@@ -0,0 +1,56 @@
1
+ lib = File.dirname(__FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ module Brief
5
+ # Haven't decided if the brief config system should support different profiles or not
6
+ def self.profile
7
+ configuration
8
+ end
9
+
10
+ def self.config
11
+ configuration
12
+ end
13
+
14
+ def self.configuration
15
+ @configuration ||= Brief::Configuration.instance
16
+ end
17
+
18
+ def self.root
19
+ Pathname(Dir.pwd())
20
+ end
21
+
22
+ def self.gem_root
23
+ Pathname(File.dirname(__FILE__))
24
+ end
25
+
26
+ def self.define publisher_name, &config
27
+ Brief::Publisher.define(publisher_name, &config)
28
+ end
29
+ end
30
+
31
+ require 'pathname'
32
+ require 'hashie'
33
+ require 'digest'
34
+ require 'yaml'
35
+
36
+ require 'active_support'
37
+ require 'active_support/core_ext'
38
+
39
+ require 'brief/line'
40
+ require 'brief/parser'
41
+ require 'brief/document'
42
+ require 'brief/tree'
43
+ require 'brief/version'
44
+ require 'brief/configuration'
45
+
46
+ require 'brief/publisher'
47
+ require 'brief/publisher/handler_manager'
48
+ # These should be able to be loaded separately
49
+ # some other way, but to help develoment..
50
+ require 'brief/handlers/base'
51
+ require 'brief/formatters/base'
52
+
53
+ # these may be optional one day
54
+ require 'brief/github'
55
+ require 'brief/git'
56
+ require 'brief/github/wiki'
data/lib/core_ext.rb ADDED
@@ -0,0 +1,37 @@
1
+ class Object
2
+ def present?
3
+ !nil?
4
+ end
5
+
6
+ def try(*a, &b)
7
+ if a.empty? && block_given?
8
+ yield self
9
+ else
10
+ public_send(*a, &b) if respond_to?(a.first)
11
+ end
12
+ end
13
+
14
+ def try!(*a, &b)
15
+ if a.empty? && block_given?
16
+ yield self
17
+ else
18
+ public_send(*a, &b)
19
+ end
20
+ end
21
+ end
22
+
23
+ class NilClass
24
+ def present?
25
+ false
26
+ end
27
+ def try(*args)
28
+ nil
29
+ end
30
+
31
+ def try!(*args)
32
+ nil
33
+ end
34
+ end
35
+
36
+ require 'core_ext/hash'
37
+ require 'core_ext/string'