apical 0.1.4
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/.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
|