jmoses_apipie-rails 0.0.23

Sign up to get free protection for your applications and to get access to all the features.
Files changed (120) hide show
  1. data/.gitignore +11 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +4 -0
  4. data/APACHE-LICENSE-2.0 +202 -0
  5. data/CHANGELOG +55 -0
  6. data/Gemfile +3 -0
  7. data/MIT-LICENSE +20 -0
  8. data/NOTICE +4 -0
  9. data/README.rst +938 -0
  10. data/Rakefile +13 -0
  11. data/apipie-rails.gemspec +26 -0
  12. data/app/controllers/apipie/apipies_controller.rb +105 -0
  13. data/app/public/apipie/javascripts/apipie.js +6 -0
  14. data/app/public/apipie/javascripts/bundled/bootstrap-collapse.js +138 -0
  15. data/app/public/apipie/javascripts/bundled/bootstrap.js +1726 -0
  16. data/app/public/apipie/javascripts/bundled/jquery-1.7.2.js +9404 -0
  17. data/app/public/apipie/javascripts/bundled/prettify.js +28 -0
  18. data/app/public/apipie/stylesheets/application.css +20 -0
  19. data/app/public/apipie/stylesheets/bundled/bootstrap-responsive.min.css +12 -0
  20. data/app/public/apipie/stylesheets/bundled/bootstrap.min.css +689 -0
  21. data/app/public/apipie/stylesheets/bundled/prettify.css +30 -0
  22. data/app/views/apipie/apipies/_disqus.html.erb +11 -0
  23. data/app/views/apipie/apipies/_params.html.erb +29 -0
  24. data/app/views/apipie/apipies/_params_plain.html.erb +16 -0
  25. data/app/views/apipie/apipies/apipie_404.html.erb +12 -0
  26. data/app/views/apipie/apipies/getting_started.html.erb +4 -0
  27. data/app/views/apipie/apipies/index.html.erb +43 -0
  28. data/app/views/apipie/apipies/method.html.erb +71 -0
  29. data/app/views/apipie/apipies/plain.html.erb +70 -0
  30. data/app/views/apipie/apipies/resource.html.erb +98 -0
  31. data/app/views/apipie/apipies/static.html.erb +101 -0
  32. data/app/views/layouts/apipie/apipie.html.erb +26 -0
  33. data/lib/apipie-rails.rb +15 -0
  34. data/lib/apipie/apipie_module.rb +62 -0
  35. data/lib/apipie/application.rb +334 -0
  36. data/lib/apipie/client/generator.rb +135 -0
  37. data/lib/apipie/configuration.rb +128 -0
  38. data/lib/apipie/dsl_definition.rb +379 -0
  39. data/lib/apipie/error_description.rb +25 -0
  40. data/lib/apipie/errors.rb +38 -0
  41. data/lib/apipie/extractor.rb +151 -0
  42. data/lib/apipie/extractor/collector.rb +113 -0
  43. data/lib/apipie/extractor/recorder.rb +124 -0
  44. data/lib/apipie/extractor/writer.rb +367 -0
  45. data/lib/apipie/helpers.rb +52 -0
  46. data/lib/apipie/markup.rb +48 -0
  47. data/lib/apipie/method_description.rb +191 -0
  48. data/lib/apipie/param_description.rb +204 -0
  49. data/lib/apipie/railtie.rb +9 -0
  50. data/lib/apipie/resource_description.rb +102 -0
  51. data/lib/apipie/routing.rb +15 -0
  52. data/lib/apipie/see_description.rb +39 -0
  53. data/lib/apipie/static_dispatcher.rb +59 -0
  54. data/lib/apipie/validator.rb +310 -0
  55. data/lib/apipie/version.rb +3 -0
  56. data/lib/generators/apipie/install/README +6 -0
  57. data/lib/generators/apipie/install/install_generator.rb +25 -0
  58. data/lib/generators/apipie/install/templates/initializer.rb.erb +7 -0
  59. data/lib/tasks/apipie.rake +166 -0
  60. data/rel-eng/packages/.readme +3 -0
  61. data/rel-eng/packages/rubygem-apipie-rails +1 -0
  62. data/rel-eng/tito.props +5 -0
  63. data/spec/controllers/api/v1/architectures_controller_spec.rb +30 -0
  64. data/spec/controllers/api/v2/architectures_controller_spec.rb +12 -0
  65. data/spec/controllers/api/v2/nested/resources_controller_spec.rb +11 -0
  66. data/spec/controllers/apipies_controller_spec.rb +141 -0
  67. data/spec/controllers/concerns_controller_spec.rb +42 -0
  68. data/spec/controllers/users_controller_spec.rb +473 -0
  69. data/spec/dummy/Rakefile +7 -0
  70. data/spec/dummy/app/controllers/api/base_controller.rb +4 -0
  71. data/spec/dummy/app/controllers/api/v1/architectures_controller.rb +42 -0
  72. data/spec/dummy/app/controllers/api/v1/base_controller.rb +11 -0
  73. data/spec/dummy/app/controllers/api/v2/architectures_controller.rb +30 -0
  74. data/spec/dummy/app/controllers/api/v2/base_controller.rb +11 -0
  75. data/spec/dummy/app/controllers/api/v2/nested/architectures_controller.rb +30 -0
  76. data/spec/dummy/app/controllers/api/v2/nested/resources_controller.rb +33 -0
  77. data/spec/dummy/app/controllers/application_controller.rb +6 -0
  78. data/spec/dummy/app/controllers/concerns/sample_controller.rb +39 -0
  79. data/spec/dummy/app/controllers/concerns_controller.rb +8 -0
  80. data/spec/dummy/app/controllers/twitter_example_controller.rb +302 -0
  81. data/spec/dummy/app/controllers/users_controller.rb +251 -0
  82. data/spec/dummy/app/views/layouts/application.html.erb +21 -0
  83. data/spec/dummy/config.ru +4 -0
  84. data/spec/dummy/config/application.rb +45 -0
  85. data/spec/dummy/config/boot.rb +10 -0
  86. data/spec/dummy/config/database.yml +21 -0
  87. data/spec/dummy/config/environment.rb +8 -0
  88. data/spec/dummy/config/environments/development.rb +25 -0
  89. data/spec/dummy/config/environments/production.rb +49 -0
  90. data/spec/dummy/config/environments/test.rb +35 -0
  91. data/spec/dummy/config/initializers/apipie.rb +102 -0
  92. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  93. data/spec/dummy/config/initializers/inflections.rb +10 -0
  94. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  95. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  96. data/spec/dummy/config/initializers/session_store.rb +8 -0
  97. data/spec/dummy/config/locales/en.yml +5 -0
  98. data/spec/dummy/config/routes.rb +22 -0
  99. data/spec/dummy/db/.gitkeep +0 -0
  100. data/spec/dummy/doc/apipie_examples.yml +28 -0
  101. data/spec/dummy/public/404.html +26 -0
  102. data/spec/dummy/public/422.html +26 -0
  103. data/spec/dummy/public/500.html +26 -0
  104. data/spec/dummy/public/favicon.ico +0 -0
  105. data/spec/dummy/public/javascripts/application.js +2 -0
  106. data/spec/dummy/public/javascripts/controls.js +965 -0
  107. data/spec/dummy/public/javascripts/dragdrop.js +974 -0
  108. data/spec/dummy/public/javascripts/effects.js +1123 -0
  109. data/spec/dummy/public/javascripts/prototype.js +6001 -0
  110. data/spec/dummy/public/javascripts/rails.js +202 -0
  111. data/spec/dummy/public/stylesheets/.gitkeep +0 -0
  112. data/spec/dummy/script/rails +6 -0
  113. data/spec/lib/application_spec.rb +38 -0
  114. data/spec/lib/method_description_spec.rb +30 -0
  115. data/spec/lib/param_description_spec.rb +174 -0
  116. data/spec/lib/param_group_spec.rb +45 -0
  117. data/spec/lib/resource_description_spec.rb +30 -0
  118. data/spec/lib/validator_spec.rb +46 -0
  119. data/spec/spec_helper.rb +32 -0
  120. metadata +337 -0
