apipie-rails 0.0.7

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