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