useless-doc 0.2.3 → 0.3.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 (47) hide show
  1. data/lib/useless/doc/client.rb +62 -0
  2. data/lib/useless/doc/core/api.rb +6 -2
  3. data/lib/useless/doc/core/domain.rb +34 -0
  4. data/lib/useless/doc/dsl.rb +30 -0
  5. data/lib/useless/doc/rack/{stylesheet.rb → css.rb} +8 -8
  6. data/lib/useless/doc/rack/html.rb +26 -0
  7. data/lib/useless/doc/rack/subject.rb +40 -0
  8. data/lib/useless/doc/rack/ui.rb +12 -11
  9. data/lib/useless/doc/rack/url.rb +33 -0
  10. data/lib/useless/doc/rack.rb +46 -0
  11. data/lib/useless/doc/router.rb +62 -0
  12. data/lib/useless/doc/serialization/dump.rb +17 -0
  13. data/lib/useless/doc/serialization/load.rb +35 -0
  14. data/lib/useless/doc/sinatra.rb +4 -3
  15. data/lib/useless/doc/ui/godel/api.mustache +24 -24
  16. data/lib/useless/doc/ui/godel/domain.mustache +26 -0
  17. data/lib/useless/doc/ui/godel/resource.mustache +151 -155
  18. data/lib/useless/doc/ui/godel/stylesheet.css +156 -1
  19. data/lib/useless/doc/ui/godel.rb +54 -12
  20. data/lib/useless/doc/ui.rb +24 -0
  21. data/lib/useless/doc/version.rb +1 -1
  22. data/lib/useless/doc.rb +20 -2
  23. data/spec/config.ru +19 -0
  24. data/spec/documents/api.json +1 -0
  25. data/spec/documents/domain.json +17 -0
  26. data/spec/useless/doc/client_spec.rb +59 -0
  27. data/spec/useless/doc/dsl_spec.rb +28 -2
  28. data/spec/useless/doc/rack/{stylesheet_spec.rb → css_spec.rb} +3 -9
  29. data/spec/useless/doc/rack/html_spec.rb +39 -0
  30. data/spec/useless/doc/rack/subject_spec.rb +44 -0
  31. data/spec/useless/doc/rack/ui_spec.rb +1 -1
  32. data/spec/useless/doc/rack/url_spec.rb +35 -0
  33. data/spec/useless/doc/{rack/application_spec.rb → rack_spec.rb} +22 -11
  34. data/spec/useless/doc/router_spec.rb +38 -0
  35. data/spec/useless/doc/serialization/dump_spec.rb +29 -0
  36. data/spec/useless/doc/serialization/load_spec.rb +37 -0
  37. data/spec/useless/doc/sinatra_spec.rb +3 -1
  38. data/spec/useless/doc/ui/godel_spec.rb +136 -96
  39. data/useless-doc.gemspec +1 -1
  40. metadata +34 -21
  41. data/lib/useless/doc/rack/application.rb +0 -47
  42. data/lib/useless/doc/rack/proxy.rb +0 -62
  43. data/lib/useless/doc/rack/retriever.rb +0 -68
  44. data/lib/useless/doc/rack/transform.rb +0 -46
  45. data/spec/useless/doc/rack/proxy_spec.rb +0 -56
  46. data/spec/useless/doc/rack/retriever_spec.rb +0 -82
  47. data/spec/useless/doc/rack/transform_spec.rb +0 -57
