restapi 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/Gemfile +0 -1
  2. data/Gemfile.lock +0 -10
  3. data/README.rdoc +18 -12
  4. data/app/controllers/restapi/restapis_controller.rb +28 -1
  5. data/app/views/layouts/restapi/restapi.html.erb +1 -0
  6. data/app/views/restapi/restapis/_params.html.erb +22 -0
  7. data/app/views/restapi/restapis/_params_plain.html.erb +16 -0
  8. data/app/views/restapi/restapis/index.html.erb +5 -5
  9. data/app/views/restapi/restapis/method.html.erb +8 -4
  10. data/app/views/restapi/restapis/plain.html.erb +70 -0
  11. data/app/views/restapi/restapis/resource.html.erb +16 -5
  12. data/app/views/restapi/restapis/static.html.erb +4 -6
  13. data/lib/restapi.rb +2 -1
  14. data/lib/restapi/application.rb +72 -22
  15. data/lib/restapi/client/generator.rb +104 -0
  16. data/lib/restapi/client/template/Gemfile.tt +5 -0
  17. data/lib/restapi/client/template/README.tt +3 -0
  18. data/lib/restapi/client/template/base.rb.tt +33 -0
  19. data/lib/restapi/client/template/bin.rb.tt +110 -0
  20. data/lib/restapi/client/template/cli.rb.tt +25 -0
  21. data/lib/restapi/client/template/cli_command.rb.tt +129 -0
  22. data/lib/restapi/client/template/client.rb.tt +10 -0
  23. data/lib/restapi/client/template/resource.rb.tt +17 -0
  24. data/lib/restapi/dsl_definition.rb +20 -2
  25. data/lib/restapi/error_description.rb +8 -2
  26. data/lib/restapi/extractor.rb +143 -0
  27. data/lib/restapi/extractor/collector.rb +113 -0
  28. data/lib/restapi/extractor/recorder.rb +122 -0
  29. data/lib/restapi/extractor/writer.rb +356 -0
  30. data/lib/restapi/helpers.rb +10 -5
  31. data/lib/restapi/markup.rb +12 -12
  32. data/lib/restapi/method_description.rb +52 -8
  33. data/lib/restapi/param_description.rb +6 -5
  34. data/lib/restapi/railtie.rb +1 -1
  35. data/lib/restapi/resource_description.rb +1 -1
  36. data/lib/restapi/restapi_module.rb +43 -0
  37. data/lib/restapi/validator.rb +70 -3
  38. data/lib/restapi/version.rb +1 -1
  39. data/lib/tasks/restapi.rake +120 -121
  40. data/restapi.gemspec +0 -2
  41. data/spec/controllers/restapis_controller_spec.rb +41 -6
  42. data/spec/controllers/users_controller_spec.rb +51 -12
  43. data/spec/dummy/app/controllers/application_controller.rb +0 -2
  44. data/spec/dummy/app/controllers/twitter_example_controller.rb +4 -9
  45. data/spec/dummy/app/controllers/users_controller.rb +13 -6
  46. data/spec/dummy/config/initializers/restapi.rb +7 -0
  47. data/spec/dummy/doc/restapi_examples.yml +28 -0
  48. metadata +49 -76
  49. data/app/helpers/restapi/restapis_helper.rb +0 -31
