patchboard 0.4.0
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.
- 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: []
|