brainstem 1.0.0.pre.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +383 -32
  5. data/bin/brainstem +6 -0
  6. data/brainstem.gemspec +2 -0
  7. data/docs/api_doc_generator.markdown +175 -0
  8. data/docs/brainstem_executable.markdown +32 -0
  9. data/docs/docgen.png +0 -0
  10. data/docs/docgen_ascii.txt +63 -0
  11. data/docs/executable.png +0 -0
  12. data/docs/executable_ascii.txt +10 -0
  13. data/lib/brainstem/api_docs.rb +146 -0
  14. data/lib/brainstem/api_docs/abstract_collection.rb +116 -0
  15. data/lib/brainstem/api_docs/atlas.rb +158 -0
  16. data/lib/brainstem/api_docs/builder.rb +167 -0
  17. data/lib/brainstem/api_docs/controller.rb +122 -0
  18. data/lib/brainstem/api_docs/controller_collection.rb +40 -0
  19. data/lib/brainstem/api_docs/endpoint.rb +234 -0
  20. data/lib/brainstem/api_docs/endpoint_collection.rb +58 -0
  21. data/lib/brainstem/api_docs/exceptions.rb +8 -0
  22. data/lib/brainstem/api_docs/formatters/abstract_formatter.rb +64 -0
  23. data/lib/brainstem/api_docs/formatters/markdown/controller_formatter.rb +76 -0
  24. data/lib/brainstem/api_docs/formatters/markdown/endpoint_collection_formatter.rb +73 -0
  25. data/lib/brainstem/api_docs/formatters/markdown/endpoint_formatter.rb +169 -0
  26. data/lib/brainstem/api_docs/formatters/markdown/helper.rb +76 -0
  27. data/lib/brainstem/api_docs/formatters/markdown/presenter_formatter.rb +200 -0
  28. data/lib/brainstem/api_docs/introspectors/abstract_introspector.rb +100 -0
  29. data/lib/brainstem/api_docs/introspectors/rails_introspector.rb +232 -0
  30. data/lib/brainstem/api_docs/presenter.rb +225 -0
  31. data/lib/brainstem/api_docs/presenter_collection.rb +97 -0
  32. data/lib/brainstem/api_docs/resolver.rb +73 -0
  33. data/lib/brainstem/api_docs/sinks/abstract_sink.rb +37 -0
  34. data/lib/brainstem/api_docs/sinks/controller_presenter_multifile_sink.rb +93 -0
  35. data/lib/brainstem/api_docs/sinks/stdout_sink.rb +44 -0
  36. data/lib/brainstem/cli.rb +146 -0
  37. data/lib/brainstem/cli/abstract_command.rb +97 -0
  38. data/lib/brainstem/cli/generate_api_docs_command.rb +169 -0
  39. data/lib/brainstem/concerns/controller_dsl.rb +300 -0
  40. data/lib/brainstem/concerns/controller_param_management.rb +30 -9
  41. data/lib/brainstem/concerns/formattable.rb +38 -0
  42. data/lib/brainstem/concerns/inheritable_configuration.rb +3 -2
  43. data/lib/brainstem/concerns/optional.rb +43 -0
  44. data/lib/brainstem/concerns/presenter_dsl.rb +76 -15
  45. data/lib/brainstem/controller_methods.rb +6 -3
  46. data/lib/brainstem/dsl/association.rb +6 -3
  47. data/lib/brainstem/dsl/associations_block.rb +6 -3
  48. data/lib/brainstem/dsl/base_block.rb +2 -4
  49. data/lib/brainstem/dsl/conditional.rb +7 -3
  50. data/lib/brainstem/dsl/conditionals_block.rb +4 -4
  51. data/lib/brainstem/dsl/configuration.rb +184 -8
  52. data/lib/brainstem/dsl/field.rb +6 -3
  53. data/lib/brainstem/dsl/fields_block.rb +2 -3
  54. data/lib/brainstem/help_text.txt +8 -0
  55. data/lib/brainstem/presenter.rb +27 -6
  56. data/lib/brainstem/presenter_validator.rb +5 -2
  57. data/lib/brainstem/time_classes.rb +1 -1
  58. data/lib/brainstem/version.rb +1 -1
  59. data/spec/brainstem/api_docs/abstract_collection_spec.rb +156 -0
  60. data/spec/brainstem/api_docs/atlas_spec.rb +353 -0
  61. data/spec/brainstem/api_docs/builder_spec.rb +100 -0
  62. data/spec/brainstem/api_docs/controller_collection_spec.rb +92 -0
  63. data/spec/brainstem/api_docs/controller_spec.rb +225 -0
  64. data/spec/brainstem/api_docs/endpoint_collection_spec.rb +144 -0
  65. data/spec/brainstem/api_docs/endpoint_spec.rb +346 -0
  66. data/spec/brainstem/api_docs/formatters/abstract_formatter_spec.rb +30 -0
  67. data/spec/brainstem/api_docs/formatters/markdown/controller_formatter_spec.rb +126 -0
  68. data/spec/brainstem/api_docs/formatters/markdown/endpoint_collection_formatter_spec.rb +85 -0
  69. data/spec/brainstem/api_docs/formatters/markdown/endpoint_formatter_spec.rb +261 -0
  70. data/spec/brainstem/api_docs/formatters/markdown/helper_spec.rb +100 -0
  71. data/spec/brainstem/api_docs/formatters/markdown/presenter_formatter_spec.rb +485 -0
  72. data/spec/brainstem/api_docs/introspectors/abstract_introspector_spec.rb +192 -0
  73. data/spec/brainstem/api_docs/introspectors/rails_introspector_spec.rb +170 -0
  74. data/spec/brainstem/api_docs/presenter_collection_spec.rb +84 -0
  75. data/spec/brainstem/api_docs/presenter_spec.rb +519 -0
  76. data/spec/brainstem/api_docs/resolver_spec.rb +72 -0
  77. data/spec/brainstem/api_docs/sinks/abstract_sink_spec.rb +16 -0
  78. data/spec/brainstem/api_docs/sinks/controller_presenter_multifile_sink_spec.rb +56 -0
  79. data/spec/brainstem/api_docs/sinks/stdout_sink_spec.rb +22 -0
  80. data/spec/brainstem/api_docs_spec.rb +58 -0
  81. data/spec/brainstem/cli/abstract_command_spec.rb +91 -0
  82. data/spec/brainstem/cli/generate_api_docs_command_spec.rb +125 -0
  83. data/spec/brainstem/cli_spec.rb +67 -0
  84. data/spec/brainstem/concerns/controller_dsl_spec.rb +471 -0
  85. data/spec/brainstem/concerns/controller_param_management_spec.rb +36 -16
  86. data/spec/brainstem/concerns/formattable_spec.rb +30 -0
  87. data/spec/brainstem/concerns/inheritable_configuration_spec.rb +104 -4
  88. data/spec/brainstem/concerns/optional_spec.rb +48 -0
  89. data/spec/brainstem/concerns/presenter_dsl_spec.rb +202 -31
  90. data/spec/brainstem/dsl/association_spec.rb +18 -2
  91. data/spec/brainstem/dsl/conditional_spec.rb +25 -2
  92. data/spec/brainstem/dsl/configuration_spec.rb +1 -1
  93. data/spec/brainstem/dsl/field_spec.rb +18 -2
  94. data/spec/brainstem/presenter_collection_spec.rb +10 -2
  95. data/spec/brainstem/presenter_spec.rb +32 -0
  96. data/spec/brainstem/presenter_validator_spec.rb +12 -7
  97. data/spec/dummy/rails.rb +49 -0
  98. data/spec/shared/atlas_taker.rb +18 -0
  99. data/spec/shared/formattable.rb +14 -0
  100. data/spec/spec_helper.rb +2 -0
  101. data/spec/spec_helpers/db.rb +1 -1
  102. data/spec/spec_helpers/presenters.rb +20 -14
  103. metadata +106 -6