@@ -0,0 +1,356 @@
1
+ module Restapi
2
+ module Extractor
3
+ class Writer
4
+
5
+ def initialize(collector)
6
+ @collector = collector
7
+ @examples_file = File.join(Rails.root, "doc", "restapi_examples.yml")
8
+ end
9
+
10
+ def write_examples
11
+ merged_examples = merge_old_new_examples
12
+ FileUtils.mkdir_p(File.dirname(@examples_file))
13
+ File.open(@examples_file, "w") do |f|
14
+ f << YAML.dump(OrderedHash[*merged_examples.sort_by(&:first).flatten(1)])
15
+ end
16
+ end
17
+
18
+ def write_docs
19
+ descriptions = @collector.finalize_descriptions
20
+ descriptions.each do |_, desc|
21
+ if desc[:api].empty?
22
+ logger.warn("REST_API: Couldn't find any path for #{desc_to_s(desc)}")
23
+ next
24
+ end
25
+ self.class.update_action_description(desc[:controller], desc[:action]) do |u|
26
+ u.update_generated_description desc
27
+ end
28
+ end
29
+ end
30
+
31
+ def self.update_action_description(controller, action)
32
+ updater = ActionDescriptionUpdater.new(controller, action)
33
+ yield updater
34
+ updater.write!
35
+ rescue ActionDescriptionUpdater::ControllerNotFound
36
+ logger.warn("REST_API: Couldn't find controller file for #{controller}")
37
+ rescue ActionDescriptionUpdater::ActionNotFound
38
+ logger.warn("REST_API: Couldn't find action #{action} in #{controller}")
39
+ end
40
+
41
+ protected
42
+
43
+ def desc_to_s(description)
44
+ "#{description[:controller].name}##{description[:action]}"
45
+ end
46
+
47
+ def ordered_call(call)
48
+ call = call.stringify_keys
49
+ ordered_call = OrderedHash.new
50
+ %w[verb path query request_data response_data code show_in_doc recorded].each do |k|
51
+ next unless call.has_key?(k)
52
+ ordered_call[k] = case call[k]
53
+ when ActiveSupport::HashWithIndifferentAccess
54
+ JSON.parse(call[k].to_json) # to_hash doesn't work recursively and I'm too lazy to write the recursion:)
55
+ else
56
+ call[k]
57
+ end
58
+ end
59
+ return ordered_call
60
+ end
61
+
62
+ def merge_old_new_examples
63
+ new_examples = self.load_new_examples
64
+ old_examples = self.load_old_examples
65
+ merged_examples = []
66
+ (new_examples.keys + old_examples.keys).uniq.each do |key|
67
+ if new_examples.has_key?(key)
68
+ records = new_examples[key]
69
+ else
70
+ records = old_examples[key]
71
+ end
72
+ merged_examples << [key, records.map { |r| ordered_call(r) } ]
73
+ end
74
+ return merged_examples
75
+ end
76
+
77
+ def load_new_examples
78
+ @collector.records.reduce({}) do |h, (method, calls)|
79
+ show_in_doc = nil
80
+ recorded_examples = calls.map do |call|
81
+ if show_in_doc.nil?
82
+ show_in_doc = 1 if showable_in_doc?(call.with_indifferent_access)
83
+ else
84
+ show_in_doc = 0
85
+ end
86
+ example = call.merge(:show_in_doc => show_in_doc.to_i, :recorded => true)
87
+ example
88
+ end
89
+ h.update(method => recorded_examples)
90
+ end
91
+ end
92
+
93
+ def load_old_examples
94
+ if File.exists?(@examples_file)
95
+ return YAML.load(File.read(@examples_file))
96
+ end
97
+ return {}
98
+ end
99
+
100
+ def logger
101
+ self.class.logger
102
+ end
103
+
104
+ def self.logger
105
+ Extractor.logger
106
+ end
107
+
108
+ def showable_in_doc?(call)
109
+ # we don't want to mess documentation with too large examples
110
+ if hash_nodes_count(call["request_data"]) + hash_nodes_count(call["response_data"]) > 100
111
+ return false
112
+ else
113
+ return 1
114
+ end
115
+ end
116
+
117
+ def hash_nodes_count(node)
118
+ case node
119
+ when Hash
120
+ 1 + (node.values.map { |v| hash_nodes_count(v) }.reduce(:+) || 0)
121
+ when Array
122
+ node.map { |v| hash_nodes_count(v) }.reduce(:+) || 1
123
+ else
124
+ 1
125
+ end
126
+ end
127
+
128
+ end
129
+
130
+ class ActionDescriptionUpdater
131
+
132
+ class ControllerNotFound < Exception; end
133
+
134
+ class ActionNotFound < Exception; end
135
+
136
+ def initialize(controller, action)
137
+ @controller = controller
138
+ @action = action
139
+ end
140
+
141
+ def generated?
142
+ old_header.include?(Restapi.configuration.generated_doc_disclaimer)
143
+ end
144
+
145
+ def update_apis(apis)
146
+ new_header = ""
147
+ new_header << Restapi.configuration.generated_doc_disclaimer << "\n" if generated?
148
+ new_header << generate_apis_code(apis)
149
+ new_header << ensure_line_breaks(old_header.lines).reject do |line|
150
+ line.include?(Restapi.configuration.generated_doc_disclaimer) ||
151
+ line =~ /^api/
152
+ end.join
153
+ overwrite_header(new_header)
154
+ end
155
+
156
+ def update_generated_description(desc)
157
+ if generated? || old_header.empty?
158
+ new_header = generate_code(desc)
159
+ overwrite_header(new_header)
160
+ end
161
+ end
162
+
163
+ def update(new_header)
164
+ overwrite_header(new_header)
165
+ end
166
+
167
+ def old_header
168
+ return @old_header if defined? @old_header
169
+ @old_header = lines_above_method[/^\s*?#{Regexp.escape(Restapi.configuration.generated_doc_disclaimer)}.*/m]
170
+ @old_header ||= lines_above_method[/^\s*?\b(api|params|error|example)\b.*/m]
171
+ @old_header ||= ""
172
+ @old_header.sub!(/\A\s*\n/,"")
173
+ @old_header = align_indented(@old_header)
174
+ end
175
+
176
+ def write!
177
+ File.open(controller_path, "w") { |f| f << @controller_content }
178
+ @changed=false
179
+ end
180
+
181
+ protected
182
+
183
+ def logger
184
+ Extractor.logger
185
+ end
186
+
187
+ def action_line
188
+ return @action_line if defined? @action_line
189
+ @action_line = ensure_line_breaks(controller_content.lines).find_index { |line| line =~ /def \b#{@action}\b/ }
190
+ raise ActionNotFound unless @action_line
191
+ @action_line
192
+ end
193
+
194
+ def controller_path
195
+ @controller_path ||= File.join(Rails.root, "app", "controllers", "#{@controller.controller_path}_controller.rb")
196
+ end
197
+
198
+ def controller_content
199
+ raise ControllerNotFound.new unless File.exists? controller_path
200
+ @controller_content ||= File.read(controller_path)
201
+ end
202
+
203
+ def controller_content=(new_content)
204
+ return if @controller_name == new_content
205
+ remove_instance_variable("@action_line")
206
+ remove_instance_variable("@old_header")
207
+ @controller_content=new_content
208
+ @changed = true
209
+ end
210
+
211
+ def generate_code(desc)
212
+ code = "#{Restapi.configuration.generated_doc_disclaimer}\n"
213
+ code << generate_apis_code(desc[:api])
214
+ code << generate_params_code(desc[:params])
215
+ code << generate_errors_code(desc[:errors])
216
+ return code
217
+ end
218
+
219
+ def generate_apis_code(apis)
220
+ code = ""
221
+ apis.sort_by {|a| a[:path] }.each do |api|
222
+ desc = api[:desc]
223
+ name = @controller.controller_name.gsub("_", " ")
224
+ desc ||= case @action.to_s
225
+ when "show", "create", "update", "destroy"
226
+ name = name.singularize
227
+ "#{@action.capitalize} #{name =~ /^[aeiou]/ ? "an" : "a"} #{name}"
228
+ when "index"
229
+ "List #{name}"
230
+ end
231
+
232
+ code << "api :#{api[:method]}, \"#{api[:path]}\""
233
+ code << ", \"#{desc}\"" if desc
234
+ code << "\n"
235
+ end
236
+ return code
237
+ end
238
+
239
+ def generate_params_code(params, indent = "")
240
+ code = ""
241
+ params.sort_by {|n,_| n }.each do |(name, desc)|
242
+ desc[:type] = (desc[:type] && desc[:type].first) || Object
243
+ code << "#{indent}param"
244
+ if name =~ /\W/
245
+ code << " :\"#{name}\""
246
+ else
247
+ code << " :#{name}"
248
+ end
249
+ code << ", #{desc[:type].inspect}"
250
+ if desc[:allow_nil]
251
+ code << ", :allow_nil => true"
252
+ end
253
+ if desc[:required]
254
+ code << ", :required => true"
255
+ end
256
+ if desc[:nested]
257
+ code << " do\n"
258
+ code << generate_params_code(desc[:nested], indent + " ")
259
+ code << "#{indent}end"
260
+ else
261
+ end
262
+ code << "\n"
263
+ end
264
+ code
265
+ end
266
+
267
+ def generate_errors_code(errors)
268
+ code = ""
269
+ errors.sort_by {|e| e[:code] }.each do |error|
270
+ code << "error :code => #{error[:code]}\n"
271
+ end
272
+ code
273
+ end
274
+
275
+ def align_indented(text)
276
+ shift_left = ensure_line_breaks(text.lines).map { |l| l[/^\s*/].size }.min
277
+ ensure_line_breaks(text.lines).map { |l| l[shift_left..-1] }.join
278
+ end
279
+
280
+ def overwrite_header(new_header)
281
+ overwrite_line_from = action_line
282
+ overwrite_line_to = action_line
283
+ unless old_header.empty?
284
+ overwrite_line_from -= ensure_line_breaks(old_header.lines).count
285
+ end
286
+ lines = ensure_line_breaks(controller_content.lines).to_a
287
+ indentation = lines[action_line][/^\s*/]
288
+ self.controller_content= (lines[0...overwrite_line_from] +
289
+ [new_header.gsub(/^/,indentation)] +
290
+ lines[overwrite_line_to..-1]).join
291
+ end
292
+
293
+ # returns all the lines before the method that might contain the restpi descriptions
294
+ # not bulletproof but working for conventional Ruby code
295
+ def lines_above_method
296
+ added_lines = []
297
+ lines_to_add = []
298
+ block_level = 0
299
+ ensure_line_breaks(controller_content.lines).first(action_line).reverse_each do |line|
300
+ if line =~ /\s*\bend\b\s*/
301
+ block_level += 1
302
+ end
303
+ if block_level > 0
304
+ lines_to_add << line
305
+ else
306
+ added_lines << line
307
+ end
308
+ if line =~ /\s*\b(module|class|def)\b /
309
+ break
310
+ end
311
+ if line =~ /do\s*(\|.*?\|)?\s*$/
312
+ block_level -= 1
313
+ if block_level == 0
314
+ added_lines.concat(lines_to_add)
315
+ lines_to_add = []
316
+ end
317
+ end
318
+ end
319
+ return added_lines.reverse.join
320
+ end
321
+
322
+ # this method would be totally useless unless some clever guy
323
+ # desided that it would be great idea to change a behavior of
324
+ # "".lines method to not include end of lines.
325
+ #
326
+ # For more details:
327
+ # https://github.com/puppetlabs/puppet/blob/0dc44695/lib/puppet/util/monkey_patches.rb
328
+ def ensure_line_breaks(lines)
329
+ if lines.to_a.size > 1 && lines.first !~ /\n\Z/
330
+ lines.map { |l| l !~ /\n\Z/ ? (l << "\n") : l }.to_enum
331
+ else
332
+ lines
333
+ end
334
+ end
335
+ end
336
+
337
+ # Used to keep restapi_examples.yml params in order
338
+ class OrderedHash < ActiveSupport::OrderedHash
339
+
340
+ def to_yaml_type
341
+ "!tag:yaml.org,2002:map"
342
+ end
343
+
344
+ def to_yaml(opts = {})
345
+ YAML.quick_emit(self, opts) do |out|
346
+ out.map(taguri) do |map|
347
+ each do |k, v|
348
+ map.add(k, v)
349
+ end
350
+ end
351
+ end
352
+ end
353
+ end
354
+
355
+ end
356
+ end
@@ -4,16 +4,21 @@ module Restapi
4
4
  Restapi.configuration.markup.to_html(text.strip_heredoc)
5
5
  end
6
6
 
7
+ attr_accessor :url_prefix
8
+
7
9
  def full_url(path)
8
- unless @prefix
9
- @prefix = ""
10
+ unless @url_prefix
11
+ @url_prefix = ""
10
12
  if rails_prefix = ENV["RAILS_RELATIVE_URL_ROOT"]
11
- @prefix << rails_prefix
13
+ @url_prefix << rails_prefix
12
14
  end
13
- @prefix << Restapi.configuration.doc_base_url
15
+ @url_prefix << Restapi.configuration.doc_base_url
14
16
  end
15
17
  path = path.sub(/^\//,"")
16
- "#{@prefix}/#{path}"
18
+ ret = "#{@url_prefix}/#{path}"
19
+ ret.insert(0,"/") unless ret =~ /\A[.\/]/
20
+ ret.sub!(/\/*\Z/,"")
21
+ ret
17
22
  end
18
23
  end
19
24
  end
@@ -1,40 +1,40 @@
1
1
  module Restapi
2
-
2
+
3
3
  module Markup
4
-
4
+
5
5
  class RDoc
6
-
6
+
7
7
  def initialize
8
8
  require 'rdoc'
9
9
  require 'rdoc/markup/to_html'
10
10
  @rdoc ||= ::RDoc::Markup::ToHtml.new
11
11
  end
12
-
12
+
13
13
  def to_html(text)
14
14
  @rdoc.convert(text)
15
15
  end
16
-
16
+
17
17
  end
18
-
18
+
19
19
  class Markdown
20
-
20
+
21
21
  def initialize
22
22
  require 'redcarpet'
23
23
  @redcarpet ||= ::Redcarpet::Markdown.new(::Redcarpet::Render::HTML.new)
24
24
  end
25
-
25
+
26
26
  def to_html(text)
27
27
  @redcarpet.render(text)
28
28
  end
29
-
29
+
30
30
  end
31
-
31
+
32
32
  class Textile
33
-
33
+
34
34
  def initialize
35
35
  require 'RedCloth'
36
36
  end
37
-
37
+
38
38
  def to_html(text)
39
39
  RedCloth.new(text).to_html
40
40
  end
@@ -21,25 +21,32 @@ module Restapi
21
21
 
22
22
  end
23
23
 
24
- attr_reader :errors, :full_description, :method, :resource, :apis, :examples
24
+ attr_reader :errors, :full_description, :method, :resource, :apis, :examples, :see
25
25
 
26
26
  def initialize(method, resource, app)
27
27
  @method = method
28
28
  @resource = resource
29
29
 
30
30
  @apis = app.get_api_args
31
+ @see = app.get_see
31
32
 
32
33
  desc = app.get_description || ''
33
34
  @full_description = Restapi.markup_to_html(desc)
34
35
  @errors = app.get_errors
35
36
  @params_ordered = app.get_params
36
37
  @examples = app.get_examples
38
+
39
+ @examples += load_recorded_examples
37
40
 
38
41
  parent = @resource.controller.superclass
39
42
  if parent != ActionController::Base
40
43
  @parent_resource = parent.controller_name
41
44
  end
42
- @resource.add_method("#{resource._id}##{method}")
45
+ @resource.add_method(id)
46
+ end
47
+
48
+ def id
49
+ "#{resource._id}##{method}"
43
50
  end
44
51
 
45
52
  def params
@@ -64,11 +71,7 @@ module Restapi
64
71
  end
65
72
 
66
73
  def doc_url
67
- [
68
- ENV["RAILS_RELATIVE_URL_ROOT"],
69
- Restapi.configuration.doc_base_url,
70
- "/#{@resource._id}/#{@method}"
71
- ].join
74
+ Restapi.full_url("#{@resource._id}/#{@method}")
72
75
  end
73
76
 
74
77
  def method_apis_to_json
@@ -81,6 +84,20 @@ module Restapi
81
84
  end
82
85
  end
83
86
 
87
+ def see_url
88
+ if @see
89
+ method_description = Restapi[@see]
90
+ if method_description.nil?
91
+ raise ArgumentError.new("Method #{@see} referenced in 'see' does not exist.")
92
+ end
93
+ method_description.doc_url
94
+ end
95
+ end
96
+
97
+ def see
98
+ @see
99
+ end
100
+
84
101
  def to_json
85
102
  {
86
103
  :doc_url => doc_url,
@@ -89,7 +106,9 @@ module Restapi
89
106
  :full_description => @full_description,
90
107
  :errors => @errors,
91
108
  :params => params_ordered.map(&:to_json).flatten,
92
- :examples => @examples
109
+ :examples => @examples,
110
+ :see => @see,
111
+ :see_url => see_url
93
112
  }
94
113
  end
95
114
 
@@ -101,6 +120,31 @@ module Restapi
101
120
  params.concat(new_params)
102
121
  end
103
122
 
123
+ def load_recorded_examples
124
+ (Restapi.recorded_examples[id] || []).
125
+ find_all { |ex| ex["show_in_doc"].to_i > 0 }.
126
+ sort_by { |ex| ex["show_in_doc"] }.
127
+ map { |ex| format_example(ex.symbolize_keys) }
128
+ end
129
+
130
+ def format_example_data(data)
131
+ case data
132
+ when Array, Hash
133
+ JSON.pretty_generate(data).gsub(/: \[\s*\]/,": []").gsub(/\{\s*\}/,"{}")
134
+ else
135
+ data
136
+ end
137
+ end
138
+
139
+ def format_example(ex)
140
+ example = "#{ex[:verb]} #{ex[:path]}"
141
+ example << "?#{ex[:query]}" unless ex[:query].blank?
142
+ example << "\n" << format_example_data(ex[:request_data]).to_s if ex[:request_data]
143
+ example << "\n" << ex[:code].to_s
144
+ example << "\n" << format_example_data(ex[:response_data]).to_s if ex[:response_data]
145
+ example
146
+ end
147
+
104
148
  end
105
149
 
106
150
  end