@@ -0,0 +1,62 @@
1
+ require 'time'
2
+ require 'typhoeus'
3
+ require 'uri'
4
+
5
+ require 'useless/doc/serialization/load'
6
+
7
+ module Useless
8
+ module Doc
9
+ module Client
10
+ def self.standard
11
+ @standard ||= Standard.new
12
+ end
13
+
14
+ def self.stub
15
+ @stub ||= Stub.new
16
+ end
17
+
18
+ def get(url)
19
+ nil
20
+ end
21
+
22
+ class Standard
23
+ include Client
24
+
25
+ NotModified = 304
26
+
27
+ def initialize
28
+ @cache = {}
29
+ end
30
+
31
+ def get(url)
32
+ headers = { 'Accept' => 'application/json' }
33
+
34
+ if @cache[url]
35
+ headers['If-Modified-Since'] = @cache[url][:timestamp].httpdate()
36
+ end
37
+
38
+ response = Typhoeus.options url, headers: headers
39
+
40
+ unless response.response_code == NotModified
41
+ @cache[url] = { response_body: response.response_body, timestamp: Time.now }
42
+ end
43
+
44
+ Useless::Doc::Serialization::Load.load(@cache[url][:response_body])
45
+ end
46
+ end
47
+
48
+ class Stub
49
+ include Client
50
+
51
+ def get(url)
52
+ uri = URI(url)
53
+ path = File.dirname(__FILE__) + "/../../../spec/documents#{uri.path}.json"
54
+
55
+ if File.exists?(path)
56
+ Useless::Doc::Serialization::Load.load(File.read(path))
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -4,6 +4,9 @@ module Useless
4
4
 
5
5
  # Documentation for an entire API.
6
6
  #
7
+ # @!attribute [r] name
8
+ # @return [String] nameof the API.
9
+ #
7
10
  # @!attribute [r] url
8
11
  # @return [String] a the URL of the API.
9
12
  #
@@ -18,15 +21,16 @@ module Useless
18
21
  #
19
22
  class API
20
23
 
21
- attr_accessor :url, :description, :timestamp, :resources
24
+ attr_accessor :name, :url, :description, :timestamp, :resources
22
25
 
23
26
  # @param [Hash] attrs corresponds to the class's instance attributes.
24
27
  #
25
28
  def initialize(attrs = {})
29
+ @name = attrs[:name]
26
30
  @url = attrs[:url]
27
31
  @description = attrs[:description]
28
32
  @timestamp = attrs[:timestamp]
29
- @resources = attrs[:resources]
33
+ @resources = attrs[:resources] || []
30
34
  end
31
35
  end
32
36
  end
@@ -0,0 +1,34 @@
1
+ module Useless
2
+ module Doc
3
+ module Core
4
+
5
+ # Documentation for a domain - a group of APIs.
6
+ #
7
+ # @!attribute [r] name
8
+ # @return [String] a name of the domain.
9
+ #
10
+ # @!attribute [r] url
11
+ # @return [String] a the URL of the domain.
12
+ #
13
+ # @!attribute [r] description
14
+ # @return [String] a description of the domain.
15
+ #
16
+ # @!attribute [r] apis
17
+ # @return [Array<API>] the APIs included in this domain.
18
+ #
19
+ class Domain
20
+
21
+ attr_accessor :name, :url, :description, :apis
22
+
23
+ # @param [Hash] attrs corresponds to the class's instance attributes.
24
+ #
25
+ def initialize(attrs = {})
26
+ @name = attrs[:name]
27
+ @url = attrs[:url]
28
+ @description = attrs[:description]
29
+ @apis = attrs[:apis]
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,5 +1,6 @@
1
1
  require 'useless/doc/core/api'
2
2
  require 'useless/doc/core/body'
3
+ require 'useless/doc/core/domain'
3
4
  require 'useless/doc/core/header'
4
5
  require 'useless/doc/core/request'
5
6
  require 'useless/doc/core/resource'
@@ -64,6 +65,31 @@ module Useless
64
65
  end
65
66
  end
66
67
 
68
+ class Domain
69
+ include DSL::Member
70
+
71
+ def default_attributes
72
+ { apis: [] }
73
+ end
74
+
75
+ def name
76
+ @attributes[:name] = name
77
+ end
78
+
79
+ def url(url)
80
+ @attributes[:url] = url
81
+ end
82
+
83
+ def description(description)
84
+ @attributes[:description] = description
85
+ end
86
+
87
+ def api(name, &block)
88
+ api = API.build name: name, &block
89
+ @attributes[:apis] << api
90
+ end
91
+ end
92
+
67
93
  class API
