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.
- data/Gemfile +0 -1
- data/Gemfile.lock +0 -10
- data/README.rdoc +18 -12
- data/app/controllers/restapi/restapis_controller.rb +28 -1
- data/app/views/layouts/restapi/restapi.html.erb +1 -0
- data/app/views/restapi/restapis/_params.html.erb +22 -0
- data/app/views/restapi/restapis/_params_plain.html.erb +16 -0
- data/app/views/restapi/restapis/index.html.erb +5 -5
- data/app/views/restapi/restapis/method.html.erb +8 -4
- data/app/views/restapi/restapis/plain.html.erb +70 -0
- data/app/views/restapi/restapis/resource.html.erb +16 -5
- data/app/views/restapi/restapis/static.html.erb +4 -6
- data/lib/restapi.rb +2 -1
- data/lib/restapi/application.rb +72 -22
- data/lib/restapi/client/generator.rb +104 -0
- data/lib/restapi/client/template/Gemfile.tt +5 -0
- data/lib/restapi/client/template/README.tt +3 -0
- data/lib/restapi/client/template/base.rb.tt +33 -0
- data/lib/restapi/client/template/bin.rb.tt +110 -0
- data/lib/restapi/client/template/cli.rb.tt +25 -0
- data/lib/restapi/client/template/cli_command.rb.tt +129 -0
- data/lib/restapi/client/template/client.rb.tt +10 -0
- data/lib/restapi/client/template/resource.rb.tt +17 -0
- data/lib/restapi/dsl_definition.rb +20 -2
- data/lib/restapi/error_description.rb +8 -2
- data/lib/restapi/extractor.rb +143 -0
- data/lib/restapi/extractor/collector.rb +113 -0
- data/lib/restapi/extractor/recorder.rb +122 -0
- data/lib/restapi/extractor/writer.rb +356 -0
- data/lib/restapi/helpers.rb +10 -5
- data/lib/restapi/markup.rb +12 -12
- data/lib/restapi/method_description.rb +52 -8
- data/lib/restapi/param_description.rb +6 -5
- data/lib/restapi/railtie.rb +1 -1
- data/lib/restapi/resource_description.rb +1 -1
- data/lib/restapi/restapi_module.rb +43 -0
- data/lib/restapi/validator.rb +70 -3
- data/lib/restapi/version.rb +1 -1
- data/lib/tasks/restapi.rake +120 -121
- data/restapi.gemspec +0 -2
- data/spec/controllers/restapis_controller_spec.rb +41 -6
- data/spec/controllers/users_controller_spec.rb +51 -12
- data/spec/dummy/app/controllers/application_controller.rb +0 -2
- data/spec/dummy/app/controllers/twitter_example_controller.rb +4 -9
- data/spec/dummy/app/controllers/users_controller.rb +13 -6
- data/spec/dummy/config/initializers/restapi.rb +7 -0
- data/spec/dummy/doc/restapi_examples.yml +28 -0
- metadata +49 -76
- 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
|