useless-doc 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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>