68
94
  include DSL::Member
69
95
 
@@ -84,6 +110,10 @@ module Useless
84
110
  super
85
111
  end
86
112
 
113
+ def name
114
+ @attributes[:name] = name
115
+ end
116
+
87
117
  def url(url)
88
118
  @attributes[:url] = url
89
119
  end
@@ -1,22 +1,22 @@
1
1
  module Useless
2
2
  module Doc
3
- module Rack
3
+ class Rack
4
4
 
5
- # +Doc::Rack::Stylesheet+ serves the stylesheet for the current +Doc::UI+
5
+ # +Doc::Rack::CSS+ serves the stylesheet for the current +Doc::UI+
6
6
  # iff the request path is '/doc.css'. Otherwise, it passes the request
7
- # through.
7
+ # down the stack.
8
8
  #
9
- class Stylesheet
9
+ class CSS
10
10
  def initialize(app)
11
11
  @app = app
12
12
  end
13
13
 
14
14
  def call(env)
15
- unless env['useless.doc.ui']
16
- raise 'No UI specified.'
17
- end
18
-
19
15
  if env["PATH_INFO"].to_s == '/doc.css'
16
+ if env['useless.doc.logger']
17
+ env['useless.doc.logger'].info "serving CSS for #{env['useless.doc.ui'].class.name}"
18
+ end
19
+
20
20
  [200, {'Content-Type' => 'text/css'}, [env['useless.doc.ui'].css]]
21
21
  else
22
22
  @app.call(env)
@@ -0,0 +1,26 @@
1
+ module Useless
2
+ module Doc
3
+ class Rack
4
+
5
+ # +Doc::Rack::HTML+ is the base application for +Useless::Doc::Rack+.
6
+ # It expects a +Doc::UI+ instance to be set as 'useless.doc.ui', and a
7
+ # +Doc::Core+ entity to be set as 'useless.doc.subject', and then simply
8
+ # passes the latter to the former's +#html+ method.
9
+ #
10
+ module HTML
11
+ def self.call(env)
12
+ if html = env['useless.doc.ui'].html(env['useless.doc.subject'])
13
+ if env['useless.doc.logger']
14
+ env['useless.doc.logger'].info "rendered subject HTML for #{env['useless.doc.url']}"
15
+ env['useless.doc.logger'].debug "rendered HTML: #{html}"
16
+ end
17
+
18
+ [200, {'Content-Type' => 'text/html'}, [html]]
19
+ else
20
+ [404, {'Content-Type' => 'text/plain'}, ['Could not render documentation.']]
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,40 @@
1
+ require 'useless/doc/client'
2
+
3
+ module Useless
4
+ module Doc
5
+ class Rack
6
+
7
+ # +Doc::Rack::Subject+ retrieves a +Doc::Core+ entity based upon
8
+ # 'useless.doc.url', from a environment-appropriate +Doc::Client+,
9
+ # and sets it to 'useless.doc.subject'.
10
+ #
11
+ class Subject
12
+ def initialize(app)
13
+ @app = app
14
+ end
15
+
16
+ def call(env)
17
+ client = case ENV['RACK_ENV']
18
+ when 'production'; Useless::Doc::Client.standard
19
+ else Useless::Doc::Client.stub
20
+ end
21
+
22
+ if env['useless.doc.logger']
23
+ env['useless.doc.logger'].debug "selected Client: #{client.inspect}"
24
+ end
25
+
26
+ if env['useless.doc.subject'] = client.get(env['useless.doc.url'])
27
+ if env['useless.doc.logger']
28
+ env['useless.doc.logger'].info "retrieved subject for #{env['useless.doc.url']}"
29
+ env['useless.doc.logger'].debug "retrieved subject: #{env['useless.doc.subject'].inspect}"
30
+ end
31
+
32
+ @app.call(env)
33
+ else
34
+ [404, {'Content-Type' => 'text/plain'}, ['Could not retrieve documentation.']]
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -3,17 +3,15 @@ require 'useless/doc/ui/godel'
3
3
 
