useless-doc 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/.gitignore +18 -0
  2. data/.rbenv-version +1 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +3 -0
  6. data/Rakefile +1 -0
  7. data/config.ru +4 -0
  8. data/lib/useless/doc/action.rb +50 -0
  9. data/lib/useless/doc/body.rb +58 -0
  10. data/lib/useless/doc/dsl.rb +208 -0
  11. data/lib/useless/doc/header.rb +24 -0
  12. data/lib/useless/doc/rack/application.rb +31 -0
  13. data/lib/useless/doc/rack/proxy.rb +58 -0
  14. data/lib/useless/doc/rack/stylesheet.rb +24 -0
  15. data/lib/useless/doc/rack/transform.rb +35 -0
  16. data/lib/useless/doc/rack/ui.rb +35 -0
  17. data/lib/useless/doc/request/parameter.rb +47 -0
  18. data/lib/useless/doc/request.rb +29 -0
  19. data/lib/useless/doc/resource.rb +29 -0
  20. data/lib/useless/doc/response/status.rb +27 -0
  21. data/lib/useless/doc/response.rb +29 -0
  22. data/lib/useless/doc/serialization/dump.rb +122 -0
  23. data/lib/useless/doc/serialization/load.rb +171 -0
  24. data/lib/useless/doc/sinatra.rb +48 -0
  25. data/lib/useless/doc/ui/godel/stylesheet.css +1 -0
  26. data/lib/useless/doc/ui/godel/template.mustache +199 -0
  27. data/lib/useless/doc/ui/godel.rb +92 -0
  28. data/lib/useless/doc/ui.rb +37 -0
  29. data/lib/useless/doc/version.rb +5 -0
  30. data/lib/useless/doc.rb +4 -0
  31. data/spec/documents/twonk.json +106 -0
  32. data/spec/spec_helper.rb +10 -0
  33. data/spec/useless/doc/dsl_spec.rb +71 -0
  34. data/spec/useless/doc/proxy_spec.rb +48 -0
  35. data/spec/useless/doc/serialization/dump_spec.rb +116 -0
  36. data/spec/useless/doc/serialization/load_spec.rb +99 -0
  37. data/spec/useless/doc/sinatra_spec.rb +64 -0
  38. data/useless-doc.gemspec +26 -0
  39. metadata +217 -0
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ /doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ log
data/.rbenv-version ADDED
@@ -0,0 +1 @@
1
+ 1.9.3-p327
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in useless-doc.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Kevin Hyland
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # Useless::Doc
2
+
3
+ For parsing and serving Useless documentation.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/config.ru ADDED
@@ -0,0 +1,4 @@
1
+ $:.push File.dirname(__FILE__) + '/lib'
2
+ require 'useless/doc/rack/application'
3
+
4
+ run Useless::Doc::Rack::Application
@@ -0,0 +1,50 @@
1
+ module Useless
2
+ module Doc
3
+
4
+ # Documentation for an action on a API resource.
5
+ #
6
+ # @!attribute [r] description
7
+ # @return [String] a description of the action.
8
+ #
9
+ # @!attribute [r] method
10
+ # @return [String] the action's HTTP method.
11
+ # @see Useless::Doc::Action::Method
12
+ #
13
+ # @!attribute [r] authentication_required
14
+ # @return [Boolean] whether or not the user needs to authenticate in
15
+ # order to perform this action.
16
+ #
17
+ # @!attribute [r] request
18
+ # @return [Request] the request documentation for the action.
19
+ #
20
+ # @!attribute [r] response
21
+ # @return [Response] the response documentation for the action.
22
+ #
23
+ class Action
24
+
25
+ module Method
26
+ GET = 'GET'
27
+ HEAD = 'HEAD'
28
+ POST = 'POST'
29
+ PUT = 'PUT'
30
+ PATCH = 'PATCH'
31
+ DELETE = 'DELETE'
32
+ TRACE = 'TRACE'
33
+ CONNECT = 'CONNECT'
34
+ end
35
+
36
+ attr_reader :description, :method, :authentication_required,
37
+ :request, :response
38
+
39
+ # @param [Hash] attrs corresponds to the class's instance attributes.
40
+ #
41
+ def initialize(attrs = {})
42
+ @description = attrs[:description]
43
+ @method = attrs[:method]
44
+ @authentication_required = attrs[:authentication_required]
45
+ @request = attrs[:request]
46
+ @response = attrs[:response]
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,58 @@
1
+ module Useless
2
+ module Doc
3
+
4
+ # Documentation for an HTTP body, belonging either to the request or the
5
+ # response.
6
+ #
7
+ # @!attribute [r] content_type
8
+ # @return [String] the MIME type of the body.
9
+ #
10
+ # @!attribute [r] attributes
11
+ # @return [Array<Body::Attribute>] documentation for each of the body
12
+ # attributes.
13
+ #
14
+ class Body
15
+
16
+ attr_reader :content_type, :attributes
17
+
18
+ # @param [Hash] attrs corresponds to the class's instance attributes.
19
+ #
20
+ def initialize(attrs)
21
+ @content_type = attrs[:content_type]
22
+ @attributes = attrs[:attributes]
23
+ end
24
+
25
+ # Documentation for an attribute on an HTTP body.
26
+ #
27
+ # @!attribute [r] key
28
+ # @return [String] the key of this attribute in the body.
29
+ #
30
+ # @!attribute [r] value_type
31
+ # @return [String] one of "string", "number", "object",
32
+ # "array", or "boolean". "string" is the default value.
33
+ #
34
+ # @!attribute [r] required
35
+ # @return [Boolean] whether or not the attribute is required. If it
36
+ # is required, and the attribute is omitted, the response should have
37
+ # a 4xx code. +true+ is the default value.
38
+ #
39
+ # @!attribute [r] description
40
+ # @return [String] a description of the attribute.
41
+ #
42
+ class Attribute
43
+
44
+ attr_reader :key, :type, :required, :default, :description
45
+
46
+ # @param [Hash] attrs corresponds to the class's instance attributes.
47
+ #
48
+ def initialize(attrs)
49
+ @key = attrs[:key]
50
+ @type = attrs[:type] || 'string'
51
+ @required = attrs.key?(:required) ? attrs[:required] : true
52
+ @default = attrs[:default]
53
+ @description = attrs[:description]
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,208 @@
1
+ require 'useless/doc/action'
2
+ require 'useless/doc/body'
3
+ require 'useless/doc/header'
4
+ require 'useless/doc/request'
5
+ require 'useless/doc/request/parameter'
6
+ require 'useless/doc/resource'
7
+ require 'useless/doc/response'
8
+ require 'useless/doc/response/status'
9
+
10
+ module Useless
11
+ module Doc
12
+
13
+ # A simple DSL for building resource documentation.
14
+ #
15
+ # @example
16
+ # Useless::Doc::DSL::Resource.build do
17
+ # path '/resources'
18
+ # description 'The entire collection of resources.'
19
+ #
20
+ # get 'Returns a full listing of the resources.' do
21
+ # authentication_required false
22
+ #
23
+ # request do
24
+ # parameter 'page', 'The page of resources to be returned.'
25
+ # header 'X-Twiddle', 'The twiddle threshold.'
26
+ # end
27
+ #
28
+ # response do
29
+ # status 200, 'The resources were returned successfully.'
30
+ #
31
+ # header 'X-Twonk', 'The twonk coefficient.'
32
+ #
33
+ # body do
34
+ # content_type 'application/json'
35
+ # attribute 'name', 'The name of the resource.', required: true
36
+ # end
37
+ # end
38
+ # end
39
+ # end
40
+ #
41
+ class DSL
42
+
43
+ module Member
44
+ module ClassMethods
45
+ def build(attributes = {}, &block)
46
+ resource = new(attributes)
47
+ resource.instance_eval(&block)
48
+ resource.generate
49
+ end
50
+ end
51
+
52
+ def self.included(base)
53
+ base.send(:extend, ClassMethods)
54
+ end
55
+
56
+ def initialize(attributes = {})
57
+ @attributes = default_attributes.merge(attributes)
58
+ end
59
+
60
+ def generate
61
+ name = self.class.name.split('::').last
62
+ klass = eval("Doc::#{name}")
63
+ klass.new(@attributes)
64
+ end
65
+
66
+ def default_attributes
67
+ {}
68
+ end
69
+ end
70
+
71
+ class Resource
72
+ include DSL::Member
73
+
74
+ def default_attributes
75
+ { actions: [] }
76
+ end
77
+
78
+ def path(path)
79
+ @attributes[:path] = path
80
+ end
81
+
82
+ def description(description)
83
+ @attributes[:description] = description
84
+ end
85
+
86
+ def get(description = nil, &block)
87
+ method(Doc::Action::Method::GET, description, &block)
88
+ end
89
+
90
+ def head(description = nil, &block)
91
+ method(Doc::Action::Method::HEAD, description, &block)
92
+ end
93
+
94
+ def post(description = nil, &block)
95
+ method(Doc::Action::Method::POST, description, &block)
96
+ end
97
+
98
+ def put(description = nil, &block)
99
+ method(Doc::Action::Method::PUT, description, &block)
100
+ end
101
+
102
+ def patch(description = nil, &block)
103
+ method(Doc::Action::Method::PATCH, description, &block)
104
+ end
105
+
106
+ def delete(description = nil, &block)
107
+ method(Doc::Action::Method::DELETE, description, &block)
108
+ end
109
+
110
+ def trace(description = nil, &block)
111
+ method(Doc::Action::Method::TRACE, description, &block)
112
+ end
113
+
114
+ def connect(description = nil, &block)
115
+ method(Doc::Action::Method::CONNECT, description, &block)
116
+ end
117
+
118
+ private
119
+
120
+ def method(type, description, &block)
121
+ attributes = { method: type, description: description }
122
+ @attributes[:actions] << Action.build(attributes, &block)
123
+ end
124
+ end
125
+
126
+ class Action
127
+ include DSL::Member
128
+
129
+ def description(description)
130
+ @attributes[:description] = description
131
+ end
132
+
133
+ def authentication_required(value = nil)
134
+ @attributes[:authentication_required] = value.nil? ? true : value
135
+ end
136
+
137
+ def request(&block)
138
+ @attributes[:request] = Request.build({}, &block)
139
+ end
140
+
141
+ def response(&block)
142
+ @attributes[:response] = Response.build({}, &block)
143
+ end
144
+ end
145
+
146
+ class Request
147
+ include DSL::Member
148
+
149
+ def default_attributes
150
+ { parameters: [], headers: [] }
151
+ end
152
+
153
+ def parameter(key, description, attributes = {})
154
+ parameter = Doc::Request::Parameter.new attributes.merge(key: key, description: description)
155
+ @attributes[:parameters] << parameter
156
+ end
157
+
158
+ def header(key, description)
159
+ header = Doc::Header.new key: key, description: description
160
+ @attributes[:headers] << header
161
+ end
162
+
163
+ def body(&block)
164
+ @attributes[:body] = Body.build({}, &block)
165
+ end
166
+ end
167
+
168
+ class Response
169
+ include DSL::Member
170
+
171
+ def default_attributes
172
+ { statuses: [], headers: [] }
173
+ end
174
+
175
+ def status(code, description)
176
+ status = Doc::Response::Status.new code: code, description: description
177
+ @attributes[:statuses] << status
178
+ end
179
+
180
+ def header(key, description)
181
+ header = Doc::Header.new key: key, description: description
182
+ @attributes[:headers] << header
183
+ end
184
+
185
+ def body(&block)
186
+ @attributes[:body] = Body.build({}, &block)
187
+ end
188
+ end
189
+
190
+ class Body
191
+ include DSL::Member
192
+
193
+ def default_attributes
194
+ { attributes: [] }
195
+ end
196
+
197
+ def content_type(value)
198
+ @attributes[:content_type] = value
199
+ end
200
+
201
+ def attribute(key, description, attributes = {})
202
+ attribute = Doc::Body::Attribute.new attributes.merge(key: key, description: description)
203
+ @attributes[:attributes] << attribute
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,24 @@
1
+ module Useless
2
+ module Doc
3
+
4
+ # Documentation for an HTTP header, belonging either to the request or the
5
+ # response.
6
+ #
7
+ # @!attribute [r] key
8
+ # @return [String] the key of the header.
9
+ #
10
+ # @!attribute [r] description
11
+ # @return [String] a description of the header.
12
+ #
13
+ class Header
14
+ attr_accessor :key, :description
15
+
16
+ # @param [Hash] attrs corresponds to the class's instance attributes.
17
+ #
18
+ def initialize(attrs = {})
19
+ @key = attrs[:key]
20
+ @description = attrs[:description]
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,31 @@
1
+ require 'rack/builder'
2
+ require 'rack/commonlogger'
3
+ require 'rack/reloader'
4
+ require 'low/rack/rack_errors'
5
+ require 'low/rack/request_logger'
6
+
7
+ require 'useless/doc/rack/proxy'
8
+ require 'useless/doc/rack/stylesheet'
9
+ require 'useless/doc/rack/transform'
10
+ require 'useless/doc/rack/ui'
11
+
12
+ module Useless
13
+ module Doc
14
+ module Rack
15
+ module Application
16
+ def self.call(env)
17
+ ::Rack::Builder.app do
18
+ use Low::Rack::RackErrors
19
+ use Low::Rack::RequestLogger
20
+ use ::Rack::CommonLogger
21
+ use Useless::Doc::Rack::UI
22
+ use Useless::Doc::Rack::Stylesheet
23
+ use Useless::Doc::Rack::Transform
24
+
25
+ run Useless::Doc::Rack::Proxy.new
26
+ end.call(env)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,58 @@
1
+ require 'uri'
2
+ require 'typhoeus'
3
+ require 'rack/request'
4
+
5
+ module Useless
6
+ module Doc
7
+ module Rack
8
+
9
+ # +Doc::Proxy+ is a Rack app that provides an HTML interface to Useless
10
+ # API documentation. It assumes that each API resource responds to INFO
11
+ # requests with documentation JSON that corresponds to the format
12
+ # specified by +Doc::Serialization::Load+.
13
+ #
14
+ # It proxies requests according to a simple convention. For example, a
15
+ # GET request to some-api.doc.useless.io/some/resource will result in an
16
+ # OPTIONS request to some-api.useless.io/some/resource.
17
+ #
18
+ # If there is no corresponding endpoint, the proxy will respond with a
19
+ # 404.
20
+ #
21
+ class Proxy
22
+
23
+ def self.transform_url(url)
24
+ uri = URI(url)
25
+ new_host = uri.host.gsub(/\.doc\./, '.')
26
+ "#{uri.scheme}://#{new_host}#{uri.path}"
27
+ end
28
+
29
+ def call(env)
30
+ request = ::Rack::Request.new(env)
31
+ url = Proxy.transform_url(request.url)
32
+
33
+ if json = retrieve_resource(url)
34
+ [200, {'Content-Type' => 'application/json'}, [json]]
35
+ else
36
+ [404, {'Content-Type' => 'text/plain'}, ['Documentation JSON is missing.']]
37
+ end
38
+ end
39
+
40
+ def retrieve_resource(url)
41
+ response = Typhoeus.options url, headers: { 'Accept' => 'application/json' }
42
+ response.response_body
43
+ end
44
+
45
+ # +Proxy::Stub+ retrieves JSON from the spec/documents directory for
46
+ # easy UI testing.
47
+ #
48
+ class Stub < Proxy
49
+ def retrieve_resource(url)
50
+ uri = URI(url)
51
+ path = File.dirname(__FILE__) + "/../../../../spec/documents#{uri.path}.json"
52
+ File.read(path)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,24 @@
1
+ module Useless
2
+ module Doc
3
+ module Rack
4
+
5
+ # +Doc::Rack::Stylesheet+ serves the stylesheet for the current +Doc::UI+
6
+ # iff the request path is '/doc.css'. Otherwise, it passes the request
7
+ # through.
8
+ #
9
+ class Stylesheet
10
+ def initialize(app)
11
+ @app = app
12
+ end
13
+
14
+ def call(env)
15
+ if env["PATH_INFO"].to_s == '/doc.css'
16
+ [200, {'Content-Type' => 'text/css'}, [env['useless.doc.ui'].css]]
17
+ else
18
+ @app.call(env)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,35 @@
1
+ require 'useless/doc/serialization/load'
2
+
3
+ module Useless
4
+ module Doc
5
+ module Rack
6
+
7
+ # +Doc::Rack::Transform+ takes the a JSON response and attempts to parse
8
+ # it via +Doc::Serialization::Load+ and render it as HTML via the UI
9
+ # specified by env['useless.doc.ui'].
10
+ #
11
+ # If the JSON cannot be parsed, the response will be a 502.
12
+ #
13
+ class Transform
14
+ def initialize(app)
15
+ @app = app
16
+ end
17
+
18
+ def call(env)
19
+ response = @app.call(env)
20
+
21
+ if response[0] == 200 and response[1]['Content-Type'] == 'application/json'
22
+ begin
23
+ resource = Serialization::Load.resource(response[2].first)
24
+ [200, {'Content-Type' => 'text/html'}, [env['useless.doc.ui'].html(resource)]]
25
+ rescue Oj::ParseError
26
+ [502, {'Content-Type' => 'text/plain'}, ['Documentation JSON is malformed.']]
27
+ end
28
+ else
29
+ response
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,35 @@
1
+ require 'rack/request'
2
+ require 'useless/doc/ui/godel'
3
+
4
+ module Useless
5
+ module Doc
6
+ module Rack
7
+
8
+ # +Doc::Rack::UI+ chooses which UI should be used to render the
9
+ # documentation. It can theoretically be chosen via the 'ui' parameter,
10
+ # but for now it will alway choose +Godel+
11
+ #
12
+ class UI
13
+ def self.default
14
+ Useless::Doc::UI::Godel
15
+ end
16
+
17
+ def initialize(app)
18
+ @app = app
19
+ end
20
+
21
+ def call(env)
22
+ request = ::Rack::Request.new(env)
23
+
24
+ ui = case request.params['ui']
25
+ when 'godel' then Useless::Doc::UI::Godel
26
+ else Rack::UI.default
27
+ end
28
+
29
+ env['useless.doc.ui'] = ui
30
+ @app.call(env)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,47 @@
1
+ module Useless
2
+ module Doc
3
+ class Request
4
+
5
+ # Documentation for a request parameter for an API action.
6
+ #
7
+ # @!attribute [r] type
8
+ # @return [String] either "path" if it's part of the URL path, or
9
+ # "query" if it's part of the query string.
10
+ #
11
+ # @!attribute [r] key
12
+ # @return [String] the key of the parameter.
13
+ #
14
+ # @!attribute [r] required
15
+ # @return [Boolean] whether or not the parameter is required. If it is
16
+ # required, and the attribute is omitted, the response should have a
17
+ # 4xx code.
18
+ #
19
+ # @!attribute [r] default
20
+ # @return [String, Numeric] the value used if the parameter is omitted
21
+ # and is not required.
22
+ #
23
+ # @!attribute [r] description
24
+ # @return [String] a description of the parameter.
25
+ #
26
+ class Parameter
27
+
28
+ module Type
29
+ PATH = 'path'
30
+ QUERY = 'query'
31
+ end
32
+
33
+ attr_reader :type, :key, :required, :default, :description
34
+
35
+ # @param [Hash] attrs corresponds to the class's instance attributes.
36
+ #
37
+ def initialize(attrs = {})
38
+ @type = attrs[:type]
39
+ @key = attrs[:key]
40
+ @required = attrs[:required]
41
+ @default = attrs[:default]
42
+ @description = attrs[:description]
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,29 @@
1
+ module Useless
2
+ module Doc
3
+
4
+ # Documentation for an HTTP request.
5
+ #
6
+ # @!attribute [r] parameters
7
+ # @return [Array<Request::Parameter] documentation for the parameters
8
+ # of the request.
9
+ #
10
+ # @!attribute [r] headers
11
+ # @return [Array<Header>] documentation for the headers of the
12
+ # request.
13
+ #
14
+ # @!attribute [r] body
15
+ # @return [Body] documentation for the body of the request.
16
+ #
17
+ class Request
18
+ attr_accessor :parameters, :headers, :body
19
+
20
+ # @param [Hash] attrs corresponds to the class's instance attributes.
21
+ #
22
+ def initialize(attrs = {})
23
+ @parameters = attrs[:parameters]
24
+ @headers = attrs[:headers]
25
+ @body = attrs[:body]
26
+ end
27
+ end
28
+ end
29
+ end