fdoc 0.2.1
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.
- 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
|