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
@@ -1,7 +1,7 @@
1
1
  module Restapi
2
2
 
3
3
  # method parameter description
4
- #
4
+ #
5
5
  # name - method name (show)
6
6
  # desc - description
7
7
  # required - boolean if required
@@ -11,7 +11,7 @@ module Restapi
11
11
  attr_reader :name, :desc, :required, :allow_nil, :validator
12
12
 
13
13
  attr_accessor :parent
14
-
14
+
15
15
  def initialize(name, *args, &block)
16
16
 
17
17
  if args.size > 1 || !args.first.is_a?(Hash)
@@ -20,15 +20,16 @@ module Restapi
20
20
  validator_type = nil
21
21
  end
22
22
  options = args.pop || {}
23
-
23
+
24
24
  @name = name
25
25
  @desc = Restapi.markup_to_html(options[:desc] || '')
26
26
  @required = options[:required] || false
27
27
  @allow_nil = options[:allow_nil] || false
28
-
28
+
29
29
  @validator = nil
30
30
  unless validator_type.nil?
31
- @validator = Validator::BaseValidator.find(self, validator_type, options, block)
31
+ @validator =
32
+ Validator::BaseValidator.find(self, validator_type, options, block)
32
33
  raise "Validator not found." unless validator
33
34
  end
34
35
  end
@@ -6,4 +6,4 @@ module Restapi
6
6
  end
7
7
  end
8
8
  end
9
- end
9
+ end
@@ -56,7 +56,7 @@ module Restapi
56
56
  end
57
57
 
58
58
  def doc_url
59
- "#{ENV["RAILS_RELATIVE_URL_ROOT"]}#{Restapi.configuration.doc_base_url}/#{@_id}"
59
+ Restapi.full_url(@_id)
60
60
  end
61
61
 
62
62
  def api_url; "#{Restapi.configuration.api_base_url}#{@_path}"; end
@@ -29,6 +29,8 @@ module Restapi
29
29
  attr_accessor :app_name, :app_info, :copyright, :markup,
30
30
  :validate, :api_base_url, :doc_base_url
31
31
 
32
+ alias_method :validate?, :validate
33
+
32
34
  # matcher to be used in Dir.glob to find controllers to be reloaded e.g.
33
35
  #
34
36
  # "#{Rails.root}/app/controllers/api/*.rb"
@@ -43,6 +45,47 @@ module Restapi
43
45
  @reload_controllers = Rails.env.development? unless defined? @reload_controllers
44
46
  return @reload_controllers && @api_controllers_matcher
45
47
  end
48
+
49
+ # set to true if you want to use pregenerated documentation cache and avoid
50
+ # generating the documentation on runtime (usefull for production
51
+ # environment).
52
+ # You can generate the cache by running
53
+ #
54
+ # rake restapi:cache
55
+ attr_accessor :use_cache
56
+ alias_method :use_cache?, :use_cache
57
+
58
+ attr_writer :cache_dir
59
+ def cache_dir
60
+ @cache_dir ||= File.join(Rails.root, "public", "restapi-cache")
61
+ end
62
+
63
+ # if there is not obvious reason why the DSL should be turned on (no
64
+ # validations, cache turned on etc.), it's disabled to avoid unneeded
65
+ # allocation. It you need the DSL for other reasons, you can force the
66
+ # activation.
67
+ attr_writer :force_dsl
68
+ def force_dsl?
69
+ @force_dsl
70
+ end
71
+
72
+ # array of controller names (strings) (might include actions as well)
73
+ # to be ignored # when extracting description form calls.
74
+ # e.g. %w[Api::CommentsController Api::PostsController#post]
75
+ attr_writer :ignored_by_recorder
76
+ def ignored_by_recorder
77
+ @ignored_by_recorder ||= []
78
+ @ignored_by_recorder.map(&:to_s)
79
+ end
80
+
81
+ # comment to put before docs that was generated automatically. It's used to
82
+ # determine if the description should be overwritten next recording.
83
+ # If you want to keep the documentation (prevent from overriding), remove
84
+ # the line above the docs.
85
+ attr_writer :generated_doc_disclaimer
86
+ def generated_doc_disclaimer
87
+ @generated_doc_disclaimer ||= "# DOC GENERATED AUTOMATICALLY: REMOVE THIS LINE TO PREVENT REGENARATING NEXT TIME"
88
+ end
46
89
 