@@ -0,0 +1,367 @@
1
+ require 'set'
2
+
3
+ module Apipie
4
+ module Extractor
5
+ class Writer
6
+
7
+ def initialize(collector)
8
+ @collector = collector
9
+ @examples_file = File.join(Rails.root, "doc", "apipie_examples.yml")
10
+ end
11
+
12
+ def write_examples
13
+ merged_examples = merge_old_new_examples
14
+ FileUtils.mkdir_p(File.dirname(@examples_file))
15
+ File.open(@examples_file, "w") do |f|
16
+ f << YAML.dump(OrderedHash[*merged_examples.sort_by(&:first).flatten(1)])
17
+ end
18
+ end
19
+
20
+ def write_docs
21
+ descriptions = @collector.finalize_descriptions
22
+ descriptions.each do |_, desc|
23
+ if desc[:api].empty?
24
+ logger.warn("REST_API: Couldn't find any path for #{desc_to_s(desc)}")
25
+ next
26
+ end
27
+ self.class.update_action_description(desc[:controller], desc[:action]) do |u|
28
+ u.update_generated_description desc
29
+ end
30
+ end
31
+ end
32
+
33
+ def self.update_action_description(controller, action)
34
+ updater = ActionDescriptionUpdater.new(controller, action)
35
+ yield updater
36
+ updater.write!
37
+ rescue ActionDescriptionUpdater::ControllerNotFound
38
+ logger.warn("REST_API: Couldn't find controller file for #{controller}")
39
+ rescue ActionDescriptionUpdater::ActionNotFound
40
+ logger.warn("REST_API: Couldn't find action #{action} in #{controller}")
41
+ end
42
+
43
+ protected
44
+
45
+ def desc_to_s(description)
46
+ "#{description[:controller].name}##{description[:action]}"
47
+ end
48
+
49
+ def ordered_call(call)
50
+ call = call.stringify_keys
51
+ ordered_call = OrderedHash.new
52
+ %w[verb path versions query request_data response_data code show_in_doc recorded].each do |k|
53
+ next unless call.has_key?(k)
54
+ ordered_call[k] = case call[k]
55
+ when ActiveSupport::HashWithIndifferentAccess
56
+ JSON.parse(call[k].to_json) # to_hash doesn't work recursively and I'm too lazy to write the recursion:)
57
+ else
58
+ call[k]
59
+ end
60
+ end
61
+ return ordered_call
62
+ end
63
+
64
+ def merge_old_new_examples
65
+ new_examples = self.load_new_examples
66
+ old_examples = self.load_old_examples
67
+ merged_examples = []
68
+ (new_examples.keys + old_examples.keys).uniq.each do |key|
69
+ if new_examples.has_key?(key)
70
+ records = new_examples[key]
71
+ else
72
+ records = old_examples[key]
73
+ end
74
+ merged_examples << [key, records.map { |r| ordered_call(r) } ]
75
+ end
76
+ return merged_examples
77
+ end
78
+
79
+ def load_new_examples
80
+ @collector.records.reduce({}) do |h, (method, calls)|
81
+ showed_in_versions = Set.new
82
+ # we have already shown some example
83
+ recorded_examples = calls.map do |call|
84
+ method_descriptions = Apipie.get_method_descriptions(call[:controller], call[:action])
85
+ call[:versions] = method_descriptions.map(&:version)
86
+
87
+ if call[:versions].any? { |v| ! showed_in_versions.include?(v) }
88
+ call[:versions].each { |v| showed_in_versions << v }
89
+ show_in_doc = 1
90
+ else
91
+ show_in_doc = 0
92
+ end
93
+ example = call.merge(:show_in_doc => show_in_doc.to_i, :recorded => true)
94
+ example
95
+ end
96
+ h.update(method => recorded_examples)
97
+ end
98
+ end
99
+
100
+ def load_old_examples
101
+ if File.exists?(@examples_file)
102
+ if defined? SafeYAML
103
+ return YAML.load_file(@examples_file, :safe=>false)
104
+ else
105
+ return YAML.load_file(@examples_file)
106
+ end
107
+ end
108
+ return {}
109
+ end
110
+
111
+ def logger
112
+ self.class.logger
113
+ end
114
+
115
+ def self.logger
116
+ Extractor.logger
117
+ end
118
+
119
+ def showable_in_doc?(call)
120
+ # we don't want to mess documentation with too large examples
121
+ if hash_nodes_count(call["request_data"]) + hash_nodes_count(call["response_data"]) > 100
122
+ return false
123
+ else
124
+ return 1
125
+ end
126
+ end
127
+
128
+ def hash_nodes_count(node)
129
+ case node
130
+ when Hash
131
+ 1 + (node.values.map { |v| hash_nodes_count(v) }.reduce(:+) || 0)
132
+ when Array
133
+ node.map { |v| hash_nodes_count(v) }.reduce(:+) || 1
134
+ else
135
+ 1
136
+ end
137
+ end
138
+
139
+ end
140
+
141
+ class ActionDescriptionUpdater
142
+
143
+ class ControllerNotFound < Exception; end
144
+
145
+ class ActionNotFound < Exception; end
146
+
147
+ def initialize(controller, action)
148
+ @controller = controller
149
+ @action = action
150
+ end
151
+
152
+ def generated?
153
+ old_header.include?(Apipie.configuration.generated_doc_disclaimer)
154
+ end
155
+
156
+ def update_apis(apis)
157
+ new_header = ""
158
+ new_header << Apipie.configuration.generated_doc_disclaimer << "\n" if generated?
159
+ new_header << generate_apis_code(apis)
160
+ new_header << ensure_line_breaks(old_header.lines).reject do |line|
161
+ line.include?(Apipie.configuration.generated_doc_disclaimer) ||
162
+ line =~ /^api/
163
+ end.join
164
+ overwrite_header(new_header)
165
+ end
166
+
167
+ def update_generated_description(desc)
168
+ if generated? || old_header.empty?
169
+ new_header = generate_code(desc)
170
+ overwrite_header(new_header)
171
+ end
172
+ end
173
+
174
+ def update(new_header)
175
+ overwrite_header(new_header)
176
+ end
177
+
178
+ def old_header
179
+ return @old_header if defined? @old_header
180
+ @old_header = lines_above_method[/^\s*?#{Regexp.escape(Apipie.configuration.generated_doc_disclaimer)}.*/m]
181
+ @old_header ||= lines_above_method[/^\s*?\b(api|params|error|example)\b.*/m]
182
+ @old_header ||= ""
183
+ @old_header.sub!(/\A\s*\n/,"")
184
+ @old_header = align_indented(@old_header)
185
+ end
186
+
187
+ def write!
188
+ File.open(controller_path, "w") { |f| f << @controller_content }
189
+ @changed=false
190
+ end
191
+
192
+ protected
193
+
194
+ def logger
195
+ Extractor.logger
196
+ end
197
+
198
+ def action_line
199
+ return @action_line if defined? @action_line
200
+ @action_line = ensure_line_breaks(controller_content.lines).find_index { |line| line =~ /def \b#{@action}\b/ }
201
+ raise ActionNotFound unless @action_line
202
+ @action_line
203
+ end
204
+
205
+ def controller_path
206
+ @controller_path ||= File.join(Rails.root, "app", "controllers", "#{@controller.controller_path}_controller.rb")
207
+ end
208
+
209
+ def controller_content
210
+ raise ControllerNotFound.new unless File.exists? controller_path
211
+ @controller_content ||= File.read(controller_path)
212
+ end
213
+
214
+ def controller_content=(new_content)
215
+ return if @controller_name == new_content
216
+ remove_instance_variable("@action_line")
217
+ remove_instance_variable("@old_header")
218
+ @controller_content=new_content
219
+ @changed = true
220
+ end
221
+
222
+ def generate_code(desc)
223
+ code = "#{Apipie.configuration.generated_doc_disclaimer}\n"
224
+ code << generate_apis_code(desc[:api])
225
+ code << generate_params_code(desc[:params])
226
+ code << generate_errors_code(desc[:errors])
227
+ return code
228
+ end
229
+
230
+ def generate_apis_code(apis)
231
+ code = ""
232
+ apis.sort_by {|a| a[:path] }.each do |api|
233
+ desc = api[:desc]
234
+ name = @controller.controller_name.gsub("_", " ")
235
+ desc ||= case @action.to_s
236
+ when "show", "create", "update", "destroy"
237
+ name = name.singularize
238
+ "#{@action.capitalize} #{name =~ /^[aeiou]/ ? "an" : "a"} #{name}"
239
+ when "index"
240
+ "List #{name}"
241
+ end
242
+
243
+ code << "api :#{api[:method]}, \"#{api[:path]}\""
244
+ code << ", \"#{desc}\"" if desc
245
+ code << "\n"
246
+ end
247
+ return code
248
+ end
249
+
250
+ def generate_params_code(params, indent = "")
251
+ code = ""
252
+ params.sort_by {|n,_| n }.each do |(name, desc)|
253
+ desc[:type] = (desc[:type] && desc[:type].first) || Object
254
+ code << "#{indent}param"
255
+ if name =~ /\W/
256
+ code << " :\"#{name}\""
257
+ else
258
+ code << " :#{name}"
259
+ end
260
+ code << ", #{desc[:type].inspect}"
261
+ if desc[:allow_nil]
262
+ code << ", :allow_nil => true"
263
+ end
264
+ if desc[:required]
265
+ code << ", :required => true"
266
+ end
267
+ if desc[:nested]
268
+ code << " do\n"
269
+ code << generate_params_code(desc[:nested], indent + " ")
270
+ code << "#{indent}end"
271
+ else
272
+ end
273
+ code << "\n"
274
+ end
275
+ code
276
+ end
277
+
278
+ def generate_errors_code(errors)
279
+ code = ""
280
+ errors.sort_by {|e| e[:code] }.each do |error|
281
+ code << "error :code => #{error[:code]}\n"
282
+ end
283
+ code
284
+ end
285
+
286
+ def align_indented(text)
287
+ shift_left = ensure_line_breaks(text.lines).map { |l| l[/^\s*/].size }.min
288
+ ensure_line_breaks(text.lines).map { |l| l[shift_left..-1] }.join
289
+ end
290
+
291
+ def overwrite_header(new_header)
292
+ overwrite_line_from = action_line
293
+ overwrite_line_to = action_line
294
+ unless old_header.empty?
295
+ overwrite_line_from -= ensure_line_breaks(old_header.lines).count
296
+ end
297
+ lines = ensure_line_breaks(controller_content.lines).to_a
298
+ indentation = lines[action_line][/^\s*/]
299
+ self.controller_content= (lines[0...overwrite_line_from] +
300
+ [new_header.gsub(/^/,indentation)] +
301
+ lines[overwrite_line_to..-1]).join
302
+ end
303
+
304
+ # returns all the lines before the method that might contain the restpi descriptions
305
+ # not bulletproof but working for conventional Ruby code
306
+ def lines_above_method
307
+ added_lines = []
308
+ lines_to_add = []
309
+ block_level = 0
310
+ ensure_line_breaks(controller_content.lines).first(action_line).reverse_each do |line|
311
+ if line =~ /\s*\bend\b\s*/
312
+ block_level += 1
313
+ end
314
+ if block_level > 0
315
+ lines_to_add << line
316
+ else
317
+ added_lines << line
318
+ end
319
+ if line =~ /\s*\b(module|class|def)\b /
320
+ break
321
+ end
322
+ if line =~ /do\s*(\|.*?\|)?\s*$/
323
+ block_level -= 1
324
+ if block_level == 0
325
+ added_lines.concat(lines_to_add)
326
+ lines_to_add = []
327
+ end
328
+ end
329
+ end
330
+ return added_lines.reverse.join
331
+ end
332
+
333
+ # this method would be totally useless unless some clever guy
334
+ # desided that it would be great idea to change a behavior of
335
+ # "".lines method to not include end of lines.
336
+ #
337
+ # For more details:
338
+ # https://github.com/puppetlabs/puppet/blob/0dc44695/lib/puppet/util/monkey_patches.rb
339
+ def ensure_line_breaks(lines)
340
+ if lines.to_a.size > 1 && lines.first !~ /\n\Z/
341
+ lines.map { |l| l !~ /\n\Z/ ? (l << "\n") : l }.to_enum
342
+ else
343
+ lines
344
+ end
345
+ end
346
+ end
347
+
348
+ # Used to keep apipie_examples.yml params in order
349
+ class OrderedHash < ActiveSupport::OrderedHash
350
+
351
+ def to_yaml_type
352
+ "!tag:yaml.org,2002:map"
353
+ end
354
+
355
+ def to_yaml(opts = {})
356
+ YAML.quick_emit(self, opts) do |out|
357
+ out.map(taguri) do |map|
358
+ each do |k, v|
359
+ map.add(k, v)
360
+ end
361
+ end
362
+ end
363
+ end
364
+ end
365
+
366
+ end
367
+ end
@@ -0,0 +1,52 @@
1
+ module Apipie
2
+ module Helpers
3
+ def markup_to_html(text)
4
+ return "" if text.nil?
5
+ if Apipie.configuration.markup.respond_to? :to_html
6
+ Apipie.configuration.markup.to_html(text.strip_heredoc)
7
+ else
8
+ text.strip_heredoc
9
+ end
10
+ end
11
+
12
+ attr_accessor :url_prefix
13
+
14
+ def request_script_name
15
+ Thread.current[:apipie_req_script_name] || ""
16
+ end
17
+
18
+ def request_script_name=(script_name)
19
+ Thread.current[:apipie_req_script_name] = script_name
20
+ end
21
+
22
+ def full_url(path)
23
+ unless @url_prefix
24
+ @url_prefix = ""
25
+ @url_prefix << request_script_name
26
+ @url_prefix << Apipie.configuration.doc_base_url
27
+ end
28
+ path = path.sub(/^\//,"")
29
+ ret = "#{@url_prefix}/#{path}"
30
+ ret.insert(0,"/") unless ret =~ /\A[.\/]/
31
+ ret.sub!(/\/*\Z/,"")
32
+ ret
33
+ end
34
+
35
+ def include_javascripts
36
+ %w[ bundled/jquery-1.7.2.js
37
+ bundled/bootstrap-collapse.js
38
+ bundled/prettify.js
39
+ apipie.js ].map do |file|
40
+ "<script type='text/javascript' src='#{Apipie.full_url("javascripts/#{file}")}'></script>"
41
+ end.join("\n").html_safe
42
+ end
43
+
44
+ def include_stylesheets
45
+ %w[ bundled/bootstrap.min.css
46
+ bundled/prettify.css
47
+ bundled/bootstrap-responsive.min.css ].map do |file|
48
+ "<link type='text/css' rel='stylesheet' href='#{Apipie.full_url("stylesheets/#{file}")}'/>"
49
+ end.join("\n").html_safe
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,48 @@
1
+ module Apipie
2
+
3
+ module Markup
4
+
5
+ class RDoc
6
+
7
+ def initialize
8
+ require 'rdoc'
9
+ require 'rdoc/markup/to_html'
10
+ if Gem::Version.new(::RDoc::VERSION) < Gem::Version.new('4.0.0')
11
+ @rdoc ||= ::RDoc::Markup::ToHtml.new()
12
+ else
13
+ @rdoc ||= ::RDoc::Markup::ToHtml.new(::RDoc::Options.new)
14
+ end
15
+ end
16
+
17
+ def to_html(text)
18
+ @rdoc.convert(text)
19
+ end
20
+
21
+ end
22
+
23
+ class Markdown
24
+
25
+ def initialize
26
+ require 'maruku'
27
+ end
28
+
29
+ def to_html(text)
30
+ Maruku.new(text).to_html
31
+ end
32
+
33
+ end
34
+
35
+ class Textile
36
+
37
+ def initialize
38
+ require 'RedCloth'
39
+ end
40
+
41
+ def to_html(text)
42
+ RedCloth.new(text).to_html
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+ end