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
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
# fdoc: Documentation format and verification
|
2
|
+
|
3
|
+
High-quality documentation is extremely useful, but maintaining it is often a pain. We aim to create a tool to facilitate easy creation and maintenance of API documentation.
|
4
|
+
|
5
|
+
In a Rails app, fdoc can help document an API as well as verify that requests and responses adhere to their appropriate schemata.
|
6
|
+
|
7
|
+
Outside a Rails app, fdoc can provide a common format for API documentation, as well as the ability to generate basic HTML pages for humans to consume.
|
8
|
+
|
9
|
+
fdoc is short for Farnsdocs. They are named for everybody's favorite, good news-bearing, crotchety old man, Professor Farnsworth.
|
10
|
+
|
11
|
+
![Professor Farnsworth][github_img]
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
### In a Rails app
|
16
|
+
|
17
|
+
Add fdoc to your Gemfile.
|
18
|
+
|
19
|
+
gem 'fdoc'
|
20
|
+
|
21
|
+
Tell fdoc where to look for .fdoc files. By default, fdoc will look in `docs/fdoc`, but you can change this behavior to look anywhere. This fits best in something like a spec\_helper file.
|
22
|
+
|
23
|
+
require 'fdoc'
|
24
|
+
|
25
|
+
Fdoc.service_path = "path/to/your/fdocs"
|
26
|
+
|
27
|
+
fdoc is built to work around controller specs in rspec, and provides `Fdoc::SpecWatcher` as a mixin. Make sure to include it *inside* your top level describe.
|
28
|
+
|
29
|
+
require 'fdoc/spec_watcher'
|
30
|
+
|
31
|
+
describe MembersController do
|
32
|
+
include Fdoc::SpecWatcher
|
33
|
+
...
|
34
|
+
end
|
35
|
+
|
36
|
+
To enable fdoc for an endpoint, add the `fdoc` option with the path to the endpoint. fdoc will intercept all calls to `get`, `post`, `put`, and `delete` and verify those parameters accordingly.
|
37
|
+
|
38
|
+
context "#show", :fdoc => 'members/list' do
|
39
|
+
..
|
40
|
+
end
|
41
|
+
|
42
|
+
fdoc also has a scaffolding mode, where it attemps to infer the schema of a request based on sample responses. The interface is exactly the same as verifying, just set the environment variable `FDOC_SCAFFOLD=true`.
|
43
|
+
|
44
|
+
FDOC_SCAFFOLD=true bundle exec rspec spec/controllers
|
45
|
+
|
46
|
+
For more information on scaffolding, please see the more in-depth [fdoc scaffolding example][github_scaffold].
|
47
|
+
|
48
|
+
### Outside a Rails App
|
49
|
+
|
50
|
+
fdoc provides the `fdoc_to_html` script to transform a directory of `.fdoc` files into more human-readable HTML.
|
51
|
+
|
52
|
+
In this repo, try running:
|
53
|
+
|
54
|
+
bin/fdoc_to_html spec/fixtures html
|
55
|
+
|
56
|
+
## Example
|
57
|
+
|
58
|
+
`.fdoc` files are YAML files based on [JSON schema][json_schema] to describe API endpoints. They derive their endpoint path and verb from their filename.
|
59
|
+
|
60
|
+
- For more information on fdoc file naming conventions, please see the [fdoc file conventions guide][github_files].
|
61
|
+
- For more information on how fdoc uses JSON schema, please see the [json schema usage document][github_json].
|
62
|
+
|
63
|
+
Here is `members/list-POST.fdoc`:
|
64
|
+
|
65
|
+
description: The list of members.
|
66
|
+
requestParameters:
|
67
|
+
properties:
|
68
|
+
limit:
|
69
|
+
type: integer
|
70
|
+
required: no
|
71
|
+
default: 50
|
72
|
+
description: Limits the number of results returned, used for paging.
|
73
|
+
responseParameters:
|
74
|
+
properties:
|
75
|
+
members:
|
76
|
+
type: array
|
77
|
+
items:
|
78
|
+
title: member
|
79
|
+
description: Representation of a member
|
80
|
+
type: object
|
81
|
+
properties:
|
82
|
+
name:
|
83
|
+
description: Member's name
|
84
|
+
type: string
|
85
|
+
required: yes
|
86
|
+
example: Captain Smellypants
|
87
|
+
responseCodes:
|
88
|
+
- status: 200 OK
|
89
|
+
successful: yes
|
90
|
+
description: A list of current members
|
91
|
+
- status: 400 Bad Request
|
92
|
+
successful: no
|
93
|
+
description: Indicates malformed parameters
|
94
|
+
|
95
|
+
|
96
|
+
## Goals
|
97
|
+
|
98
|
+
- Client engineers should be able to participate in documenting an API and
|
99
|
+
keeping it up to date.
|
100
|
+
- Server engineers should be able to test their implementations.
|
101
|
+
- The documentation should be as close to the code as possible.
|
102
|
+
- Branches, reviews, and merges are the appropriate way to update the docs.
|
103
|
+
- Experimental drafts should just live on branches and never get
|
104
|
+
merged into master.
|
105
|
+
- Specification alone is not enough, there needs to be room for discussion.
|
106
|
+
|
107
|
+
## Contributing
|
108
|
+
|
109
|
+
Just fork and make a pull request! You will need to sign the [Individual Contributor License Agreement (CLA)][contrib_license] before we can merge your code.
|
110
|
+
|
111
|
+
|
112
|
+
|
113
|
+
|
114
|
+
[github_img]: https://github.com/square/fdoc/raw/master/docs/farnsworth.png
|
115
|
+
[github_scaffold]: https://github.com/square/fdoc/blob/master/docs/scaffold.md
|
116
|
+
[github_json]: https://github.com/square/fdoc/blob/master/docs/json_schema.md
|
117
|
+
[github_files]: https://github.com/square/fdoc/blob/master/docs/files.md
|
118
|
+
|
119
|
+
[json_schema]: http://json-schema.org/
|
120
|
+
[contrib_license]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1
|
data/Rakefile
ADDED
data/bin/fdoc_to_html
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.join(File.dirname(__FILE__), '../lib/fdoc')
|
4
|
+
|
5
|
+
if ARGV.length < 2
|
6
|
+
exec_name = File.basename($0)
|
7
|
+
abort "Usage: #{exec_name} [fdoc_directory] [html_directory] (url_base_path)"
|
8
|
+
end
|
9
|
+
|
10
|
+
fdoc_directory, html_directory, url_base_path = ARGV[0..2]
|
11
|
+
fdoc_directory = File.expand_path(fdoc_directory)
|
12
|
+
html_directory = File.expand_path(html_directory)
|
13
|
+
|
14
|
+
html_options = {
|
15
|
+
:html_directory => html_directory,
|
16
|
+
:static_html => true,
|
17
|
+
:url_base_path => url_base_path
|
18
|
+
}
|
19
|
+
|
20
|
+
def mkdir(dirname)
|
21
|
+
`mkdir -p #{dirname}` unless File.directory?(dirname)
|
22
|
+
end
|
23
|
+
|
24
|
+
mkdir(html_directory)
|
25
|
+
|
26
|
+
meta_service = Fdoc::MetaService.new(fdoc_directory)
|
27
|
+
services = if !meta_service.empty?
|
28
|
+
meta_service.services
|
29
|
+
else
|
30
|
+
Array(Fdoc::Service.new(fdoc_directory))
|
31
|
+
end
|
32
|
+
|
33
|
+
if !meta_service.empty?
|
34
|
+
meta_service_presenter= Fdoc::MetaServicePresenter.new(
|
35
|
+
meta_service, html_options
|
36
|
+
)
|
37
|
+
|
38
|
+
index_html_path = File.join(html_directory, "index.html")
|
39
|
+
File.open(index_html_path, "w") do |file|
|
40
|
+
file.write(meta_service_presenter.to_html)
|
41
|
+
|
42
|
+
puts "created #{index_html_path}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
css_path = File.join(File.dirname(__FILE__), '../lib/fdoc/templates/styles.css')
|
47
|
+
`cp "#{css_path}" "#{html_directory}"`
|
48
|
+
puts "created #{File.expand_path(css_path)}"
|
49
|
+
|
50
|
+
services.each do |service|
|
51
|
+
service_presenter = Fdoc::ServicePresenter.new(service, html_options)
|
52
|
+
html_sub_directory = if meta_service.empty?
|
53
|
+
html_directory
|
54
|
+
else
|
55
|
+
File.join(html_directory, service_presenter.slug_name)
|
56
|
+
end
|
57
|
+
|
58
|
+
mkdir(html_sub_directory)
|
59
|
+
|
60
|
+
index_html_path = File.join(html_sub_directory, "index.html")
|
61
|
+
File.open(index_html_path, "w") do |file|
|
62
|
+
file.write(service_presenter.to_html)
|
63
|
+
|
64
|
+
puts "created #{index_html_path}"
|
65
|
+
end
|
66
|
+
|
67
|
+
service_presenter.endpoints.flatten.each do |endpoint|
|
68
|
+
endpoint_html_path = File.join(html_sub_directory, endpoint.url)
|
69
|
+
mkdir(File.dirname(endpoint_html_path))
|
70
|
+
|
71
|
+
File.open(endpoint_html_path, "w") do |file|
|
72
|
+
file.write(endpoint.to_html)
|
73
|
+
|
74
|
+
puts "created #{endpoint_html_path}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/fdoc.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "fdoc"
|
5
|
+
|
6
|
+
s.version = File.read("#{File.dirname(__FILE__)}/VERSION")
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.rubygems_version = "1.3.7"
|
9
|
+
|
10
|
+
s.authors = ["Matt Wilson", "Zach Margolis", "Sean Sorrell"]
|
11
|
+
s.email = "support@squareup.com"
|
12
|
+
|
13
|
+
s.date = "2011-11-07"
|
14
|
+
s.description = "A tool for documenting API endpoints."
|
15
|
+
s.summary = "A tool for documenting API endpoints."
|
16
|
+
s.homepage = "http://github.com/square/fdoc"
|
17
|
+
|
18
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
19
|
+
s.extra_rdoc_files = [
|
20
|
+
"README.md"
|
21
|
+
]
|
22
|
+
s.require_paths = ["lib"]
|
23
|
+
s.files = Dir['{lib,spec}/**/*'] + %w(fdoc.gemspec Rakefile README.md Gemfile)
|
24
|
+
s.test_files = Dir['spec/**/*']
|
25
|
+
s.bindir = "bin"
|
26
|
+
s.executables << "fdoc_to_html"
|
27
|
+
|
28
|
+
s.add_dependency("json")
|
29
|
+
s.add_dependency("json-schema", ">= 1.0.1")
|
30
|
+
s.add_dependency("kramdown")
|
31
|
+
|
32
|
+
s.add_development_dependency("rake")
|
33
|
+
s.add_development_dependency("rspec", "~> 2.5")
|
34
|
+
s.add_development_dependency("nokogiri")
|
35
|
+
s.add_development_dependency("cane")
|
36
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
type: object
|
2
|
+
title: endpoint
|
3
|
+
description: Describes an API endpoint
|
4
|
+
additionalProperties: no
|
5
|
+
properties:
|
6
|
+
description:
|
7
|
+
type: string
|
8
|
+
required: no
|
9
|
+
description: Free-form text describing the endpoint. Rendered with a Markdown parser.
|
10
|
+
requestParameters:
|
11
|
+
type: object
|
12
|
+
description: A schema for the request parameters
|
13
|
+
responseParameters:
|
14
|
+
type: object
|
15
|
+
description: A schema for the response parameters
|
16
|
+
responseCodes:
|
17
|
+
type: array
|
18
|
+
required: yes
|
19
|
+
items:
|
20
|
+
type: object
|
21
|
+
description: |
|
22
|
+
A status code is defined by its HTTP status code and whether or not it was successful
|
23
|
+
properties:
|
24
|
+
status:
|
25
|
+
type: string
|
26
|
+
description: An HTTP status code, such as 200 OK or 201 Created
|
27
|
+
successful:
|
28
|
+
type: boolean
|
29
|
+
description: |
|
30
|
+
Describes if the client can interpret this response as successful.
|
data/lib/fdoc.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__))
|
2
|
+
|
3
|
+
module Fdoc
|
4
|
+
DEFAULT_SERVICE_PATH = "docs/fdoc"
|
5
|
+
|
6
|
+
def self.scaffold_mode?
|
7
|
+
ENV['FDOC_SCAFFOLD']
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.service_path=(service_path)
|
11
|
+
@service_path = service_path
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.service_path
|
15
|
+
@service_path || DEFAULT_SERVICE_PATH
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.decide_success_with(&block)
|
19
|
+
@success_block = block
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.decide_success(*args)
|
23
|
+
if @success_block
|
24
|
+
@success_block.call(*args)
|
25
|
+
else
|
26
|
+
true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Top-level fdoc validation error, abstract.
|
31
|
+
class ValidationError < StandardError; end
|
32
|
+
|
33
|
+
# Indicates an unknown response code.
|
34
|
+
class UndocumentedResponseCode < ValidationError; end
|
35
|
+
end
|
36
|
+
|
37
|
+
require 'fdoc/service'
|
38
|
+
require 'fdoc/meta_service'
|
39
|
+
require 'fdoc/endpoint'
|
40
|
+
require 'fdoc/endpoint_scaffold'
|
41
|
+
require 'fdoc/presenters/html_presenter'
|
42
|
+
require 'fdoc/presenters/service_presenter'
|
43
|
+
require 'fdoc/presenters/meta_service_presenter'
|
44
|
+
require 'fdoc/presenters/endpoint_presenter'
|
45
|
+
require 'fdoc/presenters/schema_presenter'
|
46
|
+
require 'fdoc/presenters/response_code_presenter'
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'json-schema'
|
3
|
+
|
4
|
+
# Endpoints represent the schema for an API endpoint
|
5
|
+
# The #consume_* methods will raise exceptions if input differs from the schema
|
6
|
+
class Fdoc::Endpoint
|
7
|
+
attr_reader :service
|
8
|
+
attr_reader :endpoint_path
|
9
|
+
|
10
|
+
def initialize(endpoint_path, service=Fdoc::Service::DefaultService)
|
11
|
+
@endpoint_path = endpoint_path
|
12
|
+
@schema = YAML.load_file(@endpoint_path)
|
13
|
+
@service = service
|
14
|
+
end
|
15
|
+
|
16
|
+
def consume_request(params, successful=true)
|
17
|
+
if successful
|
18
|
+
schema = set_additional_properties_false_on(request_parameters.dup)
|
19
|
+
JSON::Validator.validate!(schema, stringify_keys(params))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def consume_response(params, status_code, successful=true)
|
24
|
+
response_code = response_codes.find do |rc|
|
25
|
+
rc["status"] == status_code && rc["successful"] == successful
|
26
|
+
end
|
27
|
+
|
28
|
+
if !response_code
|
29
|
+
raise Fdoc::UndocumentedResponseCode,
|
30
|
+
'Undocumented response: %s, successful: %s' % [
|
31
|
+
status_code, successful
|
32
|
+
]
|
33
|
+
elsif successful
|
34
|
+
schema = set_additional_properties_false_on(response_parameters.dup)
|
35
|
+
JSON::Validator.validate!(schema, stringify_keys(params))
|
36
|
+
else
|
37
|
+
true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def verb
|
42
|
+
@verb ||= endpoint_path.match(/([A-Z]*)\.fdoc$/)[1]
|
43
|
+
end
|
44
|
+
|
45
|
+
def path
|
46
|
+
@path ||= endpoint_path.
|
47
|
+
gsub(service.service_dir, "").
|
48
|
+
match(/\/?(.*)[-\/][A-Z]+\.fdoc/)[1]
|
49
|
+
end
|
50
|
+
|
51
|
+
# properties
|
52
|
+
|
53
|
+
def deprecated?
|
54
|
+
@schema["deprecated"]
|
55
|
+
end
|
56
|
+
|
57
|
+
def description
|
58
|
+
@schema["description"]
|
59
|
+
end
|
60
|
+
|
61
|
+
def request_parameters
|
62
|
+
@schema["requestParameters"] ||= {}
|
63
|
+
end
|
64
|
+
|
65
|
+
def response_parameters
|
66
|
+
@schema["responseParameters"] ||= {}
|
67
|
+
end
|
68
|
+
|
69
|
+
def response_codes
|
70
|
+
@schema["responseCodes"] ||= []
|
71
|
+
end
|
72
|
+
|
73
|
+
protected
|
74
|
+
|
75
|
+
# default additionalProperties on objects to false
|
76
|
+
# create a copy, so we don't mutate the input
|
77
|
+
def set_additional_properties_false_on(value)
|
78
|
+
if value.kind_of? Hash
|
79
|
+
copy = value.dup
|
80
|
+
if value["type"] == "object" || value.has_key?("properties")
|
81
|
+
copy["additionalProperties"] ||= false
|
82
|
+
end
|
83
|
+
value.each do |key, hash_val|
|
84
|
+
unless key == "additionalProperties"
|
85
|
+
copy[key] = set_additional_properties_false_on(hash_val)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
copy
|
89
|
+
elsif value.kind_of? Array
|
90
|
+
copy = value.map do |arr_val|
|
91
|
+
set_additional_properties_false_on(arr_val)
|
92
|
+
end
|
93
|
+
else
|
94
|
+
value
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def stringify_keys(obj)
|
99
|
+
case obj
|
100
|
+
when Hash
|
101
|
+
result = {}
|
102
|
+
obj.each do |k, v|
|
103
|
+
result[k.to_s] = stringify_keys(v)
|
104
|
+
end
|
105
|
+
result
|
106
|
+
when Array then obj.map { |v| stringify_keys(v) }
|
107
|
+
else obj
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# EndpointScaffolds aggregate input to guess at the structure of an API
|
2
|
+
# endpoint. The #consume_* methods can modify the structure of the
|
3
|
+
# in-memory endpoint, to save the results to the file system, call #persist!
|
4
|
+
class Fdoc::EndpointScaffold < Fdoc::Endpoint
|
5
|
+
def initialize(endpoint_path, service=Fdoc::Service::DefaultService)
|
6
|
+
if File.exist?(endpoint_path)
|
7
|
+
super
|
8
|
+
else
|
9
|
+
@endpoint_path = endpoint_path
|
10
|
+
@schema = {
|
11
|
+
"description" => "???",
|
12
|
+
"responseCodes" => []
|
13
|
+
}
|
14
|
+
@service = service
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def persist!
|
19
|
+
dirname = File.dirname(@endpoint_path)
|
20
|
+
Dir.mkdir(dirname) unless File.directory?(dirname)
|
21
|
+
|
22
|
+
File.open(@endpoint_path, "w") do |file|
|
23
|
+
YAML.dump(@schema, file)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def consume_request(params, successful = true)
|
28
|
+
scaffold_schema(request_parameters, stringify_keys(params), {
|
29
|
+
:root_object => true
|
30
|
+
})
|
31
|
+
end
|
32
|
+
|
33
|
+
def consume_response(params, status_code, successful=true)
|
34
|
+
if successful
|
35
|
+
scaffold_schema(response_parameters, stringify_keys(params), {
|
36
|
+
:root_object => true
|
37
|
+
})
|
38
|
+
end
|
39
|
+
|
40
|
+
response_code = response_codes.find do
|
41
|
+
|rc| rc["status"] == status_code && rc["successful"] == successful
|
42
|
+
end
|
43
|
+
|
44
|
+
if !response_code
|
45
|
+
response_codes << {
|
46
|
+
"status" => status_code,
|
47
|
+
"successful" => successful,
|
48
|
+
"description" => "???"
|
49
|
+
}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
protected
|
54
|
+
|
55
|
+
def scaffold_schema(schema, params, options = {:root_object => false})
|
56
|
+
unless options[:root_object]
|
57
|
+
schema["description"] ||= "???"
|
58
|
+
schema["required"] = "???" unless schema.has_key?("required")
|
59
|
+
end
|
60
|
+
|
61
|
+
if params.kind_of? Hash
|
62
|
+
scaffold_hash(schema, params, options)
|
63
|
+
elsif params.kind_of? Array
|
64
|
+
scaffold_array(schema, params, options)
|
65
|
+
else
|
66
|
+
scaffold_atom(schema, params, options)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def scaffold_hash(schema, params, options = {})
|
71
|
+
schema["type"] ||= "object" unless options[:root_object]
|
72
|
+
schema["properties"] ||= {}
|
73
|
+
|
74
|
+
params.each do |key, value|
|
75
|
+
unless schema[key]
|
76
|
+
schema["properties"][key] ||= {}
|
77
|
+
sub_options = options.merge(:root_object => false)
|
78
|
+
scaffold_schema(schema["properties"][key], value, sub_options)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def scaffold_array(schema, params, options = {})
|
84
|
+
schema["type"] ||= "array"
|
85
|
+
schema["items"] ||= {}
|
86
|
+
params.each do |arr_value|
|
87
|
+
sub_options = options.merge(:root_object => false)
|
88
|
+
scaffold_schema(schema["items"], arr_value, options.merge(sub_options))
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def scaffold_atom(schema, params, options = {})
|
93
|
+
value = params
|
94
|
+
schema["type"] ||= guess_type(params)
|
95
|
+
if format = guess_format(params)
|
96
|
+
schema["format"] ||= format
|
97
|
+
end
|
98
|
+
schema["example"] ||= value
|
99
|
+
end
|
100
|
+
|
101
|
+
def guess_type(value)
|
102
|
+
in_type = value.class.to_s
|
103
|
+
type_map = {
|
104
|
+
"Fixnum" => "integer",
|
105
|
+
"Float" => "number",
|
106
|
+
"Hash" => "object",
|
107
|
+
"Time" => "string",
|
108
|
+
"TrueClass" => "boolean",
|
109
|
+
"FalseClass" => "boolean",
|
110
|
+
"NilClass" => "null"
|
111
|
+
}
|
112
|
+
type_map[in_type] || in_type.downcase
|
113
|
+
end
|
114
|
+
|
115
|
+
def guess_format(value)
|
116
|
+
if value.kind_of? Time
|
117
|
+
"date-time"
|
118
|
+
elsif value.kind_of? String
|
119
|
+
if value.start_with? "http://"
|
120
|
+
"uri"
|
121
|
+
elsif value.match(/\#[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?\b/)
|
122
|
+
"color"
|
123
|
+
else
|
124
|
+
begin
|
125
|
+
"date-time" if Time.iso8601(value)
|
126
|
+
rescue
|
127
|
+
nil
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|