47
90
  def app_info
48
91
  Restapi.markup_to_html(@app_info)
@@ -1,7 +1,8 @@
1
+ # -*- coding: utf-8 -*-
1
2
  module Restapi
2
-
3
+
3
4
  module Validator
4
-
5
+
5
6
  # to create new validator, inherit from Restapi::Validator::Base
6
7
  # and implement class method build and instance method validate
7
8
  class BaseValidator
@@ -191,7 +192,7 @@ module Restapi
191
192
 
192
193
  self.instance_exec(&@proc)
193
194
  end
194
-
195
+
195
196
  def validate(value)
196
197
  if @hash_params
197
198
  @hash_params.each do |k, p|
@@ -221,5 +222,71 @@ module Restapi
221
222
  end
222
223
  end
223
224
 
225
+
226
+ # special type of validator: we say that it's not specified
227
+ class UndefValidator < Restapi::Validator::BaseValidator
228
+
229
+ def validate(value)
230
+ true
231
+ end
232
+
233
+ def self.build(param_description, argument, options, block)
234
+ if argument == :undef
235
+ self.new(param_description)
236
+ end
237
+ end
238
+
239
+ def description
240
+ nil
241
+ end
242
+ end
243
+
244
+ class NumberValidator < Restapi::Validator::BaseValidator
245
+
246
+ def validate(value)
247
+ self.class.validate(value)
248
+ end
249
+
250
+ def self.build(param_description, argument, options, block)
251
+ if argument == :number
252
+ self.new(param_description)
253
+ end
254
+ end
255
+
256
+ def error
257
+ "Parameter #{param_name} expecting to be a number, got: #{@error_value}"
258
+ end
259
+
260
+ def description
261
+ "Has to be a number."
262
+ end
263
+
264
+ def self.validate(value)
265
+ value.to_s =~ /\A(0|[1-9]\d*)\Z$/
266
+ end
267
+ end
268
+
269
+ class BooleanValidator < Restapi::Validator::BaseValidator
270
+
271
+ def validate(value)
272
+ %w[true false].include?(value.to_s)
273
+ end
274
+
275
+ def self.build(param_description, argument, options, block)
276
+ if argument == :bool
277
+ self.new(param_description)
278
+ end
279
+ end
280
+
281
+ def error
282
+ "Parameter #{param_name} expecting to be a boolean value, got: #{@error_value}"
283
+ end
284
+
285
+ def description
286
+ "Has to be a boolean"
287
+ end
288
+ end
289
+
224
290
  end
225
291
  end
292
+
@@ -1,3 +1,3 @@
1
1
  module Restapi
2
- VERSION = '0.0.4'
2
+ VERSION = '0.0.5'
3
3
  end
@@ -1,157 +1,156 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'fileutils'
3
+ require 'restapi/client/generator'
4
+
1
5
  namespace :restapi do
2
6
 
3
7
  desc "Generate static documentation"
8
+ # You can specify OUT=output_base_file to have the following structure:
9
+ #
10
+ # output_base_file.html
11
+ # output_base_file-onepage.html
12
+ # output_base_file
13
+ # | - resource1.html
14
+ # | - resource1
15
+ # | - | - method1.html
16
+ # | - | - method2.html
17
+ # | - resource2.html
18
+ #
19
+ # By default OUT="#{Rails.root}/doc/apidoc"
4
20
  task :static => :environment do
