betterdocs 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +5 -0
  3. data/.gitignore +11 -0
  4. data/.rspec +2 -0
  5. data/.travis.yml +9 -0
  6. data/COPYING +202 -0
  7. data/Gemfile +8 -0
  8. data/LICENSE +202 -0
  9. data/README.md +124 -0
  10. data/Rakefile +25 -0
  11. data/VERSION +1 -0
  12. data/betterdocs.gemspec +48 -0
  13. data/lib/betterdocs.rb +27 -0
  14. data/lib/betterdocs/controller_collector.rb +50 -0
  15. data/lib/betterdocs/dsl.rb +9 -0
  16. data/lib/betterdocs/dsl/common.rb +26 -0
  17. data/lib/betterdocs/dsl/controller.rb +9 -0
  18. data/lib/betterdocs/dsl/controller/action.rb +126 -0
  19. data/lib/betterdocs/dsl/controller/action/param.rb +25 -0
  20. data/lib/betterdocs/dsl/controller/action/response.rb +47 -0
  21. data/lib/betterdocs/dsl/controller/controller.rb +31 -0
  22. data/lib/betterdocs/dsl/controller/controller_base.rb +21 -0
  23. data/lib/betterdocs/dsl/json_params.rb +8 -0
  24. data/lib/betterdocs/dsl/json_params/param.rb +31 -0
  25. data/lib/betterdocs/dsl/json_type_mapper.rb +27 -0
  26. data/lib/betterdocs/dsl/naming.rb +32 -0
  27. data/lib/betterdocs/dsl/representer.rb +29 -0
  28. data/lib/betterdocs/dsl/result.rb +10 -0
  29. data/lib/betterdocs/dsl/result/collection_property.rb +9 -0
  30. data/lib/betterdocs/dsl/result/link.rb +37 -0
  31. data/lib/betterdocs/dsl/result/property.rb +53 -0
  32. data/lib/betterdocs/generator/config_shortcuts.rb +28 -0
  33. data/lib/betterdocs/generator/markdown.rb +151 -0
  34. data/lib/betterdocs/generator/markdown/templates/README.md.erb +9 -0
  35. data/lib/betterdocs/generator/markdown/templates/section.md.erb +132 -0
  36. data/lib/betterdocs/global.rb +143 -0
  37. data/lib/betterdocs/json_params_representer.rb +37 -0
  38. data/lib/betterdocs/json_params_representer_collector.rb +48 -0
  39. data/lib/betterdocs/mix_into_controller.rb +19 -0
  40. data/lib/betterdocs/rake_tasks.rb +5 -0
  41. data/lib/betterdocs/representer.rb +42 -0
  42. data/lib/betterdocs/result_representer.rb +68 -0
  43. data/lib/betterdocs/result_representer_collector.rb +82 -0
  44. data/lib/betterdocs/section.rb +6 -0
  45. data/lib/betterdocs/tasks/doc.rake +55 -0
  46. data/lib/betterdocs/version.rb +8 -0
  47. data/spec/controller_dsl_spec.rb +143 -0
  48. data/spec/generator/markdown_spec.rb +5 -0
  49. data/spec/json_params_representer_spec.rb +79 -0
  50. data/spec/json_type_mapper_spec.rb +33 -0
  51. data/spec/result_representer_dsl_spec.rb +183 -0
  52. data/spec/result_representer_spec.rb +182 -0
  53. data/spec/spec_helper.rb +19 -0
  54. metadata +234 -0
