patchboard 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +1 -0
- data/lib/patchboard/action.rb +107 -0
- data/lib/patchboard/api.rb +141 -0
- data/lib/patchboard/endpoints.rb +49 -0
- data/lib/patchboard/resource.rb +111 -0
- data/lib/patchboard/response.rb +29 -0
- data/lib/patchboard/schema_manager.rb +50 -0
- data/lib/patchboard/util.rb +12 -0
- data/lib/patchboard.rb +130 -0
- metadata +109 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 96e98fdd770b6bfa97c132fc1f7c9edfc01937c2
|
4
|
+
data.tar.gz: 4f854ad38cafa53c9775a73d668dfc20d7072e69
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ac38663214aa9bb712080af92da4340a42128da75798b4408369d8dff343cd6b7e7896a80b4474705e9b16ce75a6f8c24a5e5643a1662dab493874c60a34c507
|
7
|
+
data.tar.gz: 82e03045526bcc3946fd9f5bbcdd66740f22a7f04a5fe462068df2124e5e91cc80fbca842e4fbc7b10db2fea70963b6776e8fe8a7068ca1a96b982fc3ecda456
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2014 Matthew King
|
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
|
+
# A Ruby client for Patchboard APIs
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require_relative "response"
|
2
|
+
|
3
|
+
class Patchboard
|
4
|
+
|
5
|
+
class Action
|
6
|
+
|
7
|
+
attr_reader :method, :headers, :status
|
8
|
+
|
9
|
+
def initialize(patchboard, name, definition)
|
10
|
+
@name = name
|
11
|
+
@patchboard = patchboard
|
12
|
+
@api = @patchboard.api
|
13
|
+
@schema_manager = @patchboard.schema_manager
|
14
|
+
@method = definition[:method]
|
15
|
+
|
16
|
+
|
17
|
+
@headers = {
|
18
|
+
}
|
19
|
+
|
20
|
+
request, response = definition[:request], definition[:response]
|
21
|
+
|
22
|
+
if request
|
23
|
+
@auth_scheme = request[:authorization]
|
24
|
+
if request[:type]
|
25
|
+
@headers["Content-Type"] = request[:type]
|
26
|
+
@request_schema = @schema_manager.find :media_type => request[:type]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
if response && response[:type]
|
31
|
+
@headers["Accept"] = response[:type]
|
32
|
+
@response_schema = @schema_manager.find :media_type => response[:type]
|
33
|
+
end
|
34
|
+
@status = response[:status] || 200
|
35
|
+
end
|
36
|
+
|
37
|
+
def http
|
38
|
+
@http ||= @patchboard.http
|
39
|
+
end
|
40
|
+
|
41
|
+
def request(resource, url, *args)
|
42
|
+
options = self.prepare_request(resource, url, *args)
|
43
|
+
raw = self.http.request @method, url, options.merge(:response => :object)
|
44
|
+
response = Response.new(raw)
|
45
|
+
if response.status != @status
|
46
|
+
raise "Unexpected response status: #{response.status}"
|
47
|
+
end
|
48
|
+
out = @api.decorate(resource.context, @response_schema, response.data)
|
49
|
+
out.response = response
|
50
|
+
out
|
51
|
+
end
|
52
|
+
|
53
|
+
def prepare_request(resource, url, *args)
|
54
|
+
context = resource.context
|
55
|
+
headers = {}.merge(@headers)
|
56
|
+
options = {
|
57
|
+
:url => url, :method => @method, :headers => headers
|
58
|
+
}
|
59
|
+
|
60
|
+
if @auth_scheme && context.respond_to?(:authorizer)
|
61
|
+
credential = context.authorizer(@auth_scheme, resource, @name)
|
62
|
+
headers["Authorization"] = "#{@auth_scheme} #{credential}"
|
63
|
+
end
|
64
|
+
|
65
|
+
input_options = self.process_args(args)
|
66
|
+
if input_options[:body]
|
67
|
+
options[:body] = input_options[:body]
|
68
|
+
end
|
69
|
+
# This code looks forward to the time when we have figured out
|
70
|
+
# how we want Patchboard clients to take extra arguments for
|
71
|
+
# requests. Leaving it here now to show why process_args returns a Hash,
|
72
|
+
# not just the body.
|
73
|
+
if input_options[:headers]
|
74
|
+
options[:headers].merge!(input_options[:headers])
|
75
|
+
end
|
76
|
+
options
|
77
|
+
end
|
78
|
+
|
79
|
+
def process_args(args)
|
80
|
+
options = {}
|
81
|
+
signature = args.map {|arg| arg.class.to_s }.join(".")
|
82
|
+
if @request_schema
|
83
|
+
case signature
|
84
|
+
when "String"
|
85
|
+
options[:body] = args[0]
|
86
|
+
when "Hash", "Array"
|
87
|
+
options[:body] = args[0].to_json
|
88
|
+
else
|
89
|
+
raise "Invalid arguments for action: request content is required"
|
90
|
+
end
|
91
|
+
else
|
92
|
+
case signature
|
93
|
+
when ""
|
94
|
+
else
|
95
|
+
raise "Invalid arguments for action"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
options
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
@@ -0,0 +1,141 @@
|
|
1
|
+
|
2
|
+
class Patchboard
|
3
|
+
|
4
|
+
class API
|
5
|
+
attr_reader :mappings, :resources, :schemas, :service_url
|
6
|
+
|
7
|
+
def initialize(definition)
|
8
|
+
|
9
|
+
@service_url = definition[:service_url]
|
10
|
+
@resources = Hashie::Mash.new definition[:resources]
|
11
|
+
@resources.each do |name, definition|
|
12
|
+
definition.name = name
|
13
|
+
end
|
14
|
+
@schemas = definition[:schemas]
|
15
|
+
|
16
|
+
@mappings = {}
|
17
|
+
definition[:mappings].each do |name, mapping|
|
18
|
+
@mappings[name] = Mapping.new(self, name, mapping)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def find_mapping(schema)
|
23
|
+
# TODO: stitch this into the actual schemas.
|
24
|
+
# ex: schema.mapping
|
25
|
+
if id = (schema[:id] || schema[:$ref])
|
26
|
+
name = id.split("#").last
|
27
|
+
@mappings[name.to_sym]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def decorate(context, schema, data)
|
32
|
+
unless schema
|
33
|
+
return Hashie::Mash.new(data)
|
34
|
+
end
|
35
|
+
|
36
|
+
if mapping = self.find_mapping(schema)
|
37
|
+
# when we have a resource class, instantiate it using the input data.
|
38
|
+
data = mapping.klass.new context, data
|
39
|
+
else
|
40
|
+
# Otherwise traverse the schema in search of subschemas that have
|
41
|
+
# resource classes available.
|
42
|
+
case schema[:type]
|
43
|
+
when "array"
|
44
|
+
# TODO: handle the case where schema.items is an array, which
|
45
|
+
# signifies a tuple. schema.additionalItems then becomes important.
|
46
|
+
data.map! do |item|
|
47
|
+
self.decorate(context, schema[:items], item)
|
48
|
+
end
|
49
|
+
|
50
|
+
when "object"
|
51
|
+
if schema[:properties]
|
52
|
+
schema[:properties].each do |key, prop_schema|
|
53
|
+
if value = data[key]
|
54
|
+
data[key] = self.decorate(context, prop_schema, value)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
# TODO: handle schema.patternProperties
|
59
|
+
# TODO: consider alternative to iterating over all keys.
|
60
|
+
if schema[:additionalProperties]
|
61
|
+
data.each do |key, value|
|
62
|
+
next if schema[:properties] && schema[:properties][key]
|
63
|
+
data[key] = self.decorate(context, schema[:additionalProperties], value)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
data = Hashie::Mash.new data
|
67
|
+
end
|
68
|
+
end
|
69
|
+
data
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
class Mapping
|
76
|
+
|
77
|
+
attr_accessor :klass
|
78
|
+
attr_reader :name, :resource, :url, :path, :template, :query
|
79
|
+
|
80
|
+
def initialize(api, name, definition)
|
81
|
+
@api = api
|
82
|
+
@name = name
|
83
|
+
@definition = definition
|
84
|
+
@resource = @definition[:resource]
|
85
|
+
@query = @definition[:query]
|
86
|
+
@url = @definition[:url]
|
87
|
+
@path = @definition[:path]
|
88
|
+
@template = @definition[:template]
|
89
|
+
|
90
|
+
unless (resource_name = @definition[:resource])
|
91
|
+
raise "Mapping does not specify 'resource'"
|
92
|
+
end
|
93
|
+
|
94
|
+
unless (@resource = @api.resources[resource_name.to_sym])
|
95
|
+
raise "Mapping specifies a resource that is not defined"
|
96
|
+
end
|
97
|
+
|
98
|
+
unless (@definition[:url] || @definition[:path] || @definition[:template])
|
99
|
+
raise "Mapping is missing any form of URL specification"
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
def generate_url(params={})
|
105
|
+
if @url
|
106
|
+
base = @url
|
107
|
+
elsif params[:url]
|
108
|
+
base = params[:url]
|
109
|
+
elsif @path
|
110
|
+
if @api.service_url
|
111
|
+
base = [@api.service_url, @path].join("/")
|
112
|
+
else
|
113
|
+
raise "Tried to generate url from path, but API did not define service_url"
|
114
|
+
end
|
115
|
+
elsif @template
|
116
|
+
raise "Template mappings are not yet implemented in the client"
|
117
|
+
end
|
118
|
+
|
119
|
+
if @query
|
120
|
+
parts = []
|
121
|
+
keys = @query.keys.sort()
|
122
|
+
# TODO check query schema
|
123
|
+
keys.each do |key|
|
124
|
+
if string = (params[key.to_s] || params[key.to_sym])
|
125
|
+
parts << "#{URI.escape(key.to_s)}=#{URI.escape(string)}"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
if parts.size > 0
|
129
|
+
query_string = "?#{parts.join("&")}"
|
130
|
+
else
|
131
|
+
query_string = ""
|
132
|
+
end
|
133
|
+
[base, query_string].join()
|
134
|
+
else
|
135
|
+
base
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class Patchboard
|
2
|
+
|
3
|
+
class Endpoints
|
4
|
+
|
5
|
+
def initialize(context, api, klasses)
|
6
|
+
@context = context
|
7
|
+
@api = api
|
8
|
+
@klasses = klasses
|
9
|
+
|
10
|
+
@api.mappings.each do |name, mapping|
|
11
|
+
if klass = @klasses[name]
|
12
|
+
if mapping.template || mapping.query
|
13
|
+
# A mapping with a template or query property requires
|
14
|
+
# additional input before it can express a usable URL.
|
15
|
+
# Thus the endpoint method takes parameters and instantiates
|
16
|
+
# a resource of the correct class.
|
17
|
+
|
18
|
+
define_singleton_method name do |params={}|
|
19
|
+
if params.is_a? String
|
20
|
+
url = params
|
21
|
+
else
|
22
|
+
url = mapping.generate_url(params)
|
23
|
+
end
|
24
|
+
klass.new(context, {:url => url})
|
25
|
+
end
|
26
|
+
elsif mapping.path
|
27
|
+
# When a mapping has the 'path' property, all that is needed to
|
28
|
+
# create a usable resource is the full URL. Thus this endpoint
|
29
|
+
# method returns an instantiated resource directly.
|
30
|
+
define_singleton_method name do
|
31
|
+
klass.new(context, :url => mapping.generate_url())
|
32
|
+
end
|
33
|
+
elsif mapping.url
|
34
|
+
define_singleton_method name do
|
35
|
+
klass.new(context, :url => mapping.url)
|
36
|
+
end
|
37
|
+
else
|
38
|
+
raise "Mapping '#{name}' is invalid"
|
39
|
+
end
|
40
|
+
else
|
41
|
+
raise "No resource class for mapping '#{name}'"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
|
@@ -0,0 +1,111 @@
|
|
1
|
+
|
2
|
+
class Patchboard
|
3
|
+
|
4
|
+
class Resource
|
5
|
+
|
6
|
+
def self.assemble(patchboard, definition, schema, mapping)
|
7
|
+
|
8
|
+
define_singleton_method(:api) do
|
9
|
+
patchboard.api
|
10
|
+
end
|
11
|
+
|
12
|
+
define_singleton_method(:mapping) do
|
13
|
+
mapping
|
14
|
+
end
|
15
|
+
|
16
|
+
define_singleton_method(:schema) do
|
17
|
+
schema
|
18
|
+
end
|
19
|
+
|
20
|
+
if schema && schema[:properties]
|
21
|
+
schema[:properties].each do |name, definition|
|
22
|
+
define_method name do
|
23
|
+
@attributes[name]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
if schema && schema[:additionalProperties] != false
|
29
|
+
define_method :method_missing do |name, *args, &block|
|
30
|
+
if args.size == 0
|
31
|
+
@attributes[name.to_sym]
|
32
|
+
else
|
33
|
+
super(name, *args, &block)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
define_singleton_method :generate_url do |params|
|
39
|
+
mapping.generate_url(params)
|
40
|
+
end
|
41
|
+
|
42
|
+
definition[:actions].each do |name, action|
|
43
|
+
action = Action.new(patchboard, name, action)
|
44
|
+
|
45
|
+
define_method name do |*args|
|
46
|
+
action.request self, @url, *args
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.decorate(instance, attributes)
|
52
|
+
# TODO: non destructive decoration
|
53
|
+
# TODO: add some sort of validation for the input attributes.
|
54
|
+
# Hey, we have a JSON Schema, why not use it?
|
55
|
+
if self.schema && (properties = self.schema[:properties])
|
56
|
+
context = instance.context
|
57
|
+
properties.each do |key, sub_schema|
|
58
|
+
next unless (value = attributes[key])
|
59
|
+
|
60
|
+
if mapping = self.api.find_mapping(sub_schema)
|
61
|
+
if mapping.query
|
62
|
+
# TODO: find a way to define this at runtime, not once
|
63
|
+
# for every instance.
|
64
|
+
instance.define_singleton_method key do |params|
|
65
|
+
params[:url] = value[:url]
|
66
|
+
url = mapping.generate_url(params)
|
67
|
+
mapping.klass.new context, :url => url
|
68
|
+
end
|
69
|
+
else
|
70
|
+
attributes[key] = mapping.klass.new context, value
|
71
|
+
end
|
72
|
+
else
|
73
|
+
attributes[key] = self.api.decorate(context, sub_schema, value)
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
78
|
+
attributes
|
79
|
+
end
|
80
|
+
|
81
|
+
attr_accessor :response
|
82
|
+
attr_reader :url, :context, :attributes
|
83
|
+
|
84
|
+
def initialize(context, attributes={})
|
85
|
+
@context = context
|
86
|
+
@attributes = self.class.decorate self, Hashie::Mash.new(attributes)
|
87
|
+
@url = @attributes[:url]
|
88
|
+
end
|
89
|
+
|
90
|
+
def inspect
|
91
|
+
id = "%x" % (self.object_id << 1)
|
92
|
+
%Q{
|
93
|
+
#<#{self.class}:0x#{id}
|
94
|
+
@url="#{@url}" @context=#{@context}>
|
95
|
+
}.strip
|
96
|
+
end
|
97
|
+
|
98
|
+
def [](key)
|
99
|
+
@attributes[key]
|
100
|
+
end
|
101
|
+
|
102
|
+
def []=(key, value)
|
103
|
+
@attributes[key] = value
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
def curl
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class Patchboard
|
2
|
+
class Response
|
3
|
+
|
4
|
+
attr_accessor :resource
|
5
|
+
attr_reader :raw, :data
|
6
|
+
def initialize(raw)
|
7
|
+
@raw = raw
|
8
|
+
if @raw.headers["Content-Type"]
|
9
|
+
if @raw.headers["Content-Type"] =~ %r{application/.*json}
|
10
|
+
@data = JSON.parse @raw.body, :symbolize_names => true
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def method_missing(name, *args, &block)
|
16
|
+
if @raw.respond_to? name
|
17
|
+
@raw.send(name, *args, &block)
|
18
|
+
else
|
19
|
+
super
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def respond_to?(*args)
|
24
|
+
@raw.respond_to?(*args) || super
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require "pp"
|
2
|
+
class Patchboard
|
3
|
+
|
4
|
+
class SchemaManager
|
5
|
+
|
6
|
+
def initialize(schemas)
|
7
|
+
@schemas = schemas
|
8
|
+
@media_types = {}
|
9
|
+
@ids = {}
|
10
|
+
@names = {}
|
11
|
+
|
12
|
+
@schemas.each do |schema|
|
13
|
+
# TODO error checking for missing id
|
14
|
+
base_id = schema[:id].chomp("#")
|
15
|
+
schema[:definitions].each do |name, definition|
|
16
|
+
# `definitions` is the conventional place to put schemas,
|
17
|
+
# so we'll define fragment IDs by default where they are
|
18
|
+
# not explicitly specified.
|
19
|
+
id = definition[:id] || [base_id, name].join("#")
|
20
|
+
self.register_schema(id, definition)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def register_schema(id, schema)
|
26
|
+
schema[:id] = id
|
27
|
+
@ids[id] = schema
|
28
|
+
name = id.split("#")[1].to_sym
|
29
|
+
@names[name] = schema
|
30
|
+
if type = schema[:mediaType]
|
31
|
+
@media_types[type] = schema
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def find(options)
|
36
|
+
if type = options[:mediaType] || options[:media_type]
|
37
|
+
@media_types[type]
|
38
|
+
elsif ref = options[:ref]
|
39
|
+
@ids[ref]
|
40
|
+
elsif name = options[:name]
|
41
|
+
@names[name]
|
42
|
+
else
|
43
|
+
raise "Unusable argument to find: #{options}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
data/lib/patchboard.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
# Allows you to determine which methods are defined on a specific class
|
2
|
+
# or module
|
3
|
+
class Module
|
4
|
+
def local_methods
|
5
|
+
self.methods.select { |m| self.method(m).owner == self }
|
6
|
+
end
|
7
|
+
|
8
|
+
def local_instance_methods
|
9
|
+
self.instance_methods.select { |m| self.instance_method(m).owner == self }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
gem "http"
|
14
|
+
gem "json"
|
15
|
+
gem "hashie"
|
16
|
+
|
17
|
+
require "http"
|
18
|
+
require "json"
|
19
|
+
require "hashie"
|
20
|
+
|
21
|
+
require_relative "patchboard/api"
|
22
|
+
require_relative "patchboard/util"
|
23
|
+
require_relative "patchboard/resource"
|
24
|
+
require_relative "patchboard/endpoints"
|
25
|
+
require_relative "patchboard/action"
|
26
|
+
require_relative "patchboard/schema_manager"
|
27
|
+
|
28
|
+
class Patchboard
|
29
|
+
|
30
|
+
module Resources
|
31
|
+
# This module exists to provide a default namespace for the classes
|
32
|
+
# generated when reflecting on the API. If Patchboard is instantiated
|
33
|
+
# with the option :namespace => SomeModule, that module will be used
|
34
|
+
# instead.
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
def self.http
|
39
|
+
@http ||= HTTP.with_headers "User-Agent" => "patchboard-rb v0.1.0"
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.discover(url, options={}, &block)
|
43
|
+
begin
|
44
|
+
response = self.http.request "GET", url,
|
45
|
+
:response => :object,
|
46
|
+
:headers => {
|
47
|
+
"Accept" => "application/json"
|
48
|
+
}
|
49
|
+
data = JSON.parse(response.body, :symbolize_names => true)
|
50
|
+
self.new(data, options, &block)
|
51
|
+
rescue JSON::ParserError => error
|
52
|
+
raise "Unparseable API description: #{error}"
|
53
|
+
rescue Errno::ECONNREFUSED => error
|
54
|
+
raise "Problem discovering API: #{error}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
attr_reader :api, :resources, :http, :schema_manager, :context
|
59
|
+
|
60
|
+
def initialize(api, options={}, &block)
|
61
|
+
@api = API.new(api)
|
62
|
+
@options = options
|
63
|
+
@context_creator = block
|
64
|
+
|
65
|
+
if options[:namespace]
|
66
|
+
if options[:namespace].is_a? Module
|
67
|
+
@namespace = options[:namespace]
|
68
|
+
else
|
69
|
+
raise "Namespace must be a Module"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
@endpoint_classes = {}
|
74
|
+
|
75
|
+
@schema_manager = SchemaManager.new(@api.schemas)
|
76
|
+
|
77
|
+
@http = self.class.http
|
78
|
+
self.create_classes()
|
79
|
+
client = self.spawn({})
|
80
|
+
@resources = client.resources
|
81
|
+
@context = client.context
|
82
|
+
end
|
83
|
+
|
84
|
+
def spawn(context=nil)
|
85
|
+
context ||= @context_creator.call
|
86
|
+
self.class::Client.new(context, @api, @endpoint_classes)
|
87
|
+
end
|
88
|
+
|
89
|
+
class Client
|
90
|
+
|
91
|
+
attr_reader :resources, :context
|
92
|
+
def initialize(context, api, klasses)
|
93
|
+
@context = context
|
94
|
+
@resources = Endpoints.new @context, api, klasses
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
def create_classes
|
100
|
+
klasses = {}
|
101
|
+
@api.mappings.each do |name, mapping|
|
102
|
+
resource_name = mapping.resource.name.to_sym
|
103
|
+
schema = @schema_manager.find :name => resource_name
|
104
|
+
|
105
|
+
klass = klasses[name] ||= begin
|
106
|
+
resource_def = mapping.resource
|
107
|
+
self.create_class(name, resource_def, schema, mapping)
|
108
|
+
end
|
109
|
+
@endpoint_classes[name] = klass
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def create_class(resource_name, definition, schema, mapping)
|
114
|
+
patchboard = self
|
115
|
+
|
116
|
+
mapping.klass = klass = Class.new(self.class::Resource) do |klass|
|
117
|
+
self.assemble(patchboard, definition, schema, mapping)
|
118
|
+
end
|
119
|
+
|
120
|
+
if @namespace
|
121
|
+
@namespace.const_set Util.camel_case(resource_name).to_sym, klass
|
122
|
+
else
|
123
|
+
Patchboard::Resources.const_set Util.camel_case(resource_name).to_sym, klass
|
124
|
+
end
|
125
|
+
klass
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
end
|
130
|
+
|
metadata
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: patchboard
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matthew King
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-03-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: json
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.8.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.8.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: http
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.5.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.5.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: hashie
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 2.0.5
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.0.5
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest-reporters
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.0.2
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.0.2
|
69
|
+
description:
|
70
|
+
email: automatthew@gmail.com
|
71
|
+
executables: []
|
72
|
+
extensions: []
|
73
|
+
extra_rdoc_files: []
|
74
|
+
files:
|
75
|
+
- LICENSE
|
76
|
+
- README.md
|
77
|
+
- lib/patchboard.rb
|
78
|
+
- lib/patchboard/action.rb
|
79
|
+
- lib/patchboard/api.rb
|
80
|
+
- lib/patchboard/endpoints.rb
|
81
|
+
- lib/patchboard/resource.rb
|
82
|
+
- lib/patchboard/response.rb
|
83
|
+
- lib/patchboard/schema_manager.rb
|
84
|
+
- lib/patchboard/util.rb
|
85
|
+
homepage: https://github.com/pandastrike/patchboard-rb
|
86
|
+
licenses:
|
87
|
+
- MIT
|
88
|
+
metadata: {}
|
89
|
+
post_install_message:
|
90
|
+
rdoc_options: []
|
91
|
+
require_paths:
|
92
|
+
- lib
|
93
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
requirements: []
|
104
|
+
rubyforge_project:
|
105
|
+
rubygems_version: 2.2.0
|
106
|
+
signing_key:
|
107
|
+
specification_version: 4
|
108
|
+
summary: Ruby client for Patchboard APIs
|
109
|
+
test_files: []
|