5
-
6
- av = ActionView::Base.new(File.expand_path("../../../app/views", __FILE__))
7
-
8
- av.class_eval do
9
- include Restapi::RestapisHelper
21
+ with_loaded_documentation do
22
+ out = ENV["OUT"] || File.join(::Rails.root, 'doc', 'apidoc')
23
+ subdir = File.basename(out)
24
+
25
+ copy_jscss(out)
26
+
27
+ Restapi.url_prefix = "./#{subdir}"
28
+ doc = Restapi.to_json
29
+ generate_one_page(out, doc)
30
+ generate_plain_page(out, doc)
31
+ generate_index_page(out, doc)
32
+ Restapi.url_prefix = "../#{subdir}"
33
+ generate_resource_pages(out, doc)
34
+ Restapi.url_prefix = "../../#{subdir}"
35
+ generate_method_pages(out, doc)
10
36
  end
37
+ end
11
38
 
12
- Dir[File.join(Rails.root, "app", "controllers", "**","*.rb")].each {|f| load f}
13
-
14
- doc = Restapi.to_json()[:docs]
15
-
16
- # dir in public directory
17
- dir_path = File.join(::Rails.root.to_s, 'public', Restapi.configuration.doc_base_url)
18
- FileUtils.rm_r(dir_path) if File.directory?(dir_path)
19
- Dir.mkdir(dir_path)
39
+ desc "Generate cache to avoid production dependencies on markup languages"
40
+ task :cache => :environment do
41
+ with_loaded_documentation do
42
+ cache_dir = Restapi.configuration.cache_dir
43
+ file_base = File.join(cache_dir, Restapi.configuration.doc_base_url)
44
+ doc = Restapi.to_json
45
+ generate_index_page(file_base, doc, true)
46
+ generate_resource_pages(file_base, doc, true)
47
+ generate_method_pages(file_base, doc, true)
48
+ end
49
+ end
20
50
 
21
- copy_jscss(File.join(dir_path, Restapi.configuration.doc_base_url))
51
+ def renderer
52
+ ActionView::Base.new(File.expand_path("../../../app/views/restapi/restapis", __FILE__))
53
+ end
22
54
 
23
- Restapi.configuration.doc_base_url = Restapi.configuration.doc_base_url[1..-1]
24
- File.open(File.join(dir_path,'index.html'), "w") do |f|
55
+ def render_page(file_name, template, variables, layout = 'restapi')
56
+ av = renderer
57
+ File.open(file_name, "w") do |f|
58
+ variables.each do |var, val|
59
+ av.instance_variable_set("@#{var}", val)
60
+ end
25
61
  f.write av.render(
26
- :template => "restapi/restapis/static",
27
- :locals => {:doc => doc},
28
- :layout => 'layouts/restapi/restapi')
29
- puts File.join(dir_path,'index.html')
62
+ :template => "#{template}",
63
+ :layout => (layout && "../../layouts/restapi/#{layout}"))
30
64
  end
31
-
32
65
  end
33
66
 
34
- def copy_jscss(dest)
35
- src = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'app', 'public', 'restapi'))
36
- FileUtils.cp_r "#{src}/.", dest
37
- end
67
+ def generate_one_page(file_base, doc)
68
+ FileUtils.mkdir_p(File.dirname(file_base)) unless File.exists?(File.dirname(file_base))
38
69
 
70
+ render_page("#{file_base}-onepage.html", "static", {:doc => doc[:docs]})
71
+ end
39
72
 
40
- desc "Generate CLI client for API documented with restapi gem."
41
- task :client, [:api_base_url, :url_append] => [:environment] do |t, args|
73
+ def generate_plain_page(file_base, doc)
74
+ FileUtils.mkdir_p(File.dirname(file_base)) unless File.exists?(File.dirname(file_base))
42
75
 
43
- args.with_defaults :api_base_url => 'http://localhost:3000', :url_append => ''
76
+ render_page("#{file_base}-plain.html", "plain", {:doc => doc[:docs]}, nil)
77
+ end
44
78
 
