brief 0.0.1 → 0.0.2

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.
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'