betterdocs 0.2.0

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