45
- ActiveSupport::Dependencies.autoload_paths.each do |path|
46
- Dir[path + "/*.rb"].each { |file| require file }
47
- end
79
+ def generate_index_page(file_base, doc, include_json = false)
80
+ FileUtils.mkdir_p(File.dirname(file_base)) unless File.exists?(File.dirname(file_base))
48
81
 
49
- Dir.mkdir('clients') unless File.exists?('clients')
82
+ render_page("#{file_base}.html", "index", {:doc => doc[:docs]})
50
83
 
51
- doc = Restapi.to_json()[:docs]
84
+ File.open("#{file_base}.json", "w") { |f| f << doc.to_json } if include_json
85
+ end
52
86
 
53
- create_base doc, args
87
+ def generate_resource_pages(file_base, doc, include_json = false)
88
+ doc[:docs][:resources].each do |resource_name, _|
89
+ resource_file_base = File.join(file_base, resource_name.to_s)
90
+ FileUtils.mkdir_p(File.dirname(resource_file_base)) unless File.exists?(File.dirname(resource_file_base))
54
91
 
55
- create_cli doc
92
+ doc = Restapi.to_json(resource_name)
93
+ render_page("#{resource_file_base}.html", "resource", {:doc => doc[:docs],
94
+ :resource => doc[:docs][:resources].first})
95
+ File.open("#{resource_file_base}.json", "w") { |f| f << doc.to_json } if include_json
96
+ end
56
97
  end
57
98
 
58
- def ac(content, code, spaces = 0)
59
- content << " "*spaces << code << "\n"
60
- end
99
+ def generate_method_pages(file_base, doc, include_json = false)
100
+ doc[:docs][:resources].each do |resource_name, resource_params|
101
+ resource_params[:methods].each do |method|
102
+ method_file_base = File.join(file_base, resource_name.to_s, method[:name].to_s)
103
+ FileUtils.mkdir_p(File.dirname(method_file_base)) unless File.exists?(File.dirname(method_file_base))
61
104
 
62
- def create_base doc, args
63
- content = ""
64
- ac content, "require 'rubygems'"
65
- ac content, "require 'weary'"
66
- ac content, "require 'thread' unless defined? Mutex\n"
67
- ac content, "API_BASE_URL = ENV['API_URL'] || '#{args[:api_base_url]}'"
68
- ac content, "URL_APPEND = ENV['URL_APPEND'] || '#{args[:url_append]}'\n"
69
-
70
- doc[:resources].each do |key, resource|
71
-
72
- ac content, "class #{key.camelize}Client < Weary::Client"
73
- ac content, "domain API_BASE_URL", 2
74
- resource[:methods].each do |method|
75
- method[:apis].each do |api|
76
- ac content, "# #{api[:short_description]}", 2
77
- ac content, "#{api[:http_method].downcase} :#{method[:name]}, \"#{api[:api_url]}\#{URL_APPEND}\" do |resource|", 2
78
- required = []
79
- optional = []
80
-
81
- method[:params].each do |param|
82
- if param[:required]
83
- required << param[:name].to_sym
84
- else
85
- optional << param[:name].to_sym
86
- end
87
- end
88
- ac content, "resource.optional :#{optional.join(', :')}", 4 unless optional.blank?
89
- ac content, "resource.required :#{required.join(', :')}", 4 unless required.blank?
90
-
91
- ac content, "end", 2
92
- end
105
+ doc = Restapi.to_json(resource_name, method[:name])
106
+ render_page("#{method_file_base}.html", "method", {:doc => doc[:docs],
107
+ :resource => doc[:docs][:resources].first,
108
+ :method => doc[:docs][:resources].first[:methods].first})
109
+
110
+ File.open("#{method_file_base}.json", "w") { |f| f << doc.to_json } if include_json
93
111
  end
94
- ac content, "end\n"
95
112
  end
113
+ end
96
114
 
97
- File.open('clients/base.rb', 'w') { |f| f.write content }
115
+ def with_loaded_documentation
116
+ Restapi.configuration.use_cache = false # we don't want to skip DSL evaluation
117
+ Dir[File.join(Rails.root, "app", "controllers", "**","*.rb")].each {|f| load f}
118
+ yield
119
+ end
120
+
121
+ def copy_jscss(dest)
122
+ src = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'app', 'public', 'restapi'))
123
+ FileUtils.mkdir_p dest
124
+ FileUtils.cp_r "#{src}/.", dest
98
125
  end
99
126
 
100
- def create_cli doc
101
-
102
- content = ""
103
- ac content, "require 'rubygems'"
104
- ac content, "require 'thor'"
105
- ac content, "require './base'"
106
-
107
- doc[:resources].each do |key, resource|
108
- ac content, "class #{key.camelize} < Thor"
109
- # ac content, "$klasses['#{key}'] = #{key.camelize}", 2
110
- # ac content, "@client = RestClient::Resource.new \"\#{$API_URL}#{resource[:api_url]}\"\n", 2
111
-
112
- ac content, "no_tasks do", 2
113
- ac content, " def client", 2
114
- ac content, " @client ||= #{key.camelize}Client.new", 2
115
- ac content, " end", 2
116
- ac content, "end", 2
117
-
118
- resource[:methods].each do |method|
119
-
120
- method[:apis].each do |api|
121
-
122
- params = []
123
- method[:params].each do |param|
124
- ac content, "method_option :#{param[:name]},", 2
125
- ac content, ":required => #{param[:required] ? 'true' : 'false' },", 4
126
- ac content, ":desc => '#{plaintext(param[:description])}',", 4
127
- ac content, ":type => :#{param[:expected_type]}", 4
128
- # :type — :string, :hash, :array, :numeric, or :boolean
129
- params << param[:name]
130
- end
131
-
132
- ac content, "desc '#{method[:name]}', '#{api[:short_description]}'", 2
133
- ac content, "def #{method[:name]}", 2
134
- ac content, " resp = client.#{method[:name]}(options).perform do |response|", 2
135
- ac content, " if response.success?", 2
136
- ac content, " puts response.body", 2
137
- ac content, " else", 2
138
- ac content, " puts \"status: \#{response.status}\"", 2
139
- ac content, " puts response.body", 2
140
- ac content, " end", 2
141
- ac content, " end", 2
142
- ac content, " resp.status", 2
143
- ac content, "end\n", 2
144
-
145
- end
146
127
 
147
- end
148
- ac content, "end\n"
149
- end
128
+ desc "Generate CLI client for API documented with restapi gem."
129
+ task :client => [:environment] do |t, args|
130
+ Restapi.configuration.use_cache = false # we don't want to skip DSL evaluation
131
+ Restapi.api_controllers_paths.each { |file| require file }
150
132
 
151
- File.open('clients/cli.thor', 'w') { |f| f.write content }
133
+ Restapi::Client::Generator.start(Restapi.configuration.app_name)
152
134
  end
153
135
 
154
136
  def plaintext(text)
155
137
  text.gsub(/<.*?>/, '').gsub("\n",' ').strip
156
138
  end
139
+
140
+ desc "Update api description in controllers base on routes"
141
+ task :update_from_routes => [:environment] do
142
+ Restapi.configuration.force_dsl = true
143
+ ignored = Restapi.configuration.ignored_by_recorder
144
+ with_loaded_documentation do
145
+ apis_from_routes = Restapi::Extractor.apis_from_routes
146
+ apis_from_routes.each do |(controller, action), apis|
147
+ next if ignored.include?(controller)
148
+ next if ignored.include?("#{controller}##{action}")
149
+ Restapi::Extractor::Writer.update_action_description(controller.constantize, action) do |u|
150
+ u.update_apis(apis)
151
+ end
152
+ end
153
+ end
154
+ end
155
+
157
156
  end