@@ -0,0 +1,100 @@
1
+ require 'brainstem/concerns/optional'
2
+
3
+ module Brainstem
4
+ module ApiDocs
5
+ module Introspectors
6
+ class AbstractIntrospector
7
+ include Brainstem::Concerns::Optional
8
+
9
+ def valid_options
10
+ [ ]
11
+ end
12
+
13
+ # Returns a new instance of the introspector with the environment
14
+ # loaded, ready for introspection.
15
+ #
16
+ # @param [Hash] options arguments to pass on to the instance
17
+ # @return [AbstractIntrospector] the loaded instance
18
+ def self.with_loaded_environment(options = {})
19
+ new(options).tap(&:load_environment!)
20
+ end
21
+
22
+
23
+ # Override to return a collection of all controller classes.
24
+ #
25
+ # @return [Array<Class>] all controller classes to document
26
+ def controllers
27
+ raise NotImplementedError
28
+ end
29
+
30
+
31
+ # Override to return a collection of all presenter classes.
32
+ #
33
+ # @return [Array<Class>] all presenter classes to document
34
+ def presenters
35
+ raise NotImplementedError
36
+ end
37
+
38
+
39
+ # Override to return a collection of hashes with the minimum following
40
+ # keys:
41
+ #
42
+ # +:path+ - the relative path (i.e. the endpoint)
43
+ # +:controller+ - the managing controller
44
+ # +:action+ - the managing action
45
+ # +:http_method+ - an array of the HTTP methods this route is available on.
46
+ #
47
+ def routes
48
+ raise NotImplementedError
49
+ end
50
+
51
+
52
+ # Provides both a sanity check to ensure that output confirms to
53
+ # interface and also confirms that there is actually something to
54
+ # generate docs for.
55
+ #
56
+ # @return [Boolean] Whether the Introspector is valid
57
+ def valid?
58
+ valid_controllers? && valid_presenters? && valid_routes?
59
+ end
60
+
61
+
62
+ #######################################################################
63
+ private
64
+ #######################################################################
65
+
66
+ # Don't allow instantiation through 'new'. We want to ensure that
67
+ # instantiation happens through +with_loaded_environment.
68
+ private_class_method :new
69
+
70
+
71
+ # Loads the host application environment.
72
+ # @api private
73
+ def load_environment!
74
+ raise NotImplementedError
75
+ end
76
+
77
+
78
+ def valid_controllers?
79
+ controllers.is_a?(Array) &&
80
+ controllers.count > 0 &&
81
+ controllers.all? {|c| c.class == Class }
82
+ end
83
+
84
+ def valid_presenters?
85
+ presenters.is_a?(Array) &&
86
+ presenters.all? {|p| p.class == Class }
87
+ end
88
+
89
+ def valid_routes?
90
+ routes.is_a?(Array) &&
91
+ routes.count > 0 &&
92
+ routes.all? do |r|
93
+ r.is_a?(Hash) &&
94
+ ([:path, :controller, :action, :http_methods] - r.keys).empty?
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,232 @@
1
+ require 'brainstem/api_docs/introspectors/abstract_introspector'
2
+ require 'brainstem/api_docs/exceptions'
3
+
4
+ # For 'constantize'
5
+ require 'active_support/inflector/methods'
6
+
7
+ module Brainstem
8
+ module ApiDocs
9
+ module Introspectors
10
+ class RailsIntrospector < AbstractIntrospector
11
+
12
+ #
13
+ # Loads ./config/environment.rb (by default) and eager loads all
14
+ # classes (otherwise +#descendants+ returns an empty set).
15
+ #
16
+ def load_environment!
17
+ load rails_environment_file unless env_already_loaded?
18
+ ::Rails.application.eager_load!
19
+
20
+ validate!
21
+ rescue LoadError => e
22
+ raise IncorrectIntrospectorForAppException,
23
+ "Hosting app does not appear to be a Rails app." +
24
+ "You may have to manually specify an Introspector (#{e.message})."
25
+ end
26
+
27
+
28
+ #
29
+ # Returns a list of presenters that descend from the base presenter
30
+ # class.
31
+ #
32
+ # @return [Array<Class>] an array of descendant classes
33
+ #
34
+ def presenters
35
+ base_presenter_class.constantize.descendants
36
+ end
37
+
38
+
39
+ #
40
+ # Returns a list of controllers that descend from the base controller
41
+ # class.
42
+ #
43
+ # @return [Array<Class>] an array of descendant classes
44
+ #
45
+ def controllers
46
+ base_controller_class.constantize.descendants
47
+ end
48
+
49
+
50
+ #
51
+ # Returns an array of hashes describing the endpoints of the
52
+ # application. See +routes_method+ for the keys of those hashes.
53
+ #
54
+ # @see #routes_method
55
+ #
56
+ # @return [Array<Hash>] each route defined on the hosting app
57
+ def routes
58
+ routes_method.call
59
+ end
60
+
61
+
62
+ #######################################################################
63
+ private
64
+ #######################################################################
65
+
66
+
67
+ def valid_options
68
+ super | [
69
+ :routes_method,
70
+ :rails_environment_file,
71
+ :base_presenter_class,
72
+ :base_controller_class,
73
+ ]
74
+ end
75
+
76
+
77
+ #
78
+ # Used to short-circuit loading if Rails is already loaded, which
79
+ # reduces start-up time substantially.
80
+ #
81
+ # @return [Boolean] whether Rails has already been loaded.
82
+ def env_already_loaded?
83
+ defined? Rails
84
+ end
85
+
86
+
87
+ # Returns the path of the Rails +config/environment.rb+ file - by
88
+ # default, +#{Dir.pwd}/config/environment.rb+.
89
+ #
90
+ # @return [String] the absolute path of the config/environment.rb file.
91
+ #
92
+ def rails_environment_file
93
+ @rails_environment_file ||= File.expand_path(
94
+ File.join(Dir.pwd, 'config', 'environment.rb')
95
+ )
96
+ end
97
+
98
+
99
+ #
100
+ # Allows a custom location to be set for the environment file if - for
101
+ # example - the command were to be called from a cron task that cannot
102
+ # change directory.
103
+ #
104
+ attr_writer :rails_environment_file
105
+
106
+
107
+ #
108
+ # Returns the name of the base presenter class.
109
+ #
110
+ # Because the initializer that contains configuration data is unlikely
111
+ # to have been loaded, this may also return a Proc, which will be called
112
+ # after the environment is loaded.
113
+ #
114
+ # @return [String,Proc] the base presenter class or a proc that returns
115
+ # the same
116
+ #
117
+ def base_presenter_class
118
+ proc_or_string = (@base_presenter_class ||= "::Brainstem::Presenter")
119
+ proc_or_string.respond_to?(:call) ? proc_or_string.call : proc_or_string
120
+ end
121
+
122
+
123
+ #
124
+ # Allows for the specification for an alternate base presenter class
125
+ # if - for example - only documentation of children of +MyBasePresenter+
126
+ # is desired.
127
+ #
128
+ # This argument accepts a string because most classes will not be
129
+ # defined at the time of passing, and will only be defined after
130
+ # environment load.
131
+ #
132
+ # Because the initializer that contains configuration data is unlikely
133
+ # to have been loaded, this may also return a Proc, which will be called
134
+ # after the environment is loaded.
135
+ #
136
+ # @param [String,Proc] base_presenter_class the class name to use as the
137
+ # base presenter, or a proc which returns the same.
138
+ #
139
+ attr_writer :base_presenter_class
140
+
141
+
142
+ #
143
+ # Returns the name of the base controller class.
144
+ #
145
+ # Because the initializer that contains configuration data is unlikely
146
+ # to have been loaded, this may also return a Proc, which will be called
147
+ # after the environment is loaded.
148
+ #
149
+ # @return [String,Proc] the base controller class or a proc that returns
150
+ # the same
151
+ #
152
+ def base_controller_class
153
+ proc_or_string = (@base_controller_class ||= "::ApplicationController")
154
+ proc_or_string.respond_to?(:call) ? proc_or_string.call : proc_or_string
155
+ end
156
+
157
+
158
+ #
159
+ # Allows for the specification for an alternate base controller class
160
+ # if - for example - only documentation of children of ApiController
161
+ # is desired. Best used through passing an argument to
162
+ # +with_loaded_environment+.
163
+ #
164
+ # This argument accepts a string because most classes will not be
165
+ # defined at the time of passing, and will only be defined after
166
+ # environment load.
167
+ #
168
+ # Because the initializer that contains configuration data is unlikely
169
+ # to have been loaded, this may also return a Proc, which will be called
170
+ # after the environment is loaded.
171
+ #
172
+ # @param [String,Proc] klass the class to use as the base controller, or
173
+ # a a method which returns the same.
174
+ #
175
+ attr_writer :base_controller_class
176
+
177
+
178
+ #
179
+ # Returns the proc that is called to format and retrieve routes.
180
+ # The proc's return must be an array of hashes that contains the
181
+ # following keys:
182
+ #
183
+ # +:path+ - the relative path
184
+ # +:controller+ - the managing controller as a constant
185
+ # +:controller_name+ - the internal underscored name of the controller
186
+ # +:action+ - the managing action
187
+ # +:http_method+ - an array of the HTTP methods this route is available on.
188
+ #
189
+ def routes_method
190
+ @routes_method ||= Proc.new do
191
+ Rails.application.routes.routes.map do |route|
192
+ next unless route.defaults.has_key?(:controller) &&
193
+ controller_const = "#{route.defaults[:controller]}_controller"
194
+ .classify
195
+ .constantize rescue nil
196
+
197
+ {
198
+ alias: route.name,
199
+ path: route.path.spec.to_s,
200
+ controller_name: route.defaults[:controller],
201
+ controller: controller_const,
202
+ action: route.defaults[:action],
203
+ http_methods: route.constraints
204
+ .fetch(:request_method, nil)
205
+ .inspect
206
+ .gsub(/[\/\$\^]/, '')
207
+ .split("|")
208
+ }
209
+ end.compact
210
+ end
211
+ end
212
+
213
+
214
+ #
215
+ # Allows setting the routes method used to retrieve the routes if - for
216
+ # example - your application needs to retrieve additional data or if it
217
+ # uses an explicit routing table to define documentable endpoints.
218
+ #
219
+ attr_writer :routes_method
220
+
221
+
222
+ #
223
+ # Throws an error if the introspector did not produce valid results.
224
+ #
225
+ def validate!
226
+ raise InvalidIntrospectorError, "Introspector is not valid." \
227
+ unless valid?
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,225 @@
1
+ require 'brainstem/api_docs'
2
+ require 'brainstem/concerns/optional'
3
+ require 'brainstem/concerns/formattable'
4
+ require 'forwardable'
5
+ require 'active_support/inflector'
6
+
7
+ #
8
+ # Wrapper for common presenter information lookups.
9
+ #
10
+ module Brainstem
11
+ module ApiDocs
12
+ class Presenter
13
+ extend Forwardable
14
+ include Concerns::Optional
15
+ include Concerns::Formattable
16
+
17
+
18
+ def valid_options
19
+ super | [
20
+ :const,
21
+ :target_class,
22
+ :filename_pattern,
23
+ :filename_link_pattern,
24
+ :document_empty_associations,
25
+ :document_empty_filters
26
+ ]
27
+ end
28
+
29
+ attr_accessor :const,
30
+ :target_class,
31
+ :document_empty_associations,
32
+ :document_empty_filters
33
+
34
+ attr_writer :filename_pattern,
35
+ :filename_link_pattern
36
+
37
+ alias_method :document_empty_associations?, :document_empty_associations
38
+ alias_method :document_empty_filters?, :document_empty_filters
39
+
40
+
41
+ def initialize(atlas, options = {})
42
+ self.atlas = atlas
43
+ self.document_empty_associations = Brainstem::ApiDocs.document_empty_presenter_associations
44
+ self.document_empty_filters = Brainstem::ApiDocs.document_empty_presenter_filters
45
+
46
+ super options
47
+ yield self if block_given?
48
+ end
49
+
50
+
51
+ def suggested_filename(format)
52
+ filename_pattern
53
+ .gsub('{{name}}', target_class.to_s.underscore)
54
+ .gsub('{{extension}}', extension)
55
+ end
56
+
57
+
58
+ def suggested_filename_link(format)
59
+ filename_link_pattern
60
+ .gsub('{{name}}', target_class.to_s.underscore)
61
+ .gsub('{{extension}}', extension)
62
+ end
63
+
64
+
65
+ attr_accessor :atlas
66
+
67
+
68
+ def extension
69
+ @extension ||= Brainstem::ApiDocs.output_extension
70
+ end
71
+
72
+
73
+ def filename_pattern
74
+ @filename_pattern ||= Brainstem::ApiDocs.presenter_filename_pattern
75
+ end
76
+
77
+
78
+ def filename_link_pattern
79
+ @filename_link_pattern ||= Brainstem::ApiDocs.presenter_filename_link_pattern
80
+ end
81
+
82
+
83
+ delegate :configuration => :const
84
+ delegate :find_by_class => :atlas
85
+
86
+
87
+ def nodoc?
88
+ configuration[:nodoc]
89
+ end
90
+
91
+
92
+ def title
93
+ contextual_documentation(:title) || const.to_s.demodulize
94
+ end
95
+
96
+
97
+ def brainstem_keys
98
+ const.possible_brainstem_keys.to_a.sort
99
+ end
100
+
101
+
102
+ def description
103
+ contextual_documentation(:description) || ""
104
+ end
105
+
106
+
107
+ def valid_fields(fields = configuration[:fields])
108
+ fields.to_h.reject do |k, v|
109
+ if nested_field?(v)
110
+ valid_fields_in(v).none?
111
+ else
112
+ invalid_field?(v)
113
+ end
114
+ end
115
+ end
116
+ alias_method :valid_fields_in, :valid_fields
117
+
118
+
119
+ def invalid_field?(field)
120
+ field.options[:nodoc]
121
+ end
122
+
123
+
124
+ def nested_field?(field)
125
+ !field.respond_to?(:options)
126
+ end
127
+
128
+
129
+ def valid_filters
130
+ configuration[:filters]
131
+ .to_h
132
+ .keep_if(&method(:documentable_filter?))
133
+ end
134
+
135
+
136
+ def documentable_filter?(_, filter)
137
+ !filter[:nodoc] &&
138
+ (
139
+ document_empty_filters? || # document empty filters or
140
+ !(filter[:info] || "").empty? # has info string
141
+ )
142
+ end
143
+
144
+
145
+ def valid_sort_orders
146
+ configuration[:sort_orders].to_h.reject {|k, v| v[:nodoc] }
147
+ end
148
+
149
+
150
+ def valid_associations
151
+ configuration[:associations]
152
+ .to_h
153
+ .keep_if(&method(:documentable_association?))
154
+ end
155
+
156
+
157
+
158
+ def link_for_association(association)
159
+ if (associated_presenter = find_by_class(association.target_class)) &&
160
+ !associated_presenter.nodoc?
161
+ relative_path_to_presenter(associated_presenter, :markdown)
162
+ else
163
+ nil
164
+ end
165
+ end
166
+
167
+
168
+ #
169
+ # Returns whether this association should be documented based on nodoc
170
+ # and empty description.
171
+ #
172
+ # @return [Bool] document this association?
173
+ #
174
+ def documentable_association?(_, association)
175
+ !association.options[:nodoc] && # not marked nodoc and
176
+ (
177
+ document_empty_associations? || # document empty associations or
178
+ !(association.description.nil? || association.description.empty?) # has description
179
+ )
180
+ end
181
+
182
+
183
+ def conditionals
184
+ configuration[:conditionals]
185
+ end
186
+
187
+
188
+ def default_sort_order
189
+ configuration[:default_sort_order] || ""
190
+ end
191
+
192
+
193
+ def default_sort_field
194
+ @default_sort_field ||= (default_sort_order.split(":")[0] || nil)
195
+ end
196
+
197
+
198
+ def default_sort_direction
199
+ @default_sort_direction ||= (default_sort_order.split(":")[1] || nil)
200
+ end
201
+
202
+
203
+ #
204
+ # Returns a key if it exists and is documentable.
205
+ #
206
+ def contextual_documentation(key)
207
+ configuration.has_key?(key) &&
208
+ !configuration[key][:nodoc] &&
209
+ configuration[key][:info]
210
+ end
211
+
212
+
213
+ #
214
+ # Returns the relative path between this presenter and another given
215
+ # presenter.
216
+ #
217
+ def relative_path_to_presenter(presenter, format)
218
+ my_path = Pathname.new(File.dirname(suggested_filename_link(format)))
219
+ presenter_path = Pathname.new(presenter.suggested_filename_link(format))
220
+
221
+ presenter_path.relative_path_from(my_path).to_s
222
+ end
223
+ end
224
+ end
225
+ end