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
@@ -16,6 +16,7 @@ module Restapi
16
16
  # Long description...
17
17
  # EOS
18
18
  def resource_description(options = {}, &block) #:doc:
19
+ return unless Restapi.active_dsl?
19
20
  Restapi.remove_resource_description(self)
20
21
  Restapi.define_resource_description(self, &block) if block_given?
21
22
  end
@@ -26,6 +27,7 @@ module Restapi
26
27
  # api :GET, "/resource_route", "short description",
27
28
  #
28
29
  def api(method, path, desc = nil) #:doc:
30
+ return unless Restapi.active_dsl?
29
31
  Restapi.add_method_description_args(method, path, desc)
30
32
  end
31
33
 
@@ -38,6 +40,7 @@ module Restapi
38
40
  # end
39
41
  #
40
42
  def desc(description) #:doc:
43
+ return unless Restapi.active_dsl?
41
44
  if Restapi.last_description
42
45
  raise "Double method description."
43
46
  end
@@ -45,9 +48,21 @@ module Restapi
45
48
  end
46
49
  alias :description :desc
47
50
 
51
+ # Reference other similar method
52
+ #
53
+ # api :PUT, '/articles/:id'
54
+ # see "articles#create"
55
+ # def update; end
56
+ def see(method_key)
57
+ return unless Restapi.active_dsl?
58
+ raise "'See' method called twice." if Restapi.last_see
59
+ Restapi.last_see = method_key
60
+ end
61
+
48
62
  # Show some example of what does the described
49
63
  # method return.
50
64
  def example(example) #:doc:
65
+ return unless Restapi.active_dsl?
51
66
  Restapi.add_example(example)
52
67
  end
53
68
 
@@ -55,12 +70,14 @@ module Restapi
55
70
  #
56
71
  # Example:
57
72
  # error :desc => "speaker is sleeping", :code => 500
73
+ # error 500, "speaker is sleeping"
58
74
  # def hello_world
59
75
  # return 500 if self.speaker.sleeping?
60
76
  # puts "hello world"
61
77
  # end
62
78
  #
63
- def error(args) #:doc:
79
+ def error(*args) #:doc:
80
+ return unless Restapi.active_dsl?
64
81
  Restapi.last_errors << Restapi::ErrorDescription.new(args)
65
82
  end
66
83
 
@@ -73,14 +90,15 @@ module Restapi
73
90
  # end
74
91
  #
75
92
  def param(param_name, *args, &block) #:doc:
93
+ return unless Restapi.active_dsl?
76
94
  Restapi.last_params << Restapi::ParamDescription.new(param_name, *args, &block)
77
95
  end
78
96
 
79
97
  # create method api and redefine newly added method
80
98
  def method_added(method_name) #:doc:
81
-
82
99
  super
83
100
 
101
+ return unless Restapi.active_dsl?
84
102
  return unless Restapi.restapi_provided?
85
103
 
86
104
  # remove method description if exists and create new one
@@ -3,9 +3,15 @@ module Restapi
3
3
  class ErrorDescription
4
4
 
5
5
  attr_reader :code, :description
6
-
6
+
7
7
  def initialize(args)
8
- args ||= []
8
+ if args.first.is_a? Hash
9
+ args = args.first
10
+ elsif args.count == 2
11
+ args = {:code => args.first, :description => args.second}
12
+ else
13
+ raise ArgumentError "RestapiError: Bad use of error method."
14
+ end
9
15
  @code = args[:code] || args['code']
10
16
  @description = args[:desc] || args[:description] || args['desc'] || args['description']
11
17
  end
