apical 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/.document +5 -0
  2. data/.rspec +1 -0
  3. data/Gemfile +24 -0
  4. data/Gemfile.lock +70 -0
  5. data/LICENSE.txt +20 -0
  6. data/README.rdoc +94 -0
  7. data/Rakefile +55 -0
  8. data/VERSION +1 -0
  9. data/apical.gemspec +122 -0
  10. data/bin/apical +7 -0
  11. data/examples/taco_truck/Gemfile +7 -0
  12. data/examples/taco_truck/Gemfile.lock +50 -0
  13. data/examples/taco_truck/app.rb +13 -0
  14. data/examples/taco_truck/taco_truck.apical +18 -0
  15. data/lib/apical.rb +30 -0
  16. data/lib/apical/adapter.rb +21 -0
  17. data/lib/apical/adapters/http_adapter.rb +30 -0
  18. data/lib/apical/adapters/rack_adapter.rb +16 -0
  19. data/lib/apical/cli.rb +36 -0
  20. data/lib/apical/content_types.rb +62 -0
  21. data/lib/apical/resource.rb +138 -0
  22. data/lib/apical/resource_types.rb +36 -0
  23. data/lib/apical/runner.rb +104 -0
  24. data/lib/apical/writers/console_writer.rb +26 -0
  25. data/lib/apical/writers/html_writer.rb +37 -0
  26. data/spec/apical/adapters/http_adapter_spec.rb +24 -0
  27. data/spec/apical/cli_spec.rb +81 -0
  28. data/spec/apical/content_types_spec.rb +9 -0
  29. data/spec/apical/rack_adapter_spec.rb +12 -0
  30. data/spec/apical_spec.rb +360 -0
  31. data/spec/before_and_after_spec.rb +211 -0
  32. data/spec/fixtures/cli_example_1.rb +7 -0
  33. data/spec/fixtures/cli_example_2.rb +8 -0
  34. data/spec/fixtures/example_require.rb +3 -0
  35. data/spec/fixtures/load_paths_example.apical +1 -0
  36. data/spec/html_writer_spec.rb +117 -0
  37. data/spec/http_apical_spec.rb +23 -0
  38. data/spec/load_paths_spec.rb +12 -0
  39. data/spec/spec_helper.rb +19 -0
  40. data/spec/support/test_apps.rb +34 -0
  41. data/templates/apical_helper.rb +11 -0
  42. data/templates/layout.mustache +180 -0
  43. data/templates/resource.mustache +14 -0
  44. metadata +248 -0
@@ -0,0 +1,13 @@
1
+ require 'sinatra'
2
+ require 'json'
3
+
4
+ get '/tacos.json' do
5
+ [
6
+ { meat: 'chicken', lettuce: true, tomatoes: true }
7
+ ]
8
+ end
9
+
10
+ post '/taco.json' do
11
+ JSON.generate(params)
12
+ end
13
+
@@ -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
+
@@ -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
@@ -0,0 +1,16 @@
1
+ require 'rack/test'
2
+
3
+ module Apical
4
+
5
+ class RackAdapter < Adapter
6
+ identifier :rack
7
+ include Rack::Test::Methods
8
+
9
+ def initialize(options={})
10
+ @app = options[:app]
11
+ end
12
+
13
+ attr_reader :app
14
+ end
15
+
16
+ end
@@ -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