apical 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +24 -0
- data/Gemfile.lock +70 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +94 -0
- data/Rakefile +55 -0
- data/VERSION +1 -0
- data/apical.gemspec +122 -0
- data/bin/apical +7 -0
- data/examples/taco_truck/Gemfile +7 -0
- data/examples/taco_truck/Gemfile.lock +50 -0
- data/examples/taco_truck/app.rb +13 -0
- data/examples/taco_truck/taco_truck.apical +18 -0
- data/lib/apical.rb +30 -0
- data/lib/apical/adapter.rb +21 -0
- data/lib/apical/adapters/http_adapter.rb +30 -0
- data/lib/apical/adapters/rack_adapter.rb +16 -0
- data/lib/apical/cli.rb +36 -0
- data/lib/apical/content_types.rb +62 -0
- data/lib/apical/resource.rb +138 -0
- data/lib/apical/resource_types.rb +36 -0
- data/lib/apical/runner.rb +104 -0
- data/lib/apical/writers/console_writer.rb +26 -0
- data/lib/apical/writers/html_writer.rb +37 -0
- data/spec/apical/adapters/http_adapter_spec.rb +24 -0
- data/spec/apical/cli_spec.rb +81 -0
- data/spec/apical/content_types_spec.rb +9 -0
- data/spec/apical/rack_adapter_spec.rb +12 -0
- data/spec/apical_spec.rb +360 -0
- data/spec/before_and_after_spec.rb +211 -0
- data/spec/fixtures/cli_example_1.rb +7 -0
- data/spec/fixtures/cli_example_2.rb +8 -0
- data/spec/fixtures/example_require.rb +3 -0
- data/spec/fixtures/load_paths_example.apical +1 -0
- data/spec/html_writer_spec.rb +117 -0
- data/spec/http_apical_spec.rb +23 -0
- data/spec/load_paths_spec.rb +12 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/support/test_apps.rb +34 -0
- data/templates/apical_helper.rb +11 -0
- data/templates/layout.mustache +180 -0
- data/templates/resource.mustache +14 -0
- metadata +248 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'app'
|
2
|
+
|
3
|
+
adapter :rack, app: Sinatra::Application
|
4
|
+
name "Taco Truck"
|
5
|
+
desc "Get some tacos, post some tacos!"
|
6
|
+
|
7
|
+
accept :json
|
8
|
+
content_type :json
|
9
|
+
|
10
|
+
get "/tacos.json" do
|
11
|
+
desc "Get all your tacos"
|
12
|
+
end
|
13
|
+
|
14
|
+
post "/tacos.json" do
|
15
|
+
desc "Make a new taco!"
|
16
|
+
params { { meat: 'beef', lettuce: true } }
|
17
|
+
end
|
18
|
+
|
data/lib/apical.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '.'))
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
require 'mustache'
|
5
|
+
require 'json'
|
6
|
+
require 'kramdown'
|
7
|
+
|
8
|
+
require 'apical/content_types'
|
9
|
+
require 'apical/writers/console_writer'
|
10
|
+
require 'apical/writers/html_writer'
|
11
|
+
require 'apical/adapter'
|
12
|
+
require 'apical/adapters/rack_adapter'
|
13
|
+
require 'apical/adapters/http_adapter'
|
14
|
+
require 'apical/resource'
|
15
|
+
require 'apical/runner'
|
16
|
+
require 'apical/resource'
|
17
|
+
require 'apical/resource_types'
|
18
|
+
require 'apical/cli'
|
19
|
+
|
20
|
+
module Apical
|
21
|
+
VERSION = "0.1.0"
|
22
|
+
|
23
|
+
class << self
|
24
|
+
def new(&block)
|
25
|
+
Runner.new(&block)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Apical
|
2
|
+
|
3
|
+
class Adapter
|
4
|
+
|
5
|
+
def self.[](identifier)
|
6
|
+
@@registry ||= {}
|
7
|
+
klass = @@registry[identifier]
|
8
|
+
raise "Adapter '#{identifier}' does not exist." unless klass
|
9
|
+
|
10
|
+
klass
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.identifier(identifier)
|
14
|
+
@@registry ||= {}
|
15
|
+
@@registry[identifier] = self
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
|
3
|
+
module Apical
|
4
|
+
|
5
|
+
class HttpAdapter < Adapter
|
6
|
+
identifier :http
|
7
|
+
include HTTParty
|
8
|
+
|
9
|
+
def initialize(options={})
|
10
|
+
self.class.base_uri options[:base_uri]
|
11
|
+
@headers = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def header(name, value)
|
15
|
+
@headers[name] = value
|
16
|
+
end
|
17
|
+
|
18
|
+
def clear_headers!
|
19
|
+
@headers = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def method_missing(name, *args)
|
23
|
+
self.class.headers(@headers) unless @headers.empty?
|
24
|
+
response = self.class.send(name, *args)
|
25
|
+
clear_headers!
|
26
|
+
return response
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
data/lib/apical/cli.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
module Apical
|
2
|
+
class CLI < Thor
|
3
|
+
map '-v' => :version
|
4
|
+
|
5
|
+
desc "version", "print version information"
|
6
|
+
def version
|
7
|
+
puts Apical::VERSION
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "compile", "compile the given input files"
|
11
|
+
method_option :output_path, aliases: '-o', desc: "Path to output file"
|
12
|
+
method_option :format, aliases: '-f', desc: "Format (either 'html' or 'text')"
|
13
|
+
def compile(*files)
|
14
|
+
files.each do |file|
|
15
|
+
$:.unshift File.dirname(file)
|
16
|
+
block = eval("Proc.new { #{File.read(file)} }")
|
17
|
+
doc = Apical.new(&block)
|
18
|
+
|
19
|
+
output = if options[:output_path]
|
20
|
+
File.open(options[:output_path], 'w')
|
21
|
+
else
|
22
|
+
$stdout
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
case options[:format]
|
27
|
+
when 'html'
|
28
|
+
HtmlWriter.new(doc).write(output)
|
29
|
+
else
|
30
|
+
ConsoleWriter.new(doc).write(output)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Apical
|
2
|
+
|
3
|
+
class ContentType
|
4
|
+
|
5
|
+
def self.[](format)
|
6
|
+
@@registry ||= {}
|
7
|
+
klass = @@registry[format]
|
8
|
+
raise "Content type '#{format}' does not exist." unless klass
|
9
|
+
|
10
|
+
klass.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.format(format)
|
14
|
+
@format = format
|
15
|
+
@@registry ||= {}
|
16
|
+
@@registry[format] = self
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
class JsonContentType < ContentType
|
22
|
+
format :json
|
23
|
+
|
24
|
+
def header
|
25
|
+
'application/json'
|
26
|
+
end
|
27
|
+
|
28
|
+
def generate(obj)
|
29
|
+
JSON.pretty_generate(obj)
|
30
|
+
end
|
31
|
+
|
32
|
+
def parse(str)
|
33
|
+
JSON.parse(str)
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_s
|
37
|
+
'json'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# TODO: Actually encode/decode urlencoded params
|
42
|
+
class FormContentType < ContentType
|
43
|
+
format :form
|
44
|
+
|
45
|
+
def header
|
46
|
+
'application/x-www-form-urlencoded'
|
47
|
+
end
|
48
|
+
|
49
|
+
def generate(str)
|
50
|
+
str
|
51
|
+
end
|
52
|
+
|
53
|
+
def parse(obj)
|
54
|
+
obj
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_s
|
58
|
+
'form'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
@@ -0,0 +1,138 @@
|
|
1
|
+
module Apical
|
2
|
+
class Resource
|
3
|
+
extend Forwardable
|
4
|
+
|
5
|
+
def initialize(runner, path, &block)
|
6
|
+
@runner = runner
|
7
|
+
@path = path
|
8
|
+
@params = nil
|
9
|
+
@before_blocks = []
|
10
|
+
@after_blocks = []
|
11
|
+
@custom_headers = {}
|
12
|
+
Blueprint.new self, &block
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_accessor :path, :method, :params, :desc, :before_blocks, :after_blocks, :custom_headers
|
16
|
+
attr_writer :accept, :content_type
|
17
|
+
|
18
|
+
def_delegator :@runner, :adapter
|
19
|
+
def_delegators :adapter, :get, :put, :post, :delete, :options, :header
|
20
|
+
|
21
|
+
class Blueprint
|
22
|
+
def initialize(resource, &block)
|
23
|
+
@resource = resource
|
24
|
+
instance_eval &block
|
25
|
+
end
|
26
|
+
|
27
|
+
def desc(val)
|
28
|
+
@resource.desc = val
|
29
|
+
end
|
30
|
+
|
31
|
+
def accept(format)
|
32
|
+
@resource.accept = ContentType[format]
|
33
|
+
end
|
34
|
+
|
35
|
+
def content_type(format)
|
36
|
+
@resource.content_type = ContentType[format]
|
37
|
+
end
|
38
|
+
|
39
|
+
def header(name, value)
|
40
|
+
@resource.custom_headers[name] = value
|
41
|
+
end
|
42
|
+
|
43
|
+
def auth_header(token)
|
44
|
+
header 'Authorization', token
|
45
|
+
end
|
46
|
+
|
47
|
+
def params(&block)
|
48
|
+
@resource.params = block
|
49
|
+
end
|
50
|
+
|
51
|
+
def before(&block)
|
52
|
+
@resource.before_blocks << block
|
53
|
+
end
|
54
|
+
|
55
|
+
def after(&block)
|
56
|
+
@resource.after_blocks << block
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def content_type
|
61
|
+
@content_type || @runner.content_type
|
62
|
+
end
|
63
|
+
|
64
|
+
def accept
|
65
|
+
@accept || @runner.accept
|
66
|
+
end
|
67
|
+
|
68
|
+
def auth_header
|
69
|
+
@auth_header || @runner.auth_header
|
70
|
+
end
|
71
|
+
|
72
|
+
def content_type_header
|
73
|
+
content_type.header
|
74
|
+
end
|
75
|
+
|
76
|
+
def accept_header
|
77
|
+
accept.header
|
78
|
+
end
|
79
|
+
|
80
|
+
def run
|
81
|
+
@before_blocks.each {|block| instance_eval &block }
|
82
|
+
@params = instance_eval(&@params) if @params.is_a?(Proc)
|
83
|
+
@response = make_request
|
84
|
+
@after_blocks.each {|block| instance_eval &block }
|
85
|
+
end
|
86
|
+
|
87
|
+
def filtered_params
|
88
|
+
params.reject {|k, v| path_captures.include?(k) or path_captures.include?(k.to_s)}
|
89
|
+
end
|
90
|
+
|
91
|
+
def formatted_params
|
92
|
+
return {} unless params
|
93
|
+
return filtered_params unless accept
|
94
|
+
accept.generate(filtered_params)
|
95
|
+
end
|
96
|
+
|
97
|
+
def formatted_response
|
98
|
+
return response_body unless content_type
|
99
|
+
content_type.generate(content_type.parse(response_body))
|
100
|
+
end
|
101
|
+
|
102
|
+
def response_body
|
103
|
+
@response.body
|
104
|
+
end
|
105
|
+
|
106
|
+
PATH_CAPTURE_REGEXP = /:[^\.\/]+/
|
107
|
+
|
108
|
+
def path_captures
|
109
|
+
@path.match(PATH_CAPTURE_REGEXP).to_a.map {|m| m.gsub(':', '').intern }
|
110
|
+
end
|
111
|
+
|
112
|
+
def interpolated_path
|
113
|
+
@path.gsub(PATH_CAPTURE_REGEXP) do |match|
|
114
|
+
params[match.gsub(':', '').intern]
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def call_if_proc(proc_or_obj)
|
119
|
+
proc_or_obj.is_a?(Proc) ? instance_eval(&proc_or_obj) : proc_or_obj
|
120
|
+
end
|
121
|
+
|
122
|
+
def custom_header(name)
|
123
|
+
call_if_proc @custom_headers[name]
|
124
|
+
end
|
125
|
+
|
126
|
+
def make_request
|
127
|
+
@custom_headers.each do |name, value|
|
128
|
+
adapter.header name, custom_header(name)
|
129
|
+
end
|
130
|
+
|
131
|
+
adapter.header "Accept", accept_header if accept_header
|
132
|
+
adapter.header "Content-Type", content_type_header if content_type_header
|
133
|
+
|
134
|
+
adapter.send(@method.downcase.intern, interpolated_path, formatted_params)
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Apical
|
2
|
+
class PostResource < Resource
|
3
|
+
def initialize(runner, path, &block)
|
4
|
+
super runner, path, &block
|
5
|
+
@method = 'POST'
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class GetResource < Resource
|
10
|
+
def initialize(runner, path, &block)
|
11
|
+
super runner, path, &block
|
12
|
+
@method = 'GET'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class PutResource < Resource
|
17
|
+
def initialize(runner, path, &block)
|
18
|
+
super runner, path, &block
|
19
|
+
@method = 'PUT'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class DeleteResource < Resource
|
24
|
+
def initialize(runner, path, &block)
|
25
|
+
super runner, path, &block
|
26
|
+
@method = 'DELETE'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class OptionsResource < Resource
|
31
|
+
def initialize(runner, path, &block)
|
32
|
+
super runner, path, &block
|
33
|
+
@method = 'OPTIONS'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module Apical
|
2
|
+
class Runner
|
3
|
+
def initialize(&block)
|
4
|
+
@resources = []
|
5
|
+
@name = "apical"
|
6
|
+
@desc = ""
|
7
|
+
@accept = ContentType[:form]
|
8
|
+
@content_type = ContentType[:form]
|
9
|
+
@before_each_blocks = []
|
10
|
+
@before_all_blocks = []
|
11
|
+
@after_each_blocks = []
|
12
|
+
@after_all_blocks = []
|
13
|
+
Blueprint.new self, &block
|
14
|
+
end
|
15
|
+
|
16
|
+
include Rack::Test::Methods
|
17
|
+
|
18
|
+
attr_accessor :resources, :accept, :name, :desc, :app, :content_type, :auth_header, :before_each_blocks, :before_all_blocks, :after_each_blocks, :after_all_blocks, :adapter
|
19
|
+
|
20
|
+
class Blueprint
|
21
|
+
def initialize(runner, &block)
|
22
|
+
@runner = runner
|
23
|
+
instance_eval &block
|
24
|
+
end
|
25
|
+
|
26
|
+
def adapter(identifier, options={})
|
27
|
+
@runner.adapter = Adapter[identifier].new(options)
|
28
|
+
end
|
29
|
+
|
30
|
+
def accept(format)
|
31
|
+
@runner.accept = ContentType[format]
|
32
|
+
end
|
33
|
+
|
34
|
+
def content_type(format)
|
35
|
+
@runner.content_type = ContentType[format]
|
36
|
+
end
|
37
|
+
|
38
|
+
def auth_header(token)
|
39
|
+
@runner.auth_header = token
|
40
|
+
end
|
41
|
+
|
42
|
+
def name(name)
|
43
|
+
@runner.name = name
|
44
|
+
end
|
45
|
+
|
46
|
+
def desc(desc)
|
47
|
+
@runner.desc = desc
|
48
|
+
end
|
49
|
+
|
50
|
+
def app(app)
|
51
|
+
@runner.app = app
|
52
|
+
end
|
53
|
+
|
54
|
+
def before(type=:each, &block)
|
55
|
+
case type
|
56
|
+
when :each
|
57
|
+
@runner.before_each_blocks << block
|
58
|
+
when :all
|
59
|
+
@runner.before_all_blocks << block
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def after(type=:each, &block)
|
64
|
+
case type
|
65
|
+
when :each
|
66
|
+
@runner.after_each_blocks << block
|
67
|
+
when :all
|
68
|
+
@runner.after_all_blocks << block
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def get(path, &block)
|
73
|
+
@runner.resources << GetResource.new(@runner, path, &block)
|
74
|
+
end
|
75
|
+
|
76
|
+
def post(path, &block)
|
77
|
+
@runner.resources << PostResource.new(@runner, path, &block)
|
78
|
+
end
|
79
|
+
|
80
|
+
def put(path, &block)
|
81
|
+
@runner.resources << PutResource.new(@runner, path, &block)
|
82
|
+
end
|
83
|
+
|
84
|
+
def delete(path, &block)
|
85
|
+
@runner.resources << DeleteResource.new(@runner, path, &block)
|
86
|
+
end
|
87
|
+
|
88
|
+
def options(path, &block)
|
89
|
+
@runner.resources << OptionsResource.new(@runner, path, &block)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def run
|
94
|
+
@before_all_blocks.each {|block| instance_eval &block }
|
95
|
+
@resources.each do |resource|
|
96
|
+
@before_each_blocks.each {|block| resource.instance_eval &block }
|
97
|
+
resource.run
|
98
|
+
@after_each_blocks.each {|block| resource.instance_eval &block }
|
99
|
+
end
|
100
|
+
@after_all_blocks.each {|block| instance_eval &block }
|
101
|
+
self
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|