brainstem 1.0.0.pre.1 → 1.0.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 (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,116 @@
1
+ require 'forwardable'
2
+ require 'brainstem/concerns/optional'
3
+ require 'brainstem/concerns/formattable'
4
+
5
+ module Brainstem
6
+ module ApiDocs
7
+ class AbstractCollection
8
+ include Enumerable
9
+ extend Forwardable
10
+ include Concerns::Optional
11
+ include Concerns::Formattable
12
+
13
+ #
14
+ # Creates a new collection with all passed members. Very handy for
15
+ # reduce operations which should return a subset of members but retain
16
+ # the same utility.
17
+ #
18
+ def self.with_members(atlas, *members)
19
+ new(atlas).tap {|n| members.flatten.each { |m| n << m } }
20
+ end
21
+
22
+
23
+ def initialize(atlas, options = {})
24
+ self.atlas = atlas
25
+ self.members = []
26
+ super options
27
+ end
28
+
29
+
30
+ attr_accessor :atlas
31
+
32
+ delegate :find_by_class => :atlas
33
+
34
+
35
+ #
36
+ # Handy accessor for extracting the last member of the collection.
37
+ #
38
+ def last
39
+ members[-1]
40
+ end
41
+
42
+
43
+ #
44
+ # Appends a pre-existing object to the collection.
45
+ #
46
+ def <<(*objects)
47
+ members.push(*objects.flatten)
48
+ end
49
+
50
+
51
+ #
52
+ # Iterates over each controller in the collection.
53
+ #
54
+ def each(&block)
55
+ members.each(&block)
56
+ end
57
+
58
+
59
+ #
60
+ # Returns a map of each member formatted as specified.
61
+ #
62
+ def formatted(format, options = {})
63
+ map { |member| member.formatted_as(format, options) }
64
+ .reject(&:empty?)
65
+ end
66
+
67
+
68
+ #
69
+ # Returns a list of each member's filename.
70
+ #
71
+ # We internally refer to `formatted_with_filename` here because we don't
72
+ # want to include any filenames of empty files (i.e. nodoc).
73
+ #
74
+ def filenames(format)
75
+ formatted_with_filename(format).map { |arr| arr[1] }
76
+ end
77
+
78
+
79
+ #
80
+ # Returns a map of each formatted member and its suggested filename.
81
+ #
82
+ def formatted_with_filename(format, options = {})
83
+ map { |member| [
84
+ member.formatted_as(format, options),
85
+ member.suggested_filename(format)
86
+ ] }
87
+ .reject { |(buffer, _)| buffer.empty? }
88
+ end
89
+
90
+
91
+ def each_formatted_with_filename(format, options = {}, &block)
92
+ formatted_with_filename(format, options)
93
+ .each { |args| block.call(*args) }
94
+ end
95
+
96
+
97
+ def each_formatted(format, options = {}, &block)
98
+ formatted(format, options)
99
+ .each { |args| block.call(*args) }
100
+ end
101
+
102
+
103
+ def each_filename(format, &block)
104
+ filenames(format).each { |args| block.call(*args) }
105
+ end
106
+
107
+
108
+
109
+ #########################################################################
110
+ protected
111
+ #########################################################################
112
+
113
+ attr_accessor :members
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,158 @@
1
+ require 'forwardable'
2
+ require 'brainstem/api_docs/resolver'
3
+ require 'brainstem/api_docs/exceptions'
4
+ require 'brainstem/api_docs/endpoint_collection'
5
+ require 'brainstem/api_docs/controller_collection'
6
+ require 'brainstem/api_docs/presenter_collection'
7
+ require 'brainstem/concerns/optional'
8
+
9
+ #
10
+ #
11
+ # The Atlas is an object that makes the information from an introspector
12
+ # available in formatted and raw format.
13
+ #
14
+ module Brainstem
15
+ module ApiDocs
16
+ class Atlas
17
+ extend Forwardable
18
+ include Concerns::Optional
19
+
20
+
21
+ def initialize(introspector, options = {})
22
+ self.endpoints = EndpointCollection.new(self)
23
+ self.controllers = ControllerCollection.new(self)
24
+ self.presenters = ::Brainstem::ApiDocs::PresenterCollection.new(self)
25
+ self.resolver = Resolver.new(self)
26
+
27
+ self.controller_matches = []
28
+ self.introspector = introspector
29
+
30
+ super options
31
+
32
+ parse_routes!
33
+ extract_presenters!
34
+ validate!
35
+ end
36
+
37
+
38
+ attr_accessor :endpoints,
39
+ :controllers,
40
+ :presenters,
41
+ :resolver
42
+
43
+
44
+ delegate :find_by_class => :resolver
45
+
46
+
47
+ #########################################################################
48
+ private
49
+ #########################################################################
50
+
51
+
52
+ #
53
+ # Lists valid options that may be passed on instantiation.
54
+ #
55
+ def valid_options
56
+ super | [ :controller_matches ]
57
+ end
58
+
59
+
60
+ #
61
+ # Ensures the atlas is valid before allowing consumers to make requests
62
+ # of it.
63
+ #
64
+ def validate!
65
+ raise InvalidAtlasError, "Atlas is not valid." unless valid?
66
+ end
67
+
68
+
69
+ #
70
+ # Set and read the introspector.
71
+ #
72
+ attr_accessor :introspector
73
+
74
+
75
+ #
76
+ # Holds +Regexp+s which each controller name must match in order to be
77
+ # included in the list of endpoints.
78
+ #
79
+ attr_accessor :controller_matches
80
+
81
+
82
+ #
83
+ # Returns a list of all routes that pass the user's filtering.
84
+ #
85
+ def allowed_routes
86
+ introspector.routes.keep_if(&method(:allow_route?))
87
+ end
88
+
89
+
90
+ #
91
+ # Constructs +Endpoint+ and +Controller wrappers per route.
92
+ #
93
+ def parse_routes!
94
+ allowed_routes.each do |route|
95
+ if (endpoint = endpoints.find_from_route(route))
96
+ endpoint.merge_http_methods!(route[:http_methods])
97
+ else
98
+ controller = controllers.find_or_create_from_route(route)
99
+ endpoint = endpoints.create_from_route(route, controller)
100
+
101
+ controller.add_endpoint(endpoint)
102
+ end
103
+ end
104
+ end
105
+
106
+
107
+ #
108
+ # Extracts declared presents for each endpoint and converts it into a
109
+ # Presenter wrapper object.
110
+ #
111
+ def extract_presenters!
112
+ valid_presenter_pairs.each do |target_class, const|
113
+ presenter = presenters.find_or_create_from_presenter_collection(target_class, const)
114
+
115
+ endpoints
116
+ .select do |ep|
117
+ declared_presented_class = ep.declared_presented_class
118
+ !declared_presented_class.nil? && declared_presented_class.to_s == target_class
119
+ end
120
+ .each {|ep| ep.presenter = presenter }
121
+ end
122
+ end
123
+
124
+
125
+ #
126
+ # Returns a list of valid +target_class_to_s => PresenterConst+ pairs,
127
+ # determining validity by whether they descend from the base presenter.
128
+ #
129
+ # @return [Hash{String => Class}] valid pairs
130
+ #
131
+ def valid_presenter_pairs
132
+ Brainstem.presenter_collection.presenters.select do |target_class, const|
133
+ introspector.presenters.include? const
134
+ end
135
+ end
136
+
137
+
138
+ #
139
+ # Whether this Atlas is valid (i.e. if it has at least one endpoint).
140
+ #
141
+ # @return [Boolean] if the atlas is valid
142
+ #
143
+ def valid?
144
+ endpoints.count > 0
145
+ end
146
+
147
+
148
+ #
149
+ # Returns whether a route's controller passes the limiting regexp passed to the
150
+ # generation command.
151
+ #
152
+ def allow_route?(route)
153
+ introspector.controllers.include?(route[:controller]) &&
154
+ controller_matches.all? { |regexp| route[:controller].to_s =~ regexp }
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,167 @@
1
+ require 'brainstem/api_docs/introspectors/rails_introspector'
2
+ require 'brainstem/concerns/optional'
3
+ require 'brainstem/api_docs/atlas'
4
+ require 'active_support/core_ext/hash/slice'
5
+
6
+ #
7
+ # This class describes the main API surface for generating API documentation.
8
+ # The command-line utility provided with Brainstem is basically just a thin
9
+ # veneer over top of this class.
10
+ #
11
+ # You can use this to programmatically generate the API docs, or to browse them
12
+ # while inside a REPL.
13
+ #
14
+ module Brainstem
15
+ module ApiDocs
16
+ class Builder
17
+ include Brainstem::Concerns::Optional
18
+
19
+
20
+ def valid_options
21
+ [
22
+ :introspector_method,
23
+ :atlas_method,
24
+
25
+ :args_for_introspector,
26
+ :args_for_atlas
27
+ ]
28
+ end
29
+
30
+
31
+ #
32
+ # @param [Hash] options
33
+ # @option options [Proc] :introspector_method Proc of arity one that
34
+ # returns an Introspector (an object that introspects into
35
+ # the host application, seeking its routes, controllers, and
36
+ # presenters).
37
+ # @option options [Hash] :args_for_introspector Additional arguments to
38
+ # be passed to the introspector on creation.
39
+ # @option options [Hash] :args_for_atlas Additional arguments to be passed
40
+ # to the atlas on creation.
41
+ # @option options [Proc,Object] :introspector_method A method that
42
+ # returns an introspector when called.
43
+ # @option options [Proc,Object] :atlas_method A method that returns an Atlas-like
44
+ # object when called.
45
+ #
46
+ # @see Brainstem::ApiDocs::Introspectors::AbstractIntrospector
47
+ # @see Brainstem::ApiDocs::Introspectors::RailsIntrospector
48
+ #
49
+ def initialize(options = {})
50
+ super
51
+
52
+ build_introspector!
53
+ build_atlas!
54
+ end
55
+
56
+
57
+ #
58
+ # Builds an introspector.
59
+ #
60
+ def build_introspector!
61
+ self.introspector = introspector_method.call(args_for_introspector)
62
+ end
63
+
64
+
65
+ #
66
+ # Builds an atlas.
67
+ #
68
+ def build_atlas!
69
+ self.atlas = atlas_method.call(introspector, args_for_atlas)
70
+ end
71
+
72
+
73
+ #
74
+ # Arguments to be passed to the introspector on creation.
75
+ #
76
+ # @see Brainstem::ApiDocs::Introspectors::AbstractIntrospector
77
+ # @see Brainstem::ApiDocs::Introspectors::RailsIntrospector
78
+ #
79
+ def args_for_introspector
80
+ @args_for_introspector ||= {}
81
+ end
82
+
83
+
84
+ #
85
+ # Allows passing args to the introspector if - for example - you are
86
+ # using a custom base controller class.
87
+ #
88
+ attr_writer :args_for_introspector
89
+
90
+
91
+ #
92
+ # Arguments to be passed to the atlas on creation.
93
+ #
94
+ # @see Brainstem::ApiDocs::Atlas
95
+ #
96
+ def args_for_atlas
97
+ @args_for_atlas ||= {}
98
+ end
99
+
100
+
101
+ #
102
+ # Allows passing args to the atlas if - for example - you are
103
+ # specifying match terms for the allowable controller set.
104
+ #
105
+ attr_writer :args_for_atlas
106
+
107
+
108
+ #
109
+ # A method which returns the introspector which extracts information
110
+ # about the Brainstem-powered API from the host application.
111
+ #
112
+ # Stored as a proc because it's impossible to inject an instantiated
113
+ # object and have it receive args from this class. This is less important
114
+ # in this specific circumstance but is kept for uniformity with
115
+ # +atlas_method+.
116
+ #
117
+ # @return [Proc] a proc of arity 1 which takes an options hash and
118
+ # returns an introspector
119
+ #
120
+ def introspector_method
121
+ @introspector_method ||=
122
+ Introspectors::RailsIntrospector.method(:with_loaded_environment)
123
+ end
124
+
125
+
126
+ #
127
+ # Allows setting the introspector_method if - for example - you are using
128
+ # Brainstem on a Sinatra app and you need to customize how lookups for
129
+ # presenters, controllers, and routes are performed.
130
+ #
131
+ attr_writer :introspector_method
132
+
133
+
134
+ #
135
+ # Holds a reference to the constructed introspector.
136
+ #
137
+ attr_accessor :introspector
138
+
139
+
140
+ #
141
+ # A proc of arity 1..2 which takes an introspector and optional options,
142
+ # and which returns a new Atlas.
143
+ #
144
+ # Passed an introspector.
145
+ #
146
+ # @return [Proc] a method to return an atlas
147
+ #
148
+ def atlas_method
149
+ @atlas_method ||= Atlas.method(:new)
150
+ end
151
+
152
+
153
+ #
154
+ # Allows setting the introspector_method if - for example - you are using
155
+ # an alternative formatter and the requisite information is not present
156
+ # in the +Endpoint+ objects.
157
+ #
158
+ attr_writer :atlas_method
159
+
160
+
161
+ #
162
+ # Holds a reference to the constructed atlas.
163
+ #
164
+ attr_accessor :atlas
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,122 @@
1
+ require 'brainstem/concerns/optional'
2
+ require 'brainstem/concerns/formattable'
3
+ require 'active_support/inflector'
4
+ require 'brainstem/api_docs/endpoint_collection'
5
+ require 'forwardable'
6
+
7
+ module Brainstem
8
+ module ApiDocs
9
+ class Controller
10
+ extend Forwardable
11
+ include Concerns::Optional
12
+ include Concerns::Formattable
13
+
14
+
15
+ def initialize(atlas, options = {})
16
+ self.atlas = atlas
17
+ self.endpoints = EndpointCollection.new(atlas)
18
+ super options
19
+ yield self if block_given?
20
+ end
21
+
22
+
23
+ attr_accessor :const,
24
+ :name,
25
+ :endpoints,
26
+ :filename_pattern,
27
+ :atlas
28
+
29
+
30
+ attr_writer :filename_pattern,
31
+ :filename_link_pattern
32
+
33
+
34
+ def valid_options
35
+ super | [
36
+ :const,
37
+ :name,
38
+ :formatters,
39
+ :filename_pattern,
40
+ :filename_link_pattern
41
+ ]
42
+ end
43
+
44
+
45
+ #
46
+ # Adds an existing endpoint to its endpoint collection.
47
+ #
48
+ def add_endpoint(endpoint)
49
+ self.endpoints << endpoint
50
+ end
51
+
52
+
53
+ def suggested_filename(format)
54
+ filename_pattern
55
+ .gsub('{{namespace}}', const.to_s.deconstantize.underscore)
56
+ .gsub('{{name}}', name.to_s.split("/").last)
57
+ .gsub('{{extension}}', extension)
58
+ end
59
+
60
+
61
+ def suggested_filename_link(format)
62
+ filename_link_pattern
63
+ .gsub('{{name}}', name.to_s)
64
+ .gsub('{{extension}}', extension)
65
+ end
66
+
67
+
68
+ def extension
69
+ @extension ||= Brainstem::ApiDocs.output_extension
70
+ end
71
+
72
+
73
+ def filename_pattern
74
+ @filename_pattern ||= Brainstem::ApiDocs.controller_filename_pattern
75
+ end
76
+
77
+
78
+ def filename_link_pattern
79
+ @filename_link_pattern ||= Brainstem::ApiDocs.controller_filename_link_pattern
80
+ end
81
+
82
+
83
+ delegate :configuration => :const
84
+ delegate :find_by_class => :atlas
85
+
86
+
87
+ def default_configuration
88
+ configuration[:_default]
89
+ end
90
+
91
+
92
+ def nodoc?
93
+ default_configuration[:nodoc]
94
+ end
95
+
96
+
97
+ def title
98
+ contextual_documentation(:title) || const.to_s.demodulize
99
+ end
100
+
101
+
102
+ def description
103
+ contextual_documentation(:description) || ""
104
+ end
105
+
106
+
107
+ #
108
+ # Returns a key if it exists and is documentable.
109
+ #
110
+ def contextual_documentation(key)
111
+ default_configuration.has_key?(key) &&
112
+ !default_configuration[key][:nodoc] &&
113
+ default_configuration[key][:info]
114
+ end
115
+
116
+
117
+ def valid_sorted_endpoints
118
+ endpoints.sorted_with_actions_in_controller(const)
119
+ end
120
+ end
121
+ end
122
+ end