restapi 0.0.4 → 0.0.5

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 (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