fdoc 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. data/Gemfile +2 -0
  2. data/README.md +120 -0
  3. data/Rakefile +7 -0
  4. data/bin/fdoc_to_html +77 -0
  5. data/fdoc.gemspec +36 -0
  6. data/lib/endpoint-schema.yaml +30 -0
  7. data/lib/fdoc.rb +46 -0
  8. data/lib/fdoc/endpoint.rb +110 -0
  9. data/lib/fdoc/endpoint_scaffold.rb +132 -0
  10. data/lib/fdoc/meta_service.rb +46 -0
  11. data/lib/fdoc/presenters/endpoint_presenter.rb +152 -0
  12. data/lib/fdoc/presenters/html_presenter.rb +58 -0
  13. data/lib/fdoc/presenters/meta_service_presenter.rb +65 -0
  14. data/lib/fdoc/presenters/response_code_presenter.rb +32 -0
  15. data/lib/fdoc/presenters/schema_presenter.rb +138 -0
  16. data/lib/fdoc/presenters/service_presenter.rb +56 -0
  17. data/lib/fdoc/service.rb +88 -0
  18. data/lib/fdoc/spec_watcher.rb +48 -0
  19. data/lib/fdoc/templates/endpoint.html.erb +75 -0
  20. data/lib/fdoc/templates/meta_service.html.erb +60 -0
  21. data/lib/fdoc/templates/service.html.erb +54 -0
  22. data/lib/fdoc/templates/styles.css +63 -0
  23. data/spec/fdoc/endpoint_scaffold_spec.rb +242 -0
  24. data/spec/fdoc/endpoint_spec.rb +243 -0
  25. data/spec/fdoc/presenters/endpoint_presenter_spec.rb +93 -0
  26. data/spec/fdoc/presenters/service_presenter_spec.rb +18 -0
  27. data/spec/fdoc/service_spec.rb +63 -0
  28. data/spec/fixtures/members/add-PUT.fdoc +20 -0
  29. data/spec/fixtures/members/draft-POST.fdoc +5 -0
  30. data/spec/fixtures/members/list/GET.fdoc +50 -0
  31. data/spec/fixtures/members/list/complex-params-GET.fdoc +94 -0
  32. data/spec/fixtures/members/list/filter-GET.fdoc +60 -0
  33. data/spec/fixtures/members/members.fdoc.service +11 -0
  34. data/spec/fixtures/sample_group.fdoc.meta +9 -0
  35. data/spec/spec_helper.rb +2 -0
  36. 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(/\//, '&#8203;/') 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>&quot;%s&quot;</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