4
4
  module Useless
5
5
  module Doc
6
- module Rack
6
+ class Rack
7
7
 
8
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+
9
+ # documentation and sets it to 'useless.doc.ui'.
10
+ #
11
+ # It could theoretically be chosen via the 'ui' parameter,
12
+ # but until there are other UIs, it will alway choose +UI::Godel+.
11
13
  #
12
14
  class UI
13
- def self.default
14
- Useless::Doc::UI::Godel
15
- end
16
-
17
15
  def initialize(app)
18
16
  @app = app
19
17
  end
@@ -21,12 +19,15 @@ module Useless
21
19
  def call(env)
22
20
  request = ::Rack::Request.new(env)
23
21
 
24
- ui = case request.params['ui']
25
- when 'godel' then Useless::Doc::UI::Godel
26
- else Rack::UI.default
22
+ env['useless.doc.ui'] = case request.params['ui']
23
+ when 'godel'; Useless::Doc::UI::Godel.new(env['useless.doc.router'])
24
+ else Useless::Doc::UI::Godel.new(env['useless.doc.router'])
25
+ end
26
+
27
+ if env['useless.doc.logger']
28
+ env['useless.doc.logger'].debug "selected UI: #{env['useless.doc.ui'].class.name}"
27
29
  end
28
30
 
29
- env['useless.doc.ui'] = ui
30
31
  @app.call(env)
31
32
  end
32
33
  end
@@ -0,0 +1,33 @@
1
+ require 'rack/request'
2
+
3
+ module Useless
4
+ module Doc
5
+ class Rack
6
+
7
+ # +Doc::Rack::URL+ translates the request URL into the corresponding
8
+ # API URL using the specified 'useless.doc.router'.
9
+ #
10
+ class URL
11
+ def initialize(app)
12
+ @app = app
13
+ end
14
+
15
+ def call(env)
16
+ request = ::Rack::Request.new(env)
17
+
18
+ if url = env['useless.doc.router'].api_for_doc(request.url)
19
+ env['useless.doc.url'] = url
20
+
21
+ if env['useless.doc.logger']
22
+ env['useless.doc.logger'].info "routing #{request.url} to #{env['useless.doc.url']}"
23
+ end
24
+
25
+ @app.call(env)
26
+ else
27
+ [404, {'Content-Type' => 'text/plain'}, ['Unknown documentation.']]
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,46 @@
1
+ require 'rack/builder'
2
+ require 'rack/commonlogger'
3
+ require 'low/rack/rack_errors'
4
+ require 'low/rack/log_level'
5
+ require 'low/rack/request_logger'
6
+
7
+ require 'useless/doc/router'
8
+ require 'useless/doc/rack/ui'
9
+ require 'useless/doc/rack/css'
10
+ require 'useless/doc/rack/url'
11
+ require 'useless/doc/rack/subject'
12
+ require 'useless/doc/rack/html'
13
+
14
+ module Useless
15
+ module Doc
16
+ class Rack
17
+ def initialize(router = nil)
18
+ @router = router || Useless::Doc::Router.default
19
+ end
20
+
21
+ def call(env)
22
+ env['useless.doc.router'] ||= @router
23
+ app.call(env)
24
+ end
25
+
26
+ private
27
+
28
+ def app
29
+ @app ||= begin
30
+ ::Rack::Builder.app do
31
+ use Low::Rack::RackErrors
32
+ use Low::Rack::LogLevel
33
+ use Low::Rack::RequestLogger, key: 'useless.doc.logger'
34
+ use ::Rack::CommonLogger
35
+
36
+ use Useless::Doc::Rack::UI
37
+ use Useless::Doc::Rack::CSS
38
+ use Useless::Doc::Rack::URL
39
+ use Useless::Doc::Rack::Subject
40
+ run Useless::Doc::Rack::HTML
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,62 @@
1
+ require 'uri'
2
+
3
+ module Useless
4
+ module Doc
5
+
6
+ # +Doc::Router+ determines the doc URL for an API and vice versa via
7
+ # the #doc_for_api and #api_for_doc methods, respectively.
8
+ #
9
+ module Router
10
+ def self.default
11
+ @default ||= Doc::Router::Default.new
12
+ end
13
+
14
+ def doc_for_api(url)
15
+ end
16
+
17
+ def api_for_doc(url)
18
+ end
19
+
20
+ class Default
21
+ include Doc::Router
22
+
23
+ def initialize(*supported_urls)
24
+ @supported_urls = supported_urls
25
+ end
26
+
27
+ def doc_for_api(url)
28
+ return nil unless supported_url?(url)
29
+ uri = URI(url)
30
+ host = uri.host
31
+ new_host = host.
32
+ split('.').
33
+ insert(-3, 'doc').
34
+ join('.')
35
+ "#{uri.scheme}://#{new_host}#{uri.path}"
36
+ end
37
+
38
+ def api_for_doc(url)
39
+ uri = URI(url)
40
+ host = uri.host
41
+ parts = host.split('.')
42
+ parts.slice!(-3) if parts[-3] == 'doc'
43
+ new_host = parts.join('.')
44
+ new_url = "#{uri.scheme}://#{new_host}#{uri.path}"
45
+ new_url if supported_url?(new_url)
46
+ end
47
+
48
+ private
49
+
50
+ def supported_url?(url)
51
+ if @supported_urls.nil? or @supported_urls.empty?
52
+ true
53
+ else
54
+ @supported_urls.any? do |supported_url|
55
+ url =~ Regexp.new(supported_url)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -18,6 +18,22 @@ module Useless
18
18
  hash.is_a?(String) ? hash : Oj.dump(hash)
