apispec 0.0.3

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/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