apispec 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2011 Vincent Landgraf
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1 @@
1
+ TODO
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "rubygems"
2
+ require "spec/rake/spectask"
3
+ Spec::Rake::SpecTask.new(:spec) do |t|
4
+ t.spec_files = Dir.glob('spec/**/*_spec.rb')
5
+ t.spec_opts << '--diff --format d'
6
+ #t.rcov = true
7
+ end
8
+
9
+ task :default => :spec
data/apispec-0.0.2.gem ADDED
Binary file
data/apispec.gemspec ADDED
@@ -0,0 +1,37 @@
1
+ $:.push('lib')
2
+ require "apispec/version"
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'apispec'
6
+ s.version = APISpec::VERSION.dup
7
+ s.date = Time.now
8
+ s.summary = "A ruby based http/rest documentation generator"
9
+ s.email = "vilandgr+github@googlemail.com"
10
+ s.homepage = "http://github.com/threez/apispec/"
11
+ s.authors = ['Vincent Landgraf']
12
+ s.description = "A documentation generator for http/rest"
13
+
14
+ dependencies = [
15
+ [:runtime, "RedCloth", "~> 4.2.7"],
16
+ [:runtime, "coderay", "~> 0.9.7"],
17
+ [:development, "rspec", "~> 2.1"],
18
+ ]
19
+
20
+ s.files = Dir['**/*']
21
+ s.test_files = Dir['test/**/*'] + Dir['spec/**/*']
22
+ s.executables = Dir['bin/*'].map { |f| File.basename(f) }
23
+ s.require_paths = ["lib"]
24
+
25
+ ## Make sure you can build the gem on older versions of RubyGems too:
26
+ s.rubygems_version = APISpec::VERSION.dup
27
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
28
+ s.specification_version = 3 if s.respond_to? :specification_version
29
+
30
+ dependencies.each do |type, name, version|
31
+ if s.respond_to?("add_#{type}_dependency")
32
+ s.send("add_#{type}_dependency", name, version)
33
+ else
34
+ s.add_dependency(name, version)
35
+ end
36
+ end
37
+ end
data/bin/apispec ADDED
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env ruby
2
+ require 'optparse'
3
+
4
+ # grab the project root for tempaltes and the lib
5
+ project_root = File.join(File.dirname(__FILE__), "..")
6
+
7
+ require File.expand_path(File.join(project_root, "lib", "apispec"))
8
+
9
+ # the default tempate dir
10
+ template_dir = File.join(project_root, "templates")
11
+ resource_dir = File.join(project_root, "resources")
12
+
13
+ class DirectoryError < StandardError; end
14
+ def require_directory!(dir)
15
+ dir = File.expand_path(dir)
16
+ raise DirectoryError.new("directory #{dir} doesn't exist!") unless File.exist? dir
17
+ dir
18
+ end
19
+
20
+ options = {
21
+ :workspace => File.expand_path("."),
22
+ :template => File.expand_path(template_dir),
23
+ :output => File.expand_path("apidoc"),
24
+ :resource => File.expand_path(resource_dir)
25
+ }
26
+ parser = nil
27
+
28
+ begin
29
+ parser = OptionParser.new do |opts|
30
+ opts.banner = "Usage: #{$0} [options]"
31
+ opts.version = APISpec::VERSION
32
+
33
+ opts.separator ""
34
+ opts.separator "No existing project options:"
35
+
36
+ opts.on("-c", "--create <name>", "create a new project") do |name|
37
+ options[:create] = name
38
+ end
39
+
40
+ opts.separator ""
41
+ opts.separator "Project options:"
42
+
43
+ opts.on("-w", "--workspace <dir>",
44
+ "The directory containing the source files (default: .)") do |dir|
45
+ options[:workspace] = require_directory!(dir)
46
+ end
47
+
48
+ opts.on("-t", "--template <dir>",
49
+ "The template directory (default: bundled with apispec)") do |dir|
50
+ options[:template] = require_directory!(dir)
51
+ end
52
+
53
+ opts.on("-r", "--rescoure <dir>",
54
+ "The resource directory (default: bundled with apispec)") do |dir|
55
+ options[:template] = require_directory!(dir)
56
+ end
57
+
58
+ opts.on("-p", "--print",
59
+ "The resource directory (default: bundled with apispec)") do
60
+ options[:print] = true
61
+ end
62
+
63
+ opts.on("-o", "--output <dir>",
64
+ "The output directory (default: apidoc)") do |dir|
65
+ options[:output] = File.expand_path(dir)
66
+ end
67
+
68
+ # opts.on("-s", "--server <port>", "Don't create a output folder start a server on given port") do |dir|
69
+ # options[:output] = dir
70
+ # end
71
+
72
+ opts.separator ""
73
+ opts.separator "Common options:"
74
+
75
+ opts.on("-v", "--verbose", "Run verbosely") do
76
+ options[:verbose] = true
77
+ end
78
+ end
79
+ parser.parse!
80
+ rescue DirectoryError => ex
81
+ STDERR.puts "Invalid Argument: #{ex.message}"
82
+ STDERR.puts parser.help
83
+ exit 3
84
+ rescue OptionParser::ParseError => ex
85
+ STDERR.puts ex.message
86
+ STDERR.puts parser.help
87
+ exit 2
88
+ end
89
+
90
+ if options[:print]
91
+ # just print the structure
92
+ generator = APISpec::Generator.new(options)
93
+ generator.parse_files!
94
+ puts generator.namespace.print_tree()
95
+ elsif dir = options[:create]
96
+ # create a new project based on the example project
97
+ new_dir = File.expand_path(dir)
98
+ FileUtils.mkdir_p(new_dir)
99
+ FileUtils.cp_r(Dir[File.expand_path(File.join(project_root, "example", "*"))],
100
+ new_dir)
101
+ elsif Dir[File.join(options[:workspace], "**", "*.rb")].any?
102
+ begin
103
+ APISpec::Generator.new(options).start!
104
+ rescue APISpec::Namespace::ReferenceError => ex
105
+ STDERR.puts ex.message
106
+ exit 4
107
+ end
108
+ else
109
+ STDERR.puts "There are no files to generate a documentation from!"
110
+ STDERR.puts parser.help
111
+ exit 5
112
+ end
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": 0,
3
+ "firstname": "John",
4
+ "lastname": "Doe",
5
+ "login": "john.doe",
6
+ "password": "secretxxx"
7
+ }
@@ -0,0 +1,59 @@
1
+ field "X-User", "UserID" do
2
+ desc "The users email address"
3
+ example "test@example.com"
4
+ end
5
+
6
+ field "folder_id", "FolderID" do
7
+ desc "unique id of folder for user"
8
+ end
9
+
10
+ object "Folder" do
11
+ fields "FolderID"
12
+ example :text, %q{
13
+ asd
14
+ }
15
+ end
16
+
17
+ interface "Folders" do
18
+ base_uri "http://example.com/mail-rest"
19
+
20
+ get 'folders' do
21
+ desc "returns the list of folders"
22
+
23
+ request do
24
+ headers "UserID"
25
+ end
26
+
27
+ response do
28
+ example :json, %q|
29
+ {
30
+ "Kreditkarte" : "Xema",
31
+ "Nummer" : "1234-5678-9012-3456",
32
+ "Inhaber" : {
33
+ "Name" : "Reich",
34
+ "Vorname" : "Rainer",
35
+ "Geschlecht" : "männlich",
36
+ "Vorlieben" : [ "Reiten", "Schwimmen", "Lesen" ],
37
+ "Alter" : null
38
+ },
39
+ "Deckung" : 2e+6,
40
+ "Währung" : "EURO"
41
+ }
42
+ |
43
+ array "Folder"
44
+ end
45
+ end
46
+
47
+ get 'folder/:folder_id' do
48
+ desc "returns the mail header objects of the requested Folder"
49
+
50
+ request do
51
+ headers "UserID"
52
+ params "FolderID"
53
+ end
54
+
55
+ response do
56
+ object "Folder"
57
+ end
58
+ end
59
+ end
data/example/user.rb ADDED
@@ -0,0 +1,34 @@
1
+ namespace "Account" do
2
+ field("id") { type :integer }
3
+ field("firstname") { example "John" }
4
+ field("lastname") { example "Doe" }
5
+ field("login") { example "john.doe" }
6
+ field("password") { example "secretxxx" }
7
+
8
+ object "User" do
9
+ fields "Account.id", "Account.firstname", "Account.lastname",
10
+ "Account.login", "Account.password"
11
+ example_file :json, "datagram/user.json"
12
+ end
13
+
14
+ interface "UserRest" do
15
+ base_uri "/account"
16
+
17
+ get "users/" do
18
+ response do
19
+ array "Account.User"
20
+ example_file :json, "datagram/user.json"
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ interface "SystemCheck" do
27
+ get "check" do
28
+ desc "returns true if the system is ok otherwise false"
29
+
30
+ response do
31
+ example :text, "OK"
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,10 @@
1
+ class APISpec::Example
2
+ def initialize(format, example)
3
+ @format = format
4
+ @example = example
5
+ end
6
+
7
+ def to_html(message)
8
+ CodeRay.scan(@example, @format).div
9
+ end
10
+ end
@@ -0,0 +1,37 @@
1
+ class APISpec::Field < APISpec::Node
2
+ # default type is String and field is not optional
3
+ def initialize(name, &block)
4
+ @optional = false
5
+ @nullable = false
6
+ @type = :string
7
+ super(name, &block)
8
+ end
9
+
10
+ def type(value)
11
+ @type = value
12
+ end
13
+
14
+ def desc(value)
15
+ @desc = value
16
+ end
17
+
18
+ def default(value)
19
+ @default = value
20
+ end
21
+
22
+ def optional(value)
23
+ @optional = value
24
+ end
25
+
26
+ def nullable(value)
27
+ @nullable = value
28
+ end
29
+
30
+ def example(value)
31
+ @example = value
32
+ end
33
+
34
+ def resolve_references!(root_namespace)
35
+ @type = root_namespace.find_object(@type) if @type.is_a? String
36
+ end
37
+ end
@@ -0,0 +1,104 @@
1
+ require "logger"
2
+ require "fileutils"
3
+
4
+ class APISpec::Generator
5
+ attr_reader :namespace, :logger
6
+
7
+ def initialize(options)
8
+ @options = options
9
+ # logging
10
+ @logger = Logger.new(STDOUT)
11
+ @logger.level = Logger::WARN unless options[:verbose]
12
+ @logger.formatter = proc do |severity, datetime, progname, msg|
13
+ "#{datetime.strftime("%Y-%m-%d %H:%M:%S")} [#{severity}]: #{msg}\n"
14
+ end
15
+ @namespace = APISpec::Namespace.new(nil)
16
+ end
17
+
18
+ # returns the dir with the root based in the working directory
19
+ def path(dir)
20
+ File.join(@options[:workspace], dir)
21
+ end
22
+
23
+ # read all ruby files that can be found in the working directory
24
+ def parse_files!
25
+ @logger.info "parse all files"
26
+ Dir[File.join(@options[:workspace], "**", "*.rb")].each do |path|
27
+ logger.info "read file #{path}..."
28
+ @namespace.read_file(path)
29
+ end
30
+ end
31
+
32
+ # creates the output folder
33
+ def create_output_folder!
34
+ @logger.info "remove and create the output folder"
35
+ FileUtils.rm_rf "#{@options[:output]}"
36
+ FileUtils.mkdir_p @options[:output]
37
+ end
38
+
39
+ # create the index frame
40
+ def create_index!
41
+ @logger.info "create index frame"
42
+ File.open(File.join("#{@options[:output]}", "index.html"), "w") do |file|
43
+ file.write(template(binding, :index))
44
+ end
45
+ end
46
+
47
+ # create the links page (left part of frame)
48
+ def create_links!
49
+ @logger.info "create links page"
50
+ File.open(File.join("#{@options[:output]}", "links.html"), "w") do |file|
51
+ file.write(template(binding, :links))
52
+ end
53
+ end
54
+
55
+ # create the documentation files itself
56
+ def create_objects_and_interfaces!
57
+ @namespace.objects.each do |object|
58
+ create(object)
59
+ end
60
+ @namespace.interfaces.each do |interface|
61
+ create(interface)
62
+ end
63
+ end
64
+
65
+ # create the resources for all subfolders and main folder
66
+ def create_resources!
67
+ Dir[File.join(@options[:output], "**/*")].map do |path|
68
+ File.dirname(File.expand_path(path))
69
+ end.uniq.each do |path|
70
+ @logger.info "copy template resources to #{path}"
71
+ FileUtils.cp Dir[File.join(@options[:resource], "*")], path
72
+ end
73
+ end
74
+
75
+ # start the generator
76
+ def start!
77
+ parse_files!
78
+ create_output_folder!
79
+ create_index!
80
+ create_links!
81
+ create_objects_and_interfaces!
82
+ create_resources!
83
+ end
84
+
85
+ # render a template with the passed object as binding
86
+ def template(object, template_name)
87
+ path = File.join(@options[:template], "#{template_name}.html.erb")
88
+ erb = ERB.new(File.read(path))
89
+ erb.filename = path
90
+ erb.result(object)
91
+ end
92
+
93
+ # create a doc file for the passed node
94
+ def create(node)
95
+ dir = File.dirname(node.to_path)
96
+ file_name = File.basename(node.to_path)
97
+ path = File.join("#{@options[:output]}", dir)
98
+ FileUtils.mkdir_p path
99
+ File.open(File.join(path, file_name), "w") do |file|
100
+ @logger.info "create page for #{node}"
101
+ file.write(node.to_html(self))
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,53 @@
1
+ module APISpec
2
+ HTTP_STATUS_CODES = {
3
+ 100 => "Continue",
4
+ 101 => "Switching Protocols",
5
+ 102 => "Processing",
6
+ 200 => "OK",
7
+ 201 => "Created",
8
+ 202 => "Accepted",
9
+ 203 => "Non-Authoritative Information",
10
+ 204 => "No Content",
11
+ 205 => "Reset Content",
12
+ 206 => "Partial Content",
13
+ 207 => "Multi-Status",
14
+ 226 => "IM Used",
15
+ 300 => "Multiple Choices",
16
+ 301 => "Moved Permanently",
17
+ 302 => "Found",
18
+ 303 => "See Other",
19
+ 304 => "Not Modified",
20
+ 305 => "Use Proxy",
21
+ 307 => "Temporary Redirect",
22
+ 400 => "Bad Request",
23
+ 401 => "Unauthorized",
24
+ 402 => "Payment Required",
25
+ 403 => "Forbidden",
26
+ 404 => "Not Found",
27
+ 405 => "Method Not Allowed",
28
+ 406 => "Not Acceptable",
29
+ 407 => "Proxy Authentication Required",
30
+ 408 => "Request Timeout",
31
+ 409 => "Conflict",
32
+ 410 => "Gone",
33
+ 411 => "Length Required",
34
+ 412 => "Precondition Failed",
35
+ 413 => "Request Entity Too Large",
36
+ 414 => "Request-URI Too Long",
37
+ 415 => "Unsupported Media Type",
38
+ 416 => "Requested Range Not Satisfiable",
39
+ 417 => "Expectation Failed",
40
+ 422 => "Unprocessable Entity",
41
+ 423 => "Locked",
42
+ 424 => "Failed Dependency",
43
+ 426 => "Upgrade Required",
44
+ 500 => "Internal Server Error",
45
+ 501 => "Not Implemented",
46
+ 502 => "Bad Gateway",
47
+ 503 => "Service Unavailable",
48
+ 504 => "Gateway Timeout",
49
+ 505 => "HTTP Version Not Supported",
50
+ 507 => "Insufficient Storage",
51
+ 510 => "Not Extended"
52
+ }
53
+ end
@@ -0,0 +1,29 @@
1
+ class APISpec::Interface < APISpec::Node
2
+ def initialize(name, &block)
3
+ @resources = []
4
+ super(name, &block)
5
+ end
6
+
7
+ def base_uri(value = nil)
8
+ @base_uri ||= value
9
+ end
10
+
11
+ def get(path, &block); http(:get, path, &block); end
12
+ def put(path, &block); http(:put, path, &block); end
13
+ def post(path, &block); http(:post, path, &block); end
14
+ def delete(path, &block); http(:delete, path, &block); end
15
+ def options(path, &block); http(:options, path, &block); end
16
+ def head(path, &block); http(:head, path, &block); end
17
+ def trace(path, &block); http(:trace, path, &block); end
18
+ def connect(path, &block); http(:connect, path, &block); end
19
+
20
+ def http(method, path, &block)
21
+ @resources << APISpec::Resource.new(method, path, &block)
22
+ end
23
+
24
+ def resolve_references!(root_namespace)
25
+ @resources.each do |resource|
26
+ resource.resolve_references!(root_namespace)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,56 @@
1
+ class APISpec::Message
2
+ def initialize(&block)
3
+ @headers = []
4
+ @parameters = []
5
+ instance_eval(&block)
6
+ end
7
+
8
+ def headers(*value)
9
+ @headers = value
10
+ end
11
+
12
+ def params(*value)
13
+ @parameters = value
14
+ end
15
+
16
+ def desc(value)
17
+ @desc = value
18
+ end
19
+
20
+ def object(value)
21
+ @object = value
22
+ end
23
+
24
+ def array(value)
25
+ @array = value
26
+ end
27
+
28
+ def content_desc(value)
29
+ @content_desc = value
30
+ end
31
+
32
+ def example(format, value)
33
+ @example = APISpec::Example.new(format, value)
34
+ end
35
+
36
+ def example_file(format, path)
37
+ @example = [format, path]
38
+ end
39
+
40
+ def resolve_references!(root_namespace)
41
+ @headers = @headers.map { |name| root_namespace.find_field(name) }.flatten
42
+ @parameters = @parameters.map { |name| root_namespace.find_field(name) }.flatten
43
+ @object = root_namespace.find_object(@object) if @object
44
+ @array = root_namespace.find_object(@array) if @array
45
+ end
46
+
47
+ def to_html(generator, resource)
48
+ @generator = generator
49
+ @resource = resource
50
+ if @example.is_a? Array
51
+ format, path = @example
52
+ @example = APISpec::Example.new(format, File.read(@generator.path(path)))
53
+ end
54
+ generator.template(binding, :message)
55
+ end
56
+ end
@@ -0,0 +1,113 @@
1
+ class APISpec::Namespace < APISpec::Node
2
+ class AlreadyDefinedError < StandardError; end
3
+ class ReferenceError < StandardError; end
4
+
5
+ def initialize(name, &block)
6
+ @nodes = {}
7
+ super(name, &block)
8
+ end
9
+
10
+ # reads the passed file
11
+ def read_file(path)
12
+ eval(File.read(path), binding, path)
13
+ end
14
+
15
+ def find_field(path)
16
+ result = find(path)
17
+ if result.is_a? Array
18
+ result.map { |path| find(path) }
19
+ else
20
+ [result]
21
+ end
22
+ end
23
+
24
+ def find_object(path)
25
+ find(path)
26
+ end
27
+
28
+ def find(path)
29
+ path_parts = path.split(".")
30
+ node = self
31
+ while path_parts.any?
32
+ node = node.find_node(path_parts.shift)
33
+ raise ReferenceError.new("#{path} not found in #{self.to_s}") unless node
34
+ end
35
+ node
36
+ end
37
+
38
+ def all(method, type)
39
+ nodes = []
40
+ @nodes.keys.sort.each do |name|
41
+ node = @nodes[name]
42
+ if node.is_a? APISpec::Namespace
43
+ nodes << node.send(method)
44
+ elsif node.is_a? type
45
+ nodes << node
46
+ end
47
+ end
48
+ nodes.flatten
49
+ end
50
+
51
+ def interfaces
52
+ all(:interfaces, APISpec::Interface)
53
+ end
54
+
55
+ def objects
56
+ all(:objects, APISpec::Object)
57
+ end
58
+
59
+ def find_node(name)
60
+ @nodes[name]
61
+ end
62
+
63
+ def to_s
64
+ "#{super} (Nodes: #{@nodes.size})"
65
+ end
66
+
67
+ # print the structure of all namespaces
68
+ def print_tree(indent = 0, lines = [])
69
+ lines << (" " * indent) + self.to_s
70
+ @nodes.keys.sort.each do |reference|
71
+ node = @nodes[reference]
72
+ if node.is_a? APISpec::Namespace
73
+ node.print_tree(indent + 1, lines)
74
+ else
75
+ lines << (" " * (indent + 1)) + "#{reference} => #{node.to_s}"
76
+ end
77
+ end
78
+ lines.join("\n")
79
+ end
80
+
81
+ def namespace(name, &block)
82
+ if node = @nodes[name]
83
+ node.instance_eval &block
84
+ else
85
+ define_node name, APISpec::Namespace.new(name, &block)
86
+ end
87
+ end
88
+
89
+ def define_node(name, node)
90
+ raise AlreadyDefinedError.new("#{name} is already defined!") if @nodes[name]
91
+ @nodes[name] = node
92
+ node.parent = self if node.respond_to? :parent
93
+ end
94
+
95
+ def interface(name, reference = nil, &block)
96
+ reference = reference || name
97
+ define_node name, APISpec::Interface.new(name, &block)
98
+ end
99
+
100
+ def object(name, reference = nil, &block)
101
+ reference = reference || name
102
+ define_node reference, APISpec::Object.new(name, &block)
103
+ end
104
+
105
+ def field(name, reference = nil, &block)
106
+ reference = reference || name
107
+ define_node reference, APISpec::Field.new(name, &block)
108
+ end
109
+
110
+ def field_set(reference = nil, *fields)
111
+ define_node reference, fields
112
+ end
113
+ end