19
19
  end
20
20
 
21
+ # Converts +Core::Domain+ instance to a JSON representation.
22
+ #
23
+ # @param [Core::Domain] domain the domain to be converted to JSON.
24
+ #
25
+ # @return [String] a JSON representation of the specified domain.
26
+ #
27
+ def self.domain(domain)
28
+ if domain
29
+ hash_to_json \
30
+ 'name' => domain.name,
31
+ 'url' => domain.url,
32
+ 'description' => domain.description,
33
+ 'apis' => domain.apis.map { |api| api(api) }
34
+ end
35
+ end
36
+
21
37
  # Converts +Core::API+ instance to a JSON representation.
22
38
  #
23
39
  # @param [Core::API] api the API to be converted to JSON.
@@ -27,6 +43,7 @@ module Useless
27
43
  def self.api(api)
28
44
  if api
29
45
  hash_to_json \
46
+ 'name' => api.name,
30
47
  'url' => api.url,
31
48
  'description' => api.description,
32
49
  'resources' => api.resources.map { |resource| resource(resource) }
@@ -24,6 +24,40 @@ module Useless
24
24
  json.is_a?(Hash) ? json : Oj.load(json)
25
25
  end
26
26
 
27
+ def self.load(json)
28
+ hash = json_to_hash json
29
+
30
+ if hash['apis']
31
+ self.domain(hash)
32
+ elsif hash['url']
33
+ self.api(hash)
34
+ elsif hash['path']
35
+ self.resource(hash)
36
+ end
37
+ end
38
+
39
+ # Converts a JSON represntation to an instance of +Core::Domain+
40
+ #
41
+ # @param [String, Hash] json the JSON representation to be converted to
42
+ # a domain.
43
+ #
44
+ # @return [Core::Domain] the domain corresponding to the specified
45
+ # JSON.
46
+ #
47
+ def self.domain(json)
48
+ hash = json_to_hash json
49
+
50
+ apis = (hash['apis'] || []).map do |json|
51
+ api json
52
+ end
53
+
54
+ Useless::Doc::Core::Domain.new \
55
+ name: hash['name'],
56
+ url: hash['url'],
57
+ description: hash['description'],
58
+ apis: apis
59
+ end
60
+
27
61
  # Converts a JSON represntation to an instance of +Core::API+
