fdoc 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +2 -0
- data/README.md +120 -0
- data/Rakefile +7 -0
- data/bin/fdoc_to_html +77 -0
- data/fdoc.gemspec +36 -0
- data/lib/endpoint-schema.yaml +30 -0
- data/lib/fdoc.rb +46 -0
- data/lib/fdoc/endpoint.rb +110 -0
- data/lib/fdoc/endpoint_scaffold.rb +132 -0
- data/lib/fdoc/meta_service.rb +46 -0
- data/lib/fdoc/presenters/endpoint_presenter.rb +152 -0
- data/lib/fdoc/presenters/html_presenter.rb +58 -0
- data/lib/fdoc/presenters/meta_service_presenter.rb +65 -0
- data/lib/fdoc/presenters/response_code_presenter.rb +32 -0
- data/lib/fdoc/presenters/schema_presenter.rb +138 -0
- data/lib/fdoc/presenters/service_presenter.rb +56 -0
- data/lib/fdoc/service.rb +88 -0
- data/lib/fdoc/spec_watcher.rb +48 -0
- data/lib/fdoc/templates/endpoint.html.erb +75 -0
- data/lib/fdoc/templates/meta_service.html.erb +60 -0
- data/lib/fdoc/templates/service.html.erb +54 -0
- data/lib/fdoc/templates/styles.css +63 -0
- data/spec/fdoc/endpoint_scaffold_spec.rb +242 -0
- data/spec/fdoc/endpoint_spec.rb +243 -0
- data/spec/fdoc/presenters/endpoint_presenter_spec.rb +93 -0
- data/spec/fdoc/presenters/service_presenter_spec.rb +18 -0
- data/spec/fdoc/service_spec.rb +63 -0
- data/spec/fixtures/members/add-PUT.fdoc +20 -0
- data/spec/fixtures/members/draft-POST.fdoc +5 -0
- data/spec/fixtures/members/list/GET.fdoc +50 -0
- data/spec/fixtures/members/list/complex-params-GET.fdoc +94 -0
- data/spec/fixtures/members/list/filter-GET.fdoc +60 -0
- data/spec/fixtures/members/members.fdoc.service +11 -0
- data/spec/fixtures/sample_group.fdoc.meta +9 -0
- data/spec/spec_helper.rb +2 -0
- metadata +174 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
# MetaServices are collections of services
|
4
|
+
class Fdoc::MetaService
|
5
|
+
attr_reader :meta_service_dir
|
6
|
+
|
7
|
+
def initialize(meta_service_dir)
|
8
|
+
@meta_service_dir = meta_service_dir
|
9
|
+
|
10
|
+
service_path = Dir["#{meta_service_dir}/*.fdoc.meta"].first
|
11
|
+
@schema = if service_path
|
12
|
+
YAML.load_file(service_path)
|
13
|
+
else
|
14
|
+
{}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def empty?
|
19
|
+
@schema.empty?
|
20
|
+
end
|
21
|
+
|
22
|
+
def services
|
23
|
+
@schema['services'].map do |path|
|
24
|
+
service_path = if path.start_with?('/') || path.start_with?('~')
|
25
|
+
path
|
26
|
+
else
|
27
|
+
File.join(meta_service_dir, path)
|
28
|
+
end
|
29
|
+
serv = Fdoc::Service.new(service_path)
|
30
|
+
serv.meta_service = self
|
31
|
+
serv
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def name
|
36
|
+
@schema['name']
|
37
|
+
end
|
38
|
+
|
39
|
+
def description
|
40
|
+
@schema['description']
|
41
|
+
end
|
42
|
+
|
43
|
+
def discussion
|
44
|
+
@schema['discussion']
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# HtmlPresenter for an Endpoint
|
2
|
+
class Fdoc::EndpointPresenter < Fdoc::HtmlPresenter
|
3
|
+
attr_accessor :service_presenter, :endpoint
|
4
|
+
|
5
|
+
def initialize(endpoint, options = {})
|
6
|
+
super(options)
|
7
|
+
@endpoint = endpoint
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_html
|
11
|
+
render_erb('endpoint.html.erb')
|
12
|
+
end
|
13
|
+
|
14
|
+
def name
|
15
|
+
<<-EOS
|
16
|
+
<span class="endpoint-name #{@endpoint.deprecated? ? 'deprecated' : nil}">
|
17
|
+
<span class="verb">#{@endpoint.verb}</span>
|
18
|
+
<span class="root">#{zws_ify(@endpoint.service.base_path)}</span><span
|
19
|
+
class="path">#{zws_ify(@endpoint.path)}</span>
|
20
|
+
#{@endpoint.deprecated? ? '(deprecated)' : nil}
|
21
|
+
</span>
|
22
|
+
EOS
|
23
|
+
end
|
24
|
+
|
25
|
+
def name_as_link
|
26
|
+
<<-EOS
|
27
|
+
<a href="#{url}">
|
28
|
+
#{name}
|
29
|
+
</a>
|
30
|
+
EOS
|
31
|
+
end
|
32
|
+
|
33
|
+
def url(extension = ".html")
|
34
|
+
'%s%s-%s%s' % [ options[:prefix], endpoint.path, endpoint.verb, extension ]
|
35
|
+
end
|
36
|
+
|
37
|
+
def title
|
38
|
+
'%s %s - %s' % [ endpoint.verb, endpoint.path, endpoint.service.name ]
|
39
|
+
end
|
40
|
+
|
41
|
+
def prefix
|
42
|
+
endpoint.path.split("/").first
|
43
|
+
end
|
44
|
+
|
45
|
+
def zws_ify(str)
|
46
|
+
# zero-width-space, makes long lines friendlier for breaking
|
47
|
+
str.gsub(/\//, '​/') if str
|
48
|
+
end
|
49
|
+
|
50
|
+
def description
|
51
|
+
render_markdown(endpoint.description)
|
52
|
+
end
|
53
|
+
|
54
|
+
def show_request?
|
55
|
+
!endpoint.request_parameters.empty?
|
56
|
+
end
|
57
|
+
|
58
|
+
def show_response?
|
59
|
+
!endpoint.response_parameters.empty?
|
60
|
+
end
|
61
|
+
|
62
|
+
def request_parameters
|
63
|
+
Fdoc::SchemaPresenter.new(endpoint.request_parameters,
|
64
|
+
options.merge(:request => true)
|
65
|
+
).to_html
|
66
|
+
end
|
67
|
+
|
68
|
+
def response_parameters
|
69
|
+
Fdoc::SchemaPresenter.new(endpoint.response_parameters, options).to_html
|
70
|
+
end
|
71
|
+
|
72
|
+
def response_codes
|
73
|
+
@response_codes ||= endpoint.response_codes.map do |response_code|
|
74
|
+
Fdoc::ResponseCodePresenter.new(response_code, options)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def successful_response_codes
|
79
|
+
response_codes.select { |response_code| response_code.successful? }
|
80
|
+
end
|
81
|
+
|
82
|
+
def failure_response_codes
|
83
|
+
response_codes.select { |response_code| !response_code.successful? }
|
84
|
+
end
|
85
|
+
|
86
|
+
def example_request
|
87
|
+
render_json(example_from_schema(endpoint.request_parameters))
|
88
|
+
end
|
89
|
+
|
90
|
+
def example_response
|
91
|
+
render_json(example_from_schema(endpoint.response_parameters))
|
92
|
+
end
|
93
|
+
|
94
|
+
ATOMIC_TYPES = %w(string integer number boolean null)
|
95
|
+
|
96
|
+
def example_from_schema(schema)
|
97
|
+
if schema.nil?
|
98
|
+
return nil
|
99
|
+
end
|
100
|
+
|
101
|
+
type = Array(schema["type"])
|
102
|
+
|
103
|
+
if type.any? { |t| ATOMIC_TYPES.include?(t) }
|
104
|
+
schema["example"] || schema["default"] || example_from_atom(schema)
|
105
|
+
elsif type.include?("object") || schema["properties"]
|
106
|
+
example_from_object(schema)
|
107
|
+
elsif type.include?("array") || schema["items"]
|
108
|
+
example_from_array(schema)
|
109
|
+
else
|
110
|
+
{}
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def example_from_atom(schema)
|
115
|
+
type = Array(schema["type"])
|
116
|
+
hash = schema.hash
|
117
|
+
|
118
|
+
if type.include?("boolean")
|
119
|
+
[true, false][hash % 2]
|
120
|
+
elsif type.include?("integer")
|
121
|
+
hash % 1000
|
122
|
+
elsif type.include?("number")
|
123
|
+
Math.sqrt(hash % 1000).round 2
|
124
|
+
elsif type.include?("string")
|
125
|
+
""
|
126
|
+
else
|
127
|
+
nil
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def example_from_object(object)
|
132
|
+
example = {}
|
133
|
+
if object["properties"]
|
134
|
+
object["properties"].each do |key, value|
|
135
|
+
example[key] = example_from_schema(value)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
example
|
139
|
+
end
|
140
|
+
|
141
|
+
def example_from_array(array)
|
142
|
+
if array["items"].kind_of? Array
|
143
|
+
example = []
|
144
|
+
array["items"].each do |item|
|
145
|
+
example << example_from_schema(item)
|
146
|
+
end
|
147
|
+
example
|
148
|
+
else
|
149
|
+
[ example_from_schema(array["items"]) ]
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'kramdown'
|
3
|
+
|
4
|
+
# HtmlPresenters assist in generating Html for fdoc classes.
|
5
|
+
# HtmlPresenters is an abstract class with a lot of helper methods
|
6
|
+
# for URLs and common text styling tasks (like #render_markdown
|
7
|
+
# and #render_json)
|
8
|
+
class Fdoc::HtmlPresenter
|
9
|
+
attr_reader :options
|
10
|
+
|
11
|
+
def initialize(options = {})
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
def render_erb(erb_name)
|
16
|
+
template_path = File.join(File.dirname(__FILE__), "../templates", erb_name)
|
17
|
+
template = ERB.new(File.read(template_path))
|
18
|
+
template.result(binding)
|
19
|
+
end
|
20
|
+
|
21
|
+
def render_markdown(markdown_str)
|
22
|
+
if markdown_str
|
23
|
+
Kramdown::Document.new(markdown_str, :entity_output => :numeric).to_html
|
24
|
+
else
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def render_json(json)
|
30
|
+
if json.kind_of? String
|
31
|
+
'<tt>"%s"</tt>' % json.gsub(/\"/, 'quot;')
|
32
|
+
elsif json.kind_of?(Numeric) ||
|
33
|
+
json.kind_of?(TrueClass) ||
|
34
|
+
json.kind_of?(FalseClass)
|
35
|
+
'<tt>%s</tt>' % json
|
36
|
+
elsif json.kind_of?(Hash) ||
|
37
|
+
json.kind_of?(Array)
|
38
|
+
'<pre><code>%s</code></pre>' % JSON.pretty_generate(json)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def html_directory
|
43
|
+
options[:url_base_path] || options[:html_directory] || ""
|
44
|
+
end
|
45
|
+
|
46
|
+
def css_path
|
47
|
+
File.join(html_directory, "styles.css")
|
48
|
+
end
|
49
|
+
|
50
|
+
def index_path(subdirectory = "")
|
51
|
+
html_path = File.join(html_directory, subdirectory)
|
52
|
+
if options[:static_html]
|
53
|
+
File.join(html_path, 'index.html')
|
54
|
+
else
|
55
|
+
html_path
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# HtmlPresenter for Fdoc::MetaService
|
2
|
+
class Fdoc::MetaServicePresenter < Fdoc::HtmlPresenter
|
3
|
+
attr_reader :meta_service
|
4
|
+
|
5
|
+
def initialize(meta_service, options = {})
|
6
|
+
super(options)
|
7
|
+
@meta_service = meta_service
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_html
|
11
|
+
render_erb('meta_service.html.erb')
|
12
|
+
end
|
13
|
+
|
14
|
+
def services
|
15
|
+
@services ||= meta_service.services.
|
16
|
+
sort_by(&:name).
|
17
|
+
map do |service|
|
18
|
+
Fdoc::ServicePresenter.new(service, options)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def endpoints
|
23
|
+
if !@endpoints
|
24
|
+
@endpoints = []
|
25
|
+
prefix = nil
|
26
|
+
|
27
|
+
ungrouped_endpoints.each do |endpoint|
|
28
|
+
presenter = presenter_from_endpoint(endpoint)
|
29
|
+
current_prefix = presenter.prefix
|
30
|
+
|
31
|
+
@endpoints << [] if prefix != current_prefix
|
32
|
+
@endpoints.last << presenter
|
33
|
+
|
34
|
+
prefix = current_prefix
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
@endpoints
|
39
|
+
end
|
40
|
+
|
41
|
+
def description
|
42
|
+
render_markdown(meta_service.description)
|
43
|
+
end
|
44
|
+
|
45
|
+
def discussion
|
46
|
+
render_markdown(meta_service.discussion)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def ungrouped_endpoints
|
52
|
+
meta_service.services.
|
53
|
+
map(&:endpoints).
|
54
|
+
flatten.
|
55
|
+
sort_by(&:endpoint_path)
|
56
|
+
end
|
57
|
+
|
58
|
+
def presenter_from_endpoint(endpoint)
|
59
|
+
service_presenter = Fdoc::ServicePresenter.new(endpoint.service)
|
60
|
+
|
61
|
+
presenter = Fdoc::EndpointPresenter.new(endpoint,
|
62
|
+
options.merge(:prefix => (service_presenter.slug_name + "/")))
|
63
|
+
presenter.service_presenter = service_presenter
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# An HtmlPresenter for ResponseCodes
|
2
|
+
class Fdoc::ResponseCodePresenter < Fdoc::HtmlPresenter
|
3
|
+
attr_reader :response_code
|
4
|
+
|
5
|
+
def initialize(response_code, options)
|
6
|
+
super(options)
|
7
|
+
@response_code = response_code
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_html
|
11
|
+
<<-EOS
|
12
|
+
<div class="response-code">
|
13
|
+
<span class="status">
|
14
|
+
#{status}
|
15
|
+
</span>
|
16
|
+
#{description}
|
17
|
+
</div>
|
18
|
+
EOS
|
19
|
+
end
|
20
|
+
|
21
|
+
def successful?
|
22
|
+
response_code["successful"]
|
23
|
+
end
|
24
|
+
|
25
|
+
def status
|
26
|
+
response_code["status"]
|
27
|
+
end
|
28
|
+
|
29
|
+
def description
|
30
|
+
render_markdown(response_code["description"])
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# An HtmlPresenter for a JSON Schema fragment. Like most JSON
|
2
|
+
# schema things, has a tendency to recurse.
|
3
|
+
class Fdoc::SchemaPresenter < Fdoc::HtmlPresenter
|
4
|
+
FORMATTED_KEYS = %w(
|
5
|
+
description
|
6
|
+
type
|
7
|
+
required
|
8
|
+
example
|
9
|
+
deprecated
|
10
|
+
default
|
11
|
+
format
|
12
|
+
enum
|
13
|
+
items
|
14
|
+
properties
|
15
|
+
)
|
16
|
+
|
17
|
+
def initialize(schema, options)
|
18
|
+
super(options)
|
19
|
+
@schema = schema
|
20
|
+
end
|
21
|
+
|
22
|
+
def request?
|
23
|
+
options[:request]
|
24
|
+
end
|
25
|
+
|
26
|
+
def nested?
|
27
|
+
options[:nested]
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_html
|
31
|
+
html = StringIO.new
|
32
|
+
|
33
|
+
html << '<span class="deprecated">Deprecated</span>' if deprecated?
|
34
|
+
|
35
|
+
html << '<div class="schema">'
|
36
|
+
html << render_markdown(@schema["description"])
|
37
|
+
|
38
|
+
html << '<ul>'
|
39
|
+
begin
|
40
|
+
html << '<li>Type: %s</li>' % type if type
|
41
|
+
html << '<li>Format: %s</li>' % format if format
|
42
|
+
html << '<li>Required: %s</li>' % required? if nested?
|
43
|
+
html << '<li>Example: %s</li>' % example if example
|
44
|
+
html << enum_html
|
45
|
+
|
46
|
+
(@schema.keys - FORMATTED_KEYS).each do |key|
|
47
|
+
html << '<li>%s: %s</li>' % [ key, @schema[key] ]
|
48
|
+
end
|
49
|
+
|
50
|
+
html << items_html
|
51
|
+
html << properties_html
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
html << '</ul>'
|
56
|
+
html << '</div>'
|
57
|
+
|
58
|
+
html.string
|
59
|
+
end
|
60
|
+
|
61
|
+
def type
|
62
|
+
t = @schema["type"]
|
63
|
+
if t.kind_of? Array
|
64
|
+
t.join(", ")
|
65
|
+
elsif t != "object"
|
66
|
+
t
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def format
|
71
|
+
@schema["format"]
|
72
|
+
end
|
73
|
+
|
74
|
+
def example
|
75
|
+
return unless e = @schema["example"]
|
76
|
+
|
77
|
+
render_json(e)
|
78
|
+
end
|
79
|
+
|
80
|
+
def deprecated?
|
81
|
+
@schema["deprecated"]
|
82
|
+
end
|
83
|
+
|
84
|
+
def required?
|
85
|
+
@schema["required"] ? "yes" : "no"
|
86
|
+
end
|
87
|
+
|
88
|
+
def enum_html
|
89
|
+
enum = @schema["enum"]
|
90
|
+
return unless enum
|
91
|
+
|
92
|
+
list = enum.map do |e|
|
93
|
+
'<tt>%s</tt>' % e
|
94
|
+
end.join(", ")
|
95
|
+
|
96
|
+
html = StringIO.new
|
97
|
+
html << '<li>Enum: '
|
98
|
+
html << list
|
99
|
+
html << '</li>'
|
100
|
+
html.string
|
101
|
+
end
|
102
|
+
|
103
|
+
def items_html
|
104
|
+
return unless items = @schema["items"]
|
105
|
+
|
106
|
+
html = ""
|
107
|
+
html << '<li>Items'
|
108
|
+
|
109
|
+
sub_options = options.merge(:nested => true)
|
110
|
+
|
111
|
+
if items.kind_of? Array
|
112
|
+
item.compact.each do |item|
|
113
|
+
html << self.class.new(item, sub_options).to_html
|
114
|
+
end
|
115
|
+
else
|
116
|
+
html << self.class.new(items, sub_options).to_html
|
117
|
+
end
|
118
|
+
|
119
|
+
html << '</li>'
|
120
|
+
html
|
121
|
+
end
|
122
|
+
|
123
|
+
def properties_html
|
124
|
+
return unless properties = @schema["properties"]
|
125
|
+
|
126
|
+
html = ""
|
127
|
+
|
128
|
+
properties.each do |key, property|
|
129
|
+
next if property.nil?
|
130
|
+
html << '<li>'
|
131
|
+
html << '<tt>%s</tt>' % key
|
132
|
+
html << self.class.new(property, options.merge(:nested => true)).to_html
|
133
|
+
html << '</li>'
|
134
|
+
end
|
135
|
+
|
136
|
+
html
|
137
|
+
end
|
138
|
+
end
|