@@ -0,0 +1,9 @@
1
+ The <%= project_name %> API
2
+ ===========================
3
+
4
+ API ready for use
5
+ -----------------
6
+
7
+ <%- for secion in sections >
8
+ - [<%=section.name.to_s.camelcase](<%= section(section.name) %>.md)
9
+
@@ -0,0 +1,132 @@
1
+ <%- for action in section -%>
2
+
3
+ # <%= action.title %>
4
+
5
+ ```Rebol
6
+ <%= action.request %>
7
+ ```
8
+
9
+ <%= action.description %>
10
+
11
+ ## URL Parameters
12
+
13
+ <table>
14
+ <tr>
15
+ <th>Parameter</th>
16
+ <th>Example</th>
17
+ <th>Required</th>
18
+ <th>Description</th>
19
+ </tr>
20
+ <%- for param in action.params.values -%>
21
+ <tr>
22
+ <th align="left"><%= param.name %></th>
23
+ <td><code><%= param.value %></code></td>
24
+ <td><%= param.required ? 'yes' : 'no' %></td>
25
+ <td><%= param.description %></td>
26
+ </tr>
27
+ <%- end -%>
28
+ </table>
29
+
30
+ <%- if json_params = action.json_params.full? -%>
31
+ ## JSON Parameters
32
+
33
+ JSON parameters have to be provided in the body of the request with the
34
+ Content-Type header set to "application/json". The parameters are part of a
35
+ flat JSON document without any nesting. Some parameters are required, others
36
+ are optional.
37
+
38
+ ### Example
39
+
40
+ ```json
41
+ <%= action.json_params_example_json %>
42
+ ```
43
+
44
+ ### Supported Parameters
45
+
46
+ <table>
47
+ <tr>
48
+ <th>Parameter</th>
49
+ <th>Example</th>
50
+ <th>Types</th>
51
+ <th>Required</th>
52
+ <th>Description</th>
53
+ </tr>
54
+ <%- for param in action.json_params.values -%>
55
+ <tr>
56
+ <th align="left"><%= param.name %></th>
57
+ <td><code><%= param.value %></code></td>
58
+ <td><%= param.types * ?| -%></td>
59
+ <td><%= param.required ? 'yes' : 'no' %></td>
60
+ <td><%= param.description %></td>
61
+ </tr>
62
+ <%- end -%>
63
+ </table>
64
+ <%- end -%>
65
+
66
+ ## Response Attributes
67
+
68
+ <%- if properties = action.response.full?(:properties) -%>
69
+ <%- for (representer, properties) in properties.group_by(&:nesting_name) -%>
70
+ <%- if properties.first.nesting_name.blank? -%>
71
+ ### Root Attributes
72
+ <%- else -%>
73
+ ### <a id="<%= nn = properties.first.nesting_name %>" href="#<%= "#{nn}-ref" %>">↑Nested Attributes: <%= nn %></a>
74
+ <%- end -%>
75
+
76
+ <table>
77
+ <tr>
78
+ <th>Attribute</th>
79
+ <th>Types</th>
80
+ <th>Example</th>
81
+ <th>Description</th>
82
+ </tr>
83
+ <%- for property in properties -%>
84
+ <tr>
85
+ <%- if r = property.sub_representer? and r.docs.properties.full? -%>
86
+ <th align="left" style="white-space: nowrap">
87
+ <a id="<%= fn = property.full_name; "#{fn}-ref" %>" href="#<%= fn %>">
88
+ ↓<%= property.full_name %>
89
+ </a>
90
+ </th>
91
+ <%- else -%>
92
+ <th align="left"><%= property.full_name %></th>
93
+ <%- end -%>
94
+ <td><%= property.types * ' &#124; ' %></td>
95
+ <td><%= property.example %></td>
96
+ <td><%= property.description %></td>
97
+ </tr>
98
+ <%- end -%>
99
+ </table>
100
+ <%- end -%>
101
+ <%- else -%>
102
+ <th colspan="4">No response example defined</th>
103
+ <%- end -%>
104
+ </table>
105
+
106
+ ## Response Links
107
+
108
+ <table>
109
+ <tr>
110
+ <th>Linkname</th>
111
+ <th>Description</th>
112
+ </tr>
113
+
114
+ <%- if links = action.response.full?(:links) -%>
115
+ <%- for link in links.group_by(&:nesting_name).values.reduce(&:concat) -%>
116
+ <tr>
117
+ <th align="left"><%= link.full_name %></th>
118
+ <td><%= link.description %></td>
119
+ </tr>
120
+ <%- end -%>
121
+ <%- else -%>
122
+ <th colspan="2">No response example defined</th>
123
+ <%- end -%>
124
+ </table>
125
+
126
+ ## Response Example
127
+
128
+ ```json
129
+ <%= JSON.pretty_generate(action.response) %>
130
+ ```
131
+
132
+ <%- end -%>
@@ -0,0 +1,143 @@
1
+ module Betterdocs
2
+ module Global
3
+ class << self
4
+ extend Tins::DSLAccessor
5
+
6
+ dsl_accessor :project_name, 'Project' # Name of the project
7
+
8
+ dsl_accessor :api_prefix, 'api' # Prefix that denotes the api namespace in URLs
9
+
10
+ dsl_accessor :api_controllers do
11
+ Dir[Rails.root.join("app/controllers/#{api_prefix}/**/*_controller.rb")]
12
+ end
13
+
14
+ dsl_accessor :platform_protocol, 'http' # Not used atm
15
+
16
+ dsl_accessor :platform_host, 'localhost:3000' # Actually host with port, but rails seems to be confused about the concept
17
+
18
+ dsl_accessor :api_protocol do platform_protocol end # Protocol the API understands
19
+
20
+ dsl_accessor :api_host do platform_host end # Hostname of the API (eventuallly with port number)
21
+
22
+ dsl_accessor :asset_protocol do platform_protocol end # Reserved
23
+
24
+ dsl_accessor :asset_host do platform_host end # Rails asset host
25
+
26
+ dsl_accessor :api_default_format, 'json'
27
+
28
+ def api_base_url
29
+ "#{api_protocol}://#{api_host}/#{api_prefix}"
30
+ end
31
+
32
+ dsl_accessor :api_url_options do
33
+ { protocol: api_protocol, host: api_host, format: api_default_format }
34
+ end
35
+
36
+ dsl_accessor :templates_directory # Template directory, where customised templates live if any exist
37
+
38
+ dsl_accessor :output_directory, 'api_docs' # Output directory, where the api docs are created
39
+
40
+ dsl_accessor :publish_git # URL to the git repo to which the docs are pushed
41
+
42
+ dsl_accessor :ignore do [] end # All lines of the .gitignore file as an array
43
+
44
+ def assets
45
+ @assets ||= {}
46
+ end
47
+ private :assets
48
+
49
+ # Defines an asset for the file at +path+. If +to+ was given it will be
50
+ # copied to this path (it includes the basename) below
51
+ # +templates_directory+ in the output, otherwise it will be copied
52
+ # directly to +templates_directory+.
53
+ def asset(path, to: :root)
54
+ if destination = to.ask_and_send(:to_str)
55
+ assets[path.to_s] = destination
56
+ elsif to == :root
57
+ assets[path.to_s] = to
58
+ else
59
+ raise ArgumentError, "keyword argument to needs to be a string or :root"
60
+ end
61
+ end
62
+
63
+ # Maps the assets original source path to its destination path in the
64
+ # output by yielding to every asset's source/destination pair.
65
+ def each_asset
66
+ for path in assets.keys
67
+ path = path.to_s
68
+ if destination = assets[path]
69
+ if destination == :root && output_directory
70
+ yield path, File.join(output_directory.to_s, File.basename(path))
71
+ else
72
+ yield path, File.join(output_directory.to_s, destination.to_str)
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ def configure(&block)
79
+ instance_eval(&block)
80
+ self
81
+ end
82
+
83
+ def config
84
+ if block_given?
85
+ yield self
86
+ else
87
+ self
88
+ end
89
+ end
90
+
91
+ def all_docs
92
+ Dir[api_prefix.to_s + '/**/*_controller.rb'].each_with_object([]) do |cf, all|
93
+ controller_name = cf.sub(/\.rb$/, '').camelcase
94
+ controller =
95
+ begin
96
+ controller_name.constantize
97
+ rescue NameError => e
98
+ STDERR.puts "Skipping #{cf.inspect}, #{e.class}: #{e}"
99
+ next
100
+ end
101
+ if docs = controller.ask_and_send(:docs)
102
+ all << docs
103
+ else
104
+ STDERR.puts "Skipping #{cf.inspect}, #{controller_name.inspect} doesn't respond to :docs method"
105
+ end
106
+ end
107
+ end
108
+
109
+ def actions
110
+ all_docs.reduce([]) { |a, d| a.concat(d.actions) }
111
+ end
112
+
113
+ def sections
114
+ @sections and return @sections
115
+ Dir.chdir Rails.root.join('app/controllers') do
116
+ actions.each_with_object(@sections = {}) do |action, sections|
117
+ sections[action.section] ||= Section.new(action.section)
118
+ sections[action.section] << action
119
+ end
120
+ end
121
+ @sections.freeze
122
+ end
123
+
124
+ def sections_clear
125
+ @sections = nil
126
+ self
127
+ end
128
+
129
+ def section(name)
130
+ sections[name] if sections.key?(name)
131
+ end
132
+
133
+ def url_helpers
134
+ Betterdocs.rails.application.routes.url_helpers
135
+ end
136
+
137
+ def url_for(options = {})
138
+ Betterdocs.rails.application.routes.url_for(
139
+ options | Betterdocs::Global.config.api_url_options)
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,37 @@
1
+ module Betterdocs::JsonParamsRepresenter
2
+ extend ActiveSupport::Concern
3
+ include Betterdocs::Representer
4
+
5
+ module ClassMethods
6
+ def build_result_object
7
+ ActionController::Parameters.new
8
+ end
9
+
10
+ def hashify(object)
11
+ super do |result|
12
+ assign_params result, object
13
+ end
14
+ end
15
+
16
+ def docs
17
+ @docs ||= Betterdocs::JsonParamsRepresenterCollector.new
18
+ end
19
+
20
+ def assign_params(result, object)
21
+ for param in params
22
+ param.assign(result, object)
23
+ end
24
+ end
25
+ private :assign_params
26
+
27
+ def params
28
+ @params ||= Set.new
29
+ end
30
+
31
+ def param(name, **options, &block)
32
+ d = doc(:param, name, **options, &block) and
33
+ params << d
34
+ self
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,48 @@
1
+ module Betterdocs
2
+ class JsonParamsRepresenterCollector
3
+ def initialize
4
+ @params = {}
5
+ end
6
+
7
+ attr_reader :params
8
+
9
+ def param(param_name)
10
+ param_name = param_name.to_sym
11
+ @params[param_name]
12
+ end
13
+
14
+ def add_element(representer, type, name, **options, &block)
15
+ element = build_element(representer, type, name, options, &block)
16
+ element.add_to_collector(self)
17
+ end
18
+
19
+ def representer
20
+ @params.values.find { |v|
21
+ v.representer and break v.representer
22
+ }
23
+ end
24
+
25
+ def to_s
26
+ result = "*** #{representer} ***\n"
27
+ if params = @params.values.full?
28
+ result << "\nProperties:"
29
+ params.each_with_object(result) do |param, r|
30
+ r << "\n#{param.full_name}: (#{param.types * '|'}): #{param.description}\n"
31
+ end
32
+ end
33
+ result
34
+ end
35
+
36
+ private
37
+
38
+ def build_element(representer, type, *args, &block)
39
+ begin
40
+ element = Dsl::JsonParams.const_get(type.to_s.camelcase)
41
+ rescue NameError => e
42
+ raise ArgumentError, "unknown documentation element type #{type.inspect}"
43
+ end
44
+ element.new(representer, *args, &block)
45
+ end
46
+ end
47
+ end
48
+
@@ -0,0 +1,19 @@
1
+ module Betterdocs
2
+ module MixIntoController
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def method_added(name)
7
+ docs.configure_current_element(name)
8
+ end
9
+
10
+ def doc(type, &block)
11
+ docs.add_element(self, type, &block)
12
+ end
13
+
14
+ def docs
15
+ @docs ||= ControllerCollector.new
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ class RakeTasks < Rails::Railtie
2
+ rake_tasks do
3
+ Dir[File.join(File.dirname(__FILE__), 'tasks/*.rake')].each { |f| load f }
4
+ end
5
+ end
@@ -0,0 +1,42 @@
1
+ require 'action_controller'
2
+
3
+ module Betterdocs::Representer
4
+ extend ActiveSupport::Concern
5
+
6
+ def as_json(*)
7
+ singleton_class.ancestors.find do |c|
8
+ c != singleton_class && c < Betterdocs::Representer
9
+ end.hashify(self)
10
+ end
11
+
12
+ def to_json(*a)
13
+ JSON::generate(as_json, *a)
14
+ end
15
+
16
+ module ClassMethods
17
+ def apply(object)
18
+ object.extend self
19
+ end
20
+
21
+ def build_result_object
22
+ {}
23
+ end
24
+
25
+ def hashify(object, &block)
26
+ apply(object)
27
+ result = build_result_object
28
+ instance_exec(result, &block)
29
+ result
30
+ end
31
+
32
+ def doc(type, name, **options, &block)
33
+ docs.add_element(self, type, name, options, &block)
34
+ end
35
+
36
+ def object_name(*) end
37
+
38
+ def docs
39
+ raise NotImplementedError, 'has to be implemented in including module'
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,68 @@
1
+ require 'set'
2
+ require 'betterdocs/representer'
3
+
4
+ module Betterdocs::ResultRepresenter
5
+ extend ActiveSupport::Concern
6
+ include Betterdocs::Dsl::Common
7
+ include Betterdocs::Representer
8
+
9
+ module ClassMethods
10
+ def hashify(object)
11
+ super do |result|
12
+ assign_properties result, object
13
+ assign_links result, object
14
+ end
15
+ end
16
+ def doc(type, name, **options, &block)
17
+ docs.add_element(self, type, name, options, &block)
18
+ end
19
+
20
+ def docs
21
+ @docs ||= Betterdocs::ResultRepresenterCollector.new
22
+ end
23
+
24
+ def assign_links(result, object)
25
+ result['links'] = []
26
+ for link in links
27
+ link.assign(result, object)
28
+ end
29
+ end
30
+ private :assign_links
31
+
32
+ def assign_properties(result, object)
33
+ for property in properties
34
+ property.assign(result, object)
35
+ end
36
+ end
37
+ private :assign_properties
38
+
39
+ def properties
40
+ @properties ||= Set.new
41
+ end
42
+
43
+ def property(name, **options, &block)
44
+ d = doc(:property, name, **options, &block) and
45
+ properties << d
46
+ self
47
+ end
48
+
49
+ def collection(name, **options, &block)
50
+ d = doc(:collection_property, name, **options, &block) and
51
+ properties << d
52
+ self
53
+ end
54
+
55
+ def links
56
+ @links ||= Set.new
57
+ end
58
+
59
+ def link(name, **options, &block)
60
+ d = doc(:link, name, **options, &block) and links << d
61
+ self
62
+ end
63
+
64
+ def api_url_for(options = {})
65
+ Betterdocs::Global.url_for(options)
66
+ end
67
+ end
68
+ end