28
62
  #
29
63
  # @param [String, Hash] json the JSON representation to be converted to
@@ -40,6 +74,7 @@ module Useless
40
74
  end
41
75
 
42
76
  Useless::Doc::Core::API.new \
77
+ name: hash['name'],
43
78
  url: hash['url'],
44
79
  description: hash['description'],
45
80
  resources: resources
@@ -15,7 +15,8 @@ module Useless
15
15
  # class ResourceApp < Sinatra::Base
16
16
  # register Useless::Doc::Sinatra
17
17
  #
18
- # doc 'resources.useless.io' do
18
+ # doc 'Resouces API' do
19
+ # url 'resources.useless.io'
19
20
  # description 'A place with resources'
20
21
  # end
21
22
  #
@@ -42,8 +43,8 @@ module Useless
42
43
  @doc = doc
43
44
  end
44
45
 
45
- def doc(url = nil, &block)
46
- @dsl ||= Useless::Doc::DSL::API.new(url: url)
46
+ def doc(name = nil, &block)
47
+ @dsl ||= Useless::Doc::DSL::API.new(name: name)
47
48
  @dsl.instance_eval(&block) if block_given?
48
49
  @dsl
49
50
  end
@@ -1,38 +1,38 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
3
  <head>
4
- <title>{{url}} | doc.useless.io</title>
4
+ <title>{{name}} | doc.useless.io</title>
5
5
  <link href="/doc.css" media="screen" rel="stylesheet" type="text/css" />
6
6
  </head>
7
7
  <body>
8
8
 
9
- <h1>{{url}}</h1>
9
+ <header class="api">
10
+ <h1>{{name}}</h1>
10
11
 
11
- <p>{{description}}</p>
12
+ {{#resources}}
13
+ <section class="resource header-section">
14
+ <a class="path header-section-title" href="{{path}}">{{path}}</a>
12
15
 
13
- {{#resources}}
14
- <section>
15
- <h2>{{path}}</h2>
16
+ <p class="description">{{description}}</p>
16
17
 
17
- <p>{{description}}</p>
18
-
19
- <table>
20
- <th>
21
- <tr>
22
- <td>Method</td>
23
- <td>Description</td>
24
- <tr>
25
- </th>
26
- <tbody>
18
+ <table>
27
19
  {{#requests}}
28
- <tr>
29
- <td><a href="{{doc_path}}">{{method}}</a></td>
30
- <td>{{description}}</td>
31
- </tr>
20
+ <tbody>
21
+ <tr>
22
+ <td><a href="{{doc_path}}">{{method}}</a></td>
23
+ <td class="description">{{description}}</td>
24
+ </tr>
25
+ </tbody>
32
26
  {{/requests}}
33
- </tbody>
34
- </table>
35
- </section>
36
- {{/resources}}
27
+ </table>
28
+ </section>
29
+ {{/resources}}
30
+ </header>
31
+
32
+ <section class="main description">
33
+ <article>
34
+ <p class="description api">{{description}}</p>
35
+ </article>
36
+ </section>
37
37
 
38
38
  </body>
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>doc.useless.io</title>
5
+ <link href="/doc.css" media="screen" rel="stylesheet" type="text/css" />
6
+ </head>
7
+ <body>
8
+
9
+ <header class="domain">
10
+ <h1>{{name}}</h1>
11
+
12
+ {{#apis}}
13
+ <section class="api header-section">
14
+ <a class="name header-section-title" href="{{doc_url}}">{{name}}</a>
15
+
16
+ <p class="description">{{description}}</p>
17
+ </section>
18
+ {{/apis}}
19
+ </header>
20
+
21
+ <section class="main description">
22
+ <article>
23
+ <p class="description domain">{{description}}</p>
24
+ </article>
25
+ </section>
26
+ </body>