@@ -0,0 +1,143 @@
1
+ require 'singleton'
2
+ require 'fileutils'
3
+ require 'set'
4
+ require 'yaml'
5
+ require 'restapi/extractor/recorder'
6
+ require 'restapi/extractor/writer'
7
+ require 'restapi/extractor/collector'
8
+
9
+ class Restapi::Railtie
10
+ if ENV["RESTAPI_RECORD"]
11
+ initializer 'restapi.extractor' do |app|
12
+ ActiveSupport.on_load :action_controller do
13
+ before_filter do |controller|
14
+ Restapi::Extractor.call_recorder.analyse_controller(controller)
15
+ end
16
+ end
17
+ app.middleware.use ::Restapi::Extractor::Recorder::Middleware
18
+ ActionController::TestCase::Behavior.instance_eval do
19
+ include Restapi::Extractor::Recorder::FunctionalTestRecording
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ module Restapi
26
+
27
+ module Extractor
28
+
29
+ class << self
30
+
31
+ def logger
32
+ Rails.logger
33
+ end
34
+
35
+ def call_recorder
36
+ Thread.current[:restapi_call_recorder] ||= Recorder.new
37
+ end
38
+
39
+ def call_finished
40
+ @collector ||= Collector.new
41
+ if record = call_recorder.record
42
+ @collector.handle_record(record)
43
+ end
44
+ ensure
45
+ Thread.current[:restapi_call_recorder] = nil
46
+ end
47
+
48
+ def write_docs
49
+ Writer.new(@collector).write_docs if @collector
50
+ end
51
+
52
+ def write_examples
53
+ Writer.new(@collector).write_examples if @collector
54
+ end
55
+
56
+ def apis_from_routes
57
+ return @apis_from_routes if @apis_from_routes
58
+
59
+ api_prefix = Restapi.configuration.api_base_url.sub(/\/$/,"")
60
+ all_routes = Rails.application.routes.routes.map do |r|
61
+ {
62
+ :verb => case r.verb
63
+ when Regexp then r.verb.source[/\w+/]
64
+ else r.verb.to_s
65
+ end,
66
+ :path => case
67
+ when r.path.respond_to?(:spec) then r.path.spec.to_s
68
+ else r.path.to_s
69
+ end,
70
+ :controller => r.requirements[:controller],
71
+ :action => r.requirements[:action]
72
+ }
73
+
74
+
75
+ end
76
+ api_routes = all_routes.find_all do |r|
77
+ r[:path].starts_with?(Restapi.configuration.api_base_url)
78
+ end
79
+
80
+ @apis_from_routes = Hash.new { |h, k| h[k] = [] }
81
+
82
+ api_routes.each do |route|
83
+ controller_path, action = route[:controller], route[:action]
84
+ next unless controller_path && action
85
+
86
+ controller = "#{controller_path}_controller".camelize
87
+
88
+ path = if /^#{Regexp.escape(api_prefix)}(.*)$/ =~ route[:path]
89
+ $1.sub!(/\(\.:format\)$/,"")
90
+ else
91
+ nil
92
+ end
93
+
94
+ if route[:verb].present?
95
+ @apis_from_routes[[controller, action]] << {:method => route[:verb], :path => path}
96
+ end
97
+ end
98
+ @apis_from_routes
99
+
100
+ apis_from_docs = Restapi.method_descriptions.reduce({}) do |h, (method, desc)|
101
+ apis = desc.apis.map do |api|
102
+ {:method => api.http_method, :path => api.api_url, :desc => api.short_description}
103
+ end
104
+ h.update(method => apis)
105
+ end
106
+
107
+ @apis_from_routes.each do |(controller, action), new_apis|
108
+ method_key = "#{controller.constantize.controller_name}##{action}"
109
+ old_apis = apis_from_docs[method_key] || []
110
+ new_apis.each do |new_api|
111
+ new_api[:path].sub!(/\(\.:format\)$/,"")
112
+ if old_api = old_apis.find { |api| api[:path] == "#{api_prefix}#{new_api[:path]}" }
113
+ new_api[:desc] = old_api[:desc]
114
+ end
115
+ end
116
+ end
117
+ @apis_from_routes
118
+ end
119
+
120
+ end
121
+ end
122
+ end
123
+
124
+ if ENV["RESTAPI_RECORD"]
125
+ Restapi.configuration.force_dsl = true
126
+ at_exit do
127
+ record_params, record_examples = false, false
128
+ case ENV["RESTAPI_RECORD"]
129
+ when "params" then record_params = true
130
+ when "examples" then record_examples = true
131
+ when "all" then record_params = true, record_examples = true
132
+ end
133
+
134
+ if record_examples
135
+ puts "Writing examples to a file"
136
+ Restapi::Extractor.write_examples
137
+ end
138
+ if record_params
139
+ puts "Updating auto-generated documentation"
140
+ Restapi::Extractor.write_docs
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,113 @@
1
+ module Restapi
2
+ module Extractor
3
+ class Collector
4
+ attr_reader :descriptions, :records
5
+
6
+ def initialize
7
+ @api_controllers_paths = Restapi.api_controllers_paths
8
+ @ignored = Restapi.configuration.ignored_by_recorder
9
+ @descriptions = Hash.new do |h, k|
10
+ h[k] = {:params => {}, :errors => Set.new}
11
+ end
12
+ @records = Hash.new { |h,k| h[k] = [] }
13
+ end
14
+
15
+ def controller_full_path(controller)
16
+ File.join(Rails.root, "app", "controllers", "#{controller.controller_path}_controller.rb")
17
+ end
18
+
19
+ def ignore_call?(record)
20
+ return true unless record[:controller]
21
+ return true if @ignored.include?(record[:controller].name)
22
+ return true if @ignored.include?("#{record[:controller].name}##{record[:action]}")
23
+ return true unless @api_controllers_paths.include?(controller_full_path(record[:controller]))
24
+ end
25
+
26
+ def handle_record(record)
27
+ add_to_records(record)
28
+ if ignore_call?(record)
29
+ Extractor.logger.info("REST_API: skipping #{record_to_s(record)}")
30
+ else
31
+ refine_description(record)
32
+ end
33
+ end
34
+
35
+ def add_to_records(record)
36
+ key = "#{record[:controller].controller_name}##{record[:action]}"
37
+ @records[key] << record
38
+ end
39
+
40
+ def refine_description(record)
41
+ description = @descriptions["#{record[:controller].name}##{record[:action]}"]
42
+ description[:controller] ||= record[:controller]
43
+ description[:action] ||= record[:action]
44
+
45
+ refine_errors_description(description, record)
46
+ refine_params_description(description[:params], record[:params])
47
+ end
48
+
49
+ def refine_errors_description(description, record)
50
+ if record[:code].to_i >= 300 && !description[:errors].any? { |e| e[:code].to_i == record[:code].to_i }
51
+ description[:errors] << {:code => record[:code]}
52
+ end
53
+ end
54
+
55
+ def refine_params_description(params_desc, recorded_params)
56
+ recorded_params.each do |key, value|
57
+ params_desc[key] ||= {}
58
+ param_desc = params_desc[key]
59
+
60
+ if value.nil?
61
+ param_desc[:allow_nil] = true
62
+ else
63
+ # we specify what type it might be. At the end the first type
64
+ # that left is taken as the more general one
65
+ unless param_desc[:type]
66
+ param_desc[:type] = [:bool, :number]
67
+ param_desc[:type] << Hash if value.is_a? Hash
68
+ param_desc[:type] << :undef
69
+ end
70
+
71
+ if param_desc[:type].first == :bool && (! [true, false].include?(value))
72
+ param_desc[:type].shift
73
+ end
74
+
75
+ if param_desc[:type].first == :number && (key.to_s !~ /id$/ || !Restapi::Validator::NumberValidator.validate(value))
76
+ param_desc[:type].shift
77
+ end
78
+ end
79
+
80
+ if value.is_a? Hash
81
+ param_desc[:nested] ||= {}
82
+ refine_params_description(param_desc[:nested], value)
83
+ end
84
+ end
85
+ end
86
+
87
+ def finalize_descriptions
88
+ @descriptions.each do |method, desc|
89
+ add_routes_info(desc)
90
+ end
91
+ return @descriptions
92
+ end
93
+
94
+ def add_routes_info(desc)
95
+ api_prefix = Restapi.configuration.api_base_url.sub(/\/$/,"")
96
+ desc[:api] = Restapi::Extractor.apis_from_routes[[desc[:controller].name, desc[:action]]]
97
+ if desc[:api]
98
+ desc[:params].each do |name, param|
99
+ if desc[:api].all? { |a| a[:path].include?(":#{name}") }
100
+ param[:required] = true
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ def record_to_s(record)
107
+ "#{record[:controller]}##{record[:action]}"
108
+ end
109
+
110
+ end
111
+ end
112
+ end
113
+
@@ -0,0 +1,122 @@
1
+ module Restapi
2
+ module Extractor
3
+ class Recorder
4
+ def initialize
5
+ @ignored_params = [:controller, :action]
6
+ end
7
+
8
+ def analyse_env(env)
9
+ @verb = env["REQUEST_METHOD"].to_sym
10
+ @path = env["PATH_INFO"].sub(/^\/*/,"/")
11
+ @query = env["QUERY_STRING"] unless env["QUERY_STRING"].blank?
12
+ @params = Rack::Utils.parse_nested_query(@query)
13
+ @params.merge!(env["action_dispatch.request.request_parameters"] || {})
14
+ if data = parse_data(env["rack.input"].read)
15
+ @request_data = data
16
+ end
17
+ end
18
+
19
+ def analyse_controller(controller)
20
+ @controller = controller.class
21
+ @action = controller.params[:action]
22
+ end
23
+
24
+ def analyse_response(response)
25
+ if response.last.respond_to?(:body) && data = parse_data(response.last.body)
26
+ @response_data = data
27
+ end
28
+ @code = response.first
29
+ end
30
+
31
+ def analyze_functional_test(test_context)
32
+ request, response = test_context.request, test_context.response
33
+ @verb = request.request_method.to_sym
34
+ @path = request.path
35
+ @params = request.request_parameters
36
+ if [:POST, :PUT].include?(@verb)
37
+ @request_data = @params
38
+ else
39
+ @query = request.query_string
40
+ end
41
+ @response_data = parse_data(response.body)
42
+ @code = response.code
43
+ end
44
+
45
+ def parse_data(data)
46
+ return nil if data.to_s =~ /^\s*$/
47
+ JSON.parse(data)
48
+ rescue Exception => e
49
+ data
50
+ end
51
+
52
+ def reformat_data(data)
53
+ parsed = parse_data(data)
54
+ case parsed
55
+ when nil
56
+ nil
57
+ when String
58
+ parsed
59
+ else
60
+ JSON.pretty_generate().gsub(/: \[\s*\]/,": []").gsub(/\{\s*\}/,"{}")
61
+ end
62
+ end
63
+
64
+ def record
65
+ if @controller
66
+ {:controller => @controller,
67
+ :action => @action,
68
+ :verb => @verb,
69
+ :path => @path,
70
+ :params => @params,
71
+ :query => @query,
72
+ :request_data => @request_data,
73
+ :response_data => @response_data,
74
+ :code => @code}
75
+ else
76
+ nil
77
+ end
78
+ end
79
+
80
+ protected
81
+
82
+ def api_description
83
+ end
84
+
85
+ class Middleware
86
+ def initialize(app)
87
+ @app = app
88
+ end
89
+
90
+ def call(env)
91
+ analyze(env) do
92
+ @app.call(env)
93
+ end
94
+ end
95
+
96
+ def analyze(env, &block)
97
+ Restapi::Extractor.call_recorder.analyse_env(env)
98
+ response = block.call
99
+ Restapi::Extractor.call_recorder.analyse_response(response)
100
+ response
101
+ ensure
102
+ Restapi::Extractor.call_finished
103
+ end
104
+ end
105
+
106
+ module FunctionalTestRecording
107
+ def self.included(base)
108
+ base.alias_method_chain :process, :api_recording
109
+ end
110
+
111
+ def process_with_api_recording(*args) # action, parameters = nil, session = nil, flash = nil, http_method = 'GET')
112
+ process_without_api_recording(*args)
113
+ Restapi::Extractor.call_recorder.analyze_functional_test(self)
114
+ ensure
115
+ Restapi::Extractor.call_finished
116
+ end
117
+ end
118
+
119
+ end
120
+
121
+ end
122
+ end