frenchy 0.0.1
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/.gitignore +17 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +128 -0
- data/Rakefile +1 -0
- data/frenchie.gemspec +29 -0
- data/lib/frenchy/client.rb +60 -0
- data/lib/frenchy/collection.rb +14 -0
- data/lib/frenchy/instrumentation.rb +58 -0
- data/lib/frenchy/model.rb +132 -0
- data/lib/frenchy/request.rb +38 -0
- data/lib/frenchy/resource.rb +81 -0
- data/lib/frenchy/veneer.rb +29 -0
- data/lib/frenchy/version.rb +3 -0
- data/lib/frenchy.rb +30 -0
- metadata +157 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8bfef8fd98c84c71e524f5162592c7a482871a87
|
4
|
+
data.tar.gz: 1420a7aa007282159d99ec13dceeea34479ffbe1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 05eb4261adeb633381985e6c7e949fb6851d0247fc0c980f446c6bfea4ce34806811e579f1de66cc3f3c43f5c7df09a2446130e78c5ad72cfbbe6c1f3810209d
|
7
|
+
data.tar.gz: 155a63ef8a914acef37dc832d446fb6db131f578527997c87c96367c9826803d692b7a8f5327d555cee8dd2d29cbb14c12575880a6c11124763b0a66a24ed259
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Jason Coene
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
# Frenchy
|
2
|
+
|
3
|
+
Frenchy is a thing for turning HTTP JSON API endpoints into Rails-ish model objects. It deals with making requests, converting responses, type conversion, struct nesting, model decorating and instrumentation.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem "frenchy"
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
Frenchy supports multiple back-end services, you should register them in an initializer:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
# config/initializer/frenchy.rb
|
21
|
+
Frenchy.register_service :dodgeball, host: "http://127.0.0.1:3000"
|
22
|
+
```
|
23
|
+
|
24
|
+
Let's say we want to track the players on dodgeball team. Players have nicknames returned as a nested response:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
class Player
|
28
|
+
# Include Frenchy::Model to get field macros and instance attribute methods
|
29
|
+
include Frenchy::Model
|
30
|
+
|
31
|
+
# Include Frenchy::Resource to get resource macro and class finder methods
|
32
|
+
include Frenchy::Resource
|
33
|
+
|
34
|
+
# Declare which service the model belongs to and specify your named API endpoints
|
35
|
+
resource service: "dodgeball", endpoints: {
|
36
|
+
one: { path: "/v1/players/:id" },
|
37
|
+
many: { path: "/v1/players", many: true },
|
38
|
+
team: { path: "/v1/teams/:team_id/players", many: true }
|
39
|
+
}
|
40
|
+
|
41
|
+
# You can supply a primary key field, which really just uses it for to_param.
|
42
|
+
key :id
|
43
|
+
|
44
|
+
# Define fields which create named attributes and deal with typecasting.
|
45
|
+
# Valid built-in types: string, integer, float, bool, time, array, hash
|
46
|
+
field :id, type: "integer"
|
47
|
+
field :name, type: "string"
|
48
|
+
field :win_rate, type: "float"
|
49
|
+
field :free_agent, type: "bool"
|
50
|
+
|
51
|
+
# You can also supply types of any class that can be instantiated by sending
|
52
|
+
# a hash of attributes to the "new" class method. If you specify the "many"
|
53
|
+
# option, we'll expect that the server returns an array and will properly treat
|
54
|
+
# the response as a collection.
|
55
|
+
field :nicknames, type: "nickname", many: true
|
56
|
+
end
|
57
|
+
|
58
|
+
class Nickname
|
59
|
+
include Frenchy::Model
|
60
|
+
|
61
|
+
field :name, type: "string"
|
62
|
+
field :insulting, type: "bool"
|
63
|
+
end
|
64
|
+
|
65
|
+
# GET /v1/players/1
|
66
|
+
# Expects response '{"id": N, ...}'
|
67
|
+
# Returns a single Player object
|
68
|
+
p = Player.find(1)
|
69
|
+
|
70
|
+
# GET /v1/players/?ids=1,2,3
|
71
|
+
# Expects response '[{"id": N, ...}, ...]'
|
72
|
+
# Returns multiple Player objects
|
73
|
+
Player.find_many([1,2,3])
|
74
|
+
|
75
|
+
# GET /v1/teams/3/players?injured=true
|
76
|
+
# Expects response '[{"id": N, ...}, ...]'
|
77
|
+
# Returns multiple Player objects
|
78
|
+
Player.find_with_endpoint(:team, team_id: 3, injured: true)
|
79
|
+
```
|
80
|
+
|
81
|
+
## Decorators
|
82
|
+
|
83
|
+
Frenchy loves decorating! Call the `.decorate` method on your Frenchy models for fun and profit. Under the covers it will find an appropriately named decorator (ex. `PlayerDecorator`) and call `decorate(self)` on it.
|
84
|
+
|
85
|
+
You can also call decorate on a collection of Frenchy models (as may be returned if you supply `many: true`).
|
86
|
+
|
87
|
+
## Instrumentation
|
88
|
+
|
89
|
+
Frenchy knows you like to monitor things, so requests are instrumented. You can do something like this:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
# in an initializer...
|
93
|
+
|
94
|
+
ActiveSupport::Notifications.subscribe /request.frenchy/ do |*args|
|
95
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
96
|
+
|
97
|
+
# Generates something along the lines of:
|
98
|
+
# [StatsD] dodgeball.player.one.count:1|c
|
99
|
+
# [StatsD] dodgeball.player.one.runtime:528.78|c
|
100
|
+
label = "#{event.payload[:service]}.#{event.payload[:model]}.#{event.payload[:endpoint]}"
|
101
|
+
StatsD.increment "#{label}.count", 1
|
102
|
+
StatsD.increment "#{label}.runtime", event.duration
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
Frenchy also provides Rails controller logging and instrumentation just like ActiveRecord:
|
107
|
+
|
108
|
+
```
|
109
|
+
Dodgeball (14.49ms) GET /v1/players/3
|
110
|
+
...
|
111
|
+
Completed 200 OK in 56.6ms (Views: 49.9ms | Frenchy: 14.49ms | ActiveRecord: 0.9ms)
|
112
|
+
```
|
113
|
+
|
114
|
+
## Mascot
|
115
|
+
|
116
|
+

|
117
|
+
|
118
|
+
## Contributing
|
119
|
+
|
120
|
+
1. Fork it
|
121
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
122
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
123
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
124
|
+
5. Create new Pull Request
|
125
|
+
|
126
|
+
## License
|
127
|
+
|
128
|
+
Copyright 2014 Jason Coene. Frenchy is released under the MIT license. See LICENSE.txt for details.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/frenchie.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "frenchy/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "frenchy"
|
8
|
+
spec.version = Frenchy::VERSION
|
9
|
+
spec.authors = ["Jason Coene"]
|
10
|
+
spec.email = ["jcoene@gmail.com"]
|
11
|
+
spec.description = %q{Frenchy's got the goods}
|
12
|
+
spec.summary = %q{Frenchy's got the goods}
|
13
|
+
spec.homepage = "https://github.com/jcoene/frenchy"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "activemodel"
|
22
|
+
spec.add_dependency "activesupport"
|
23
|
+
spec.add_dependency "http"
|
24
|
+
spec.add_dependency "json"
|
25
|
+
|
26
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
27
|
+
spec.add_development_dependency "rake"
|
28
|
+
spec.add_development_dependency "rspec"
|
29
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "frenchy"
|
2
|
+
require "http"
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Frenchy
|
6
|
+
class Client
|
7
|
+
# Create a new client instance
|
8
|
+
def initialize(options={})
|
9
|
+
options.symbolize_keys!
|
10
|
+
|
11
|
+
@host = options.delete(:host) || "http://127.0.0.1:8080"
|
12
|
+
@timeout = options.delete(:timeout) || 30
|
13
|
+
@retries = options.delete(:retires) || 5
|
14
|
+
end
|
15
|
+
|
16
|
+
# Issue a request with the given path and query parameters
|
17
|
+
def get(path, params)
|
18
|
+
try = 1
|
19
|
+
error = nil
|
20
|
+
|
21
|
+
while try < @retries
|
22
|
+
begin
|
23
|
+
return get_once(path, params)
|
24
|
+
rescue Frenchy::ServerError, Frenchy::InvalidResponse => error
|
25
|
+
sleep (0.35 * (try*try))
|
26
|
+
try += 1
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
raise error
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def get_once(path, params)
|
36
|
+
url = "#{@host}#{path}"
|
37
|
+
|
38
|
+
response = begin
|
39
|
+
HTTP.accept(:json).get(url, params: params).response
|
40
|
+
rescue
|
41
|
+
raise Frenchy::ServerError
|
42
|
+
end
|
43
|
+
|
44
|
+
case response.code
|
45
|
+
when 200
|
46
|
+
begin
|
47
|
+
JSON.parse(response.body)
|
48
|
+
rescue
|
49
|
+
raise Frenchy::InvalidResponse
|
50
|
+
end
|
51
|
+
when 404
|
52
|
+
raise Frenchy::NotFound
|
53
|
+
else
|
54
|
+
raise Frenchy::ServerError, response.inspect
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
public
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Frenchy
|
2
|
+
class Collection < ::Array
|
3
|
+
# Decorate the collection using the name of the decorator inferred by the first record
|
4
|
+
def decorate
|
5
|
+
decorator_class = "#{first.class.name}Decorator".constantize
|
6
|
+
decorator_class.decorate(self)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Backwards compatibility for old version of draper
|
10
|
+
def nil?
|
11
|
+
none?
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
require "active_support/log_subscriber"
|
3
|
+
|
4
|
+
module Frenchy
|
5
|
+
module Instrumentation
|
6
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
7
|
+
def start_processing(event)
|
8
|
+
Thread.current[:frenchy_runtime] = 0.0
|
9
|
+
end
|
10
|
+
|
11
|
+
def request(event)
|
12
|
+
Thread.current[:frenchy_runtime] ||= 0.0
|
13
|
+
Thread.current[:frenchy_runtime] += event.duration
|
14
|
+
if logger.debug?
|
15
|
+
name = "%s (%.2fms)" % [event.payload[:service].capitalize, event.duration]
|
16
|
+
output = " #{color(name, YELLOW, true)} GET #{event.payload[:path]}"
|
17
|
+
if event.payload[:params].any?
|
18
|
+
output += "?"
|
19
|
+
output += event.payload[:params].map {|k,v| "#{k}=#{v}" }.join("&")
|
20
|
+
end
|
21
|
+
debug output
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.runtime
|
26
|
+
Thread.current[:frenchy_runtime] || 0.0
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module ControllerRuntime
|
31
|
+
extend ActiveSupport::Concern
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
def append_info_to_payload(payload)
|
36
|
+
super
|
37
|
+
payload[:frenchy_runtime] = Frenchy::Instrumentation::LogSubscriber.runtime
|
38
|
+
end
|
39
|
+
|
40
|
+
module ClassMethods
|
41
|
+
def log_process_action(payload)
|
42
|
+
messages = super
|
43
|
+
if runtime = payload[:frenchy_runtime]
|
44
|
+
messages << "Frenchy: %.1fms" % runtime.to_f
|
45
|
+
end
|
46
|
+
messages
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
Frenchy::Instrumentation::LogSubscriber.attach_to(:action_controller)
|
54
|
+
Frenchy::Instrumentation::LogSubscriber.attach_to(:frenchy)
|
55
|
+
|
56
|
+
ActiveSupport.on_load(:action_controller) do
|
57
|
+
include Frenchy::Instrumentation::ControllerRuntime
|
58
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module Frenchy
|
2
|
+
module Model
|
3
|
+
def self.included(base)
|
4
|
+
base.class_eval do
|
5
|
+
cattr_accessor :fields
|
6
|
+
|
7
|
+
self.fields = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
base.extend(ClassMethods)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Create a new instance of this model with the given attributes
|
14
|
+
def initialize(attrs={})
|
15
|
+
attrs.each do |k,v|
|
16
|
+
if self.class.fields[k.to_sym]
|
17
|
+
send("#{k}=", v)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Return a hash of field name as string and value pairs
|
23
|
+
def attributes
|
24
|
+
Hash[self.class.fields.map {|k,_| [k.to_s, send(k)]}]
|
25
|
+
end
|
26
|
+
|
27
|
+
# Return a string representing the value of the model instance
|
28
|
+
def inspect
|
29
|
+
"<#{self.class.name} #{attributes.map {|k,v| "#{k}: #{v.inspect}"}.join(", ")}>"
|
30
|
+
end
|
31
|
+
|
32
|
+
# Decorate the model using a decorator inferred by the class
|
33
|
+
def decorate
|
34
|
+
decorator_class = "#{self.class.name}Decorator".constantize
|
35
|
+
decorator_class.decorate(self)
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
def set(name, value, options={})
|
41
|
+
instance_variable_set("@#{name}", value)
|
42
|
+
end
|
43
|
+
|
44
|
+
module ClassMethods
|
45
|
+
# Create a new instance of the model from a hash
|
46
|
+
def from_hash(hash)
|
47
|
+
new(hash)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Create a new instance of the model from JSON
|
51
|
+
def from_json(json)
|
52
|
+
hash = JSON.parse(json)
|
53
|
+
from_hash(hash)
|
54
|
+
end
|
55
|
+
|
56
|
+
protected
|
57
|
+
|
58
|
+
# Macro to add primary key
|
59
|
+
def key(name)
|
60
|
+
define_method(:to_param) do
|
61
|
+
send(name).to_s
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Macro to add a field
|
66
|
+
def field(name, options={})
|
67
|
+
type = (options[:type] || :string).to_sym
|
68
|
+
aliases = (options[:aliases] || [])
|
69
|
+
|
70
|
+
aliases.each do |a|
|
71
|
+
define_method("#{a}") do
|
72
|
+
send(name)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
case type
|
77
|
+
when :string
|
78
|
+
define_method("#{name}=") do |v|
|
79
|
+
set(name, v.to_s, options)
|
80
|
+
end
|
81
|
+
when :integer
|
82
|
+
define_method("#{name}=") do |v|
|
83
|
+
set(name, v.to_i, options)
|
84
|
+
end
|
85
|
+
when :float
|
86
|
+
define_method("#{name}=") do |v|
|
87
|
+
set(name, v.to_f, options)
|
88
|
+
end
|
89
|
+
when :bool
|
90
|
+
define_method("#{name}=") do |v|
|
91
|
+
set(name, ["true", 1, true].include?(v), options)
|
92
|
+
end
|
93
|
+
define_method("#{name}?") do
|
94
|
+
send(name)
|
95
|
+
end
|
96
|
+
when :time
|
97
|
+
define_method("#{name}=") do |v|
|
98
|
+
if v.is_a?(Fixnum)
|
99
|
+
set(name, Time.at(v).to_datetime, options)
|
100
|
+
else
|
101
|
+
set(name, DateTime.parse(v), options)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
when :array
|
105
|
+
define_method("#{name}=") do |v|
|
106
|
+
set(name, Array(v), options)
|
107
|
+
end
|
108
|
+
when :hash
|
109
|
+
define_method("#{name}=") do |v|
|
110
|
+
set(name, Hash[v], options)
|
111
|
+
end
|
112
|
+
else
|
113
|
+
options[:class_name] ||= type.to_s.camelize
|
114
|
+
options[:many] = (name.to_s.singularize != name.to_s) unless options.key?(:many)
|
115
|
+
klass = options[:class_name].constantize
|
116
|
+
|
117
|
+
define_method("#{name}=") do |v|
|
118
|
+
if options[:many]
|
119
|
+
set(name, Frenchy::Collection.new(Array(v).map {|vv| klass.new(vv)}))
|
120
|
+
else
|
121
|
+
set(name, klass.new(v))
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
self.fields[name.to_sym] = options
|
127
|
+
|
128
|
+
attr_reader name.to_sym
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require "frenchy"
|
2
|
+
require "frenchy/client"
|
3
|
+
require "active_support/notifications"
|
4
|
+
|
5
|
+
module Frenchy
|
6
|
+
class Request
|
7
|
+
# Create a new request with given parameters
|
8
|
+
def initialize(service, path, params={}, options={})
|
9
|
+
params.stringify_keys!
|
10
|
+
|
11
|
+
path = path.dup
|
12
|
+
path.scan(/(:[a-z0-9_+]+)/).flatten.uniq.each do |pat|
|
13
|
+
k = pat.sub(":", "")
|
14
|
+
begin
|
15
|
+
v = params.fetch(pat.sub(":", "")).to_s
|
16
|
+
rescue
|
17
|
+
raise Frenchy::InvalidRequest, "The required parameter '#{k}' was not specified."
|
18
|
+
end
|
19
|
+
|
20
|
+
params.delete(k)
|
21
|
+
path.sub!(pat, v)
|
22
|
+
end
|
23
|
+
|
24
|
+
@service = service
|
25
|
+
@path = path
|
26
|
+
@params = params
|
27
|
+
@options = options
|
28
|
+
end
|
29
|
+
|
30
|
+
# Issue the request and return the value
|
31
|
+
def value
|
32
|
+
ActiveSupport::Notifications.instrument("request.frenchy", {service: @service, path: @path, params: @params}.merge(@options)) do
|
33
|
+
client = Frenchy.find_service(@service)
|
34
|
+
client.get(@path, @params)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require "frenchy"
|
2
|
+
require "frenchy/request"
|
3
|
+
|
4
|
+
module Frenchy
|
5
|
+
module Resource
|
6
|
+
def self.included(base)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
# Find record(s) using the default endpoint and flexible input
|
12
|
+
def find(params={})
|
13
|
+
params = {id: params.to_s} if [Fixnum, String].any? {|c| params.is_a? c }
|
14
|
+
find_with_endpoint(:default, params)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Find a single record using the "one" (or "default") endpoint and an id
|
18
|
+
def find_one(id, params={})
|
19
|
+
find_with_endpoint([:one, :default], {id: id}.merge(params))
|
20
|
+
end
|
21
|
+
|
22
|
+
# Find multiple record using the "many" (or "default") endpoint and an array of ids
|
23
|
+
def find_many(ids, params={})
|
24
|
+
find_with_endpoint([:many, :default], {ids: ids.join(",")}.merge(params))
|
25
|
+
end
|
26
|
+
|
27
|
+
# Find with a specific endpoint and params
|
28
|
+
def find_with_endpoint(endpoints, params={})
|
29
|
+
name, endpoint = resolve_endpoints(endpoints)
|
30
|
+
options = {model: self.name.underscore, endpoint: name.to_s}
|
31
|
+
response = Frenchy::Request.new(@service, endpoint[:path], params, options).value
|
32
|
+
|
33
|
+
if response.is_a?(Array)
|
34
|
+
Frenchy::Collection.new(Array(response).map {|v| from_hash(v) })
|
35
|
+
else
|
36
|
+
from_hash(response)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Choose the first available endpoint
|
43
|
+
def resolve_endpoints(endpoints)
|
44
|
+
Array(endpoints).map(&:to_sym).each do |sym|
|
45
|
+
if ep = @endpoints[sym]
|
46
|
+
return sym, ep
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
raise(Frenchy::ConfigurationError, "Resource does not contain any endpoints: #{endpoints.join(", ")}")
|
51
|
+
end
|
52
|
+
|
53
|
+
# Macro to set the location pattern for this request
|
54
|
+
def resource(options={})
|
55
|
+
options.symbolize_keys!
|
56
|
+
|
57
|
+
@service = options.delete(:service) || raise(Frenchy::ConfigurationError, "Resource must specify a service")
|
58
|
+
|
59
|
+
if endpoints = options.delete(:endpoints)
|
60
|
+
@endpoints = validate_endpoints(endpoints)
|
61
|
+
elsif endpoint = options.delete(:endpoint)
|
62
|
+
@endpoints = validate_endpoints({default: endpoint})
|
63
|
+
else
|
64
|
+
raise(Frenchy::ConfigurationError, "Resource must specify one or more endpoint")
|
65
|
+
end
|
66
|
+
|
67
|
+
@many = options.delete(:many) || false
|
68
|
+
end
|
69
|
+
|
70
|
+
def validate_endpoints(endpoints={})
|
71
|
+
endpoints.symbolize_keys!
|
72
|
+
|
73
|
+
Hash[endpoints.map do |k,v|
|
74
|
+
v.symbolize_keys!
|
75
|
+
raise(Frenchy::ConfigurationError, "Endpoint #{k} does not specify a path") unless v[:path]
|
76
|
+
[k,v]
|
77
|
+
end]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require "frenchy"
|
2
|
+
require "active_model/naming"
|
3
|
+
|
4
|
+
module Frenchy
|
5
|
+
# Veneer provides a friendly face on unfriendly models, allowing your Frenchy
|
6
|
+
# models to appear as though they were of another class.
|
7
|
+
module Veneer
|
8
|
+
def self.included(base)
|
9
|
+
if defined?(ActiveModel)
|
10
|
+
base.extend(ClassMethods)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
# Macro to establish a veneer for a given model
|
16
|
+
def veneer(options={})
|
17
|
+
options.symbolize_keys!
|
18
|
+
@model = options.delete(:model) || raise(Frenchy::ConfigurationError, "Veneer must specify a model")
|
19
|
+
extend ActiveModel::Naming
|
20
|
+
|
21
|
+
class_eval do
|
22
|
+
def self.model_name
|
23
|
+
ActiveModel::Name.new(self, nil, @model.to_s.camelize)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/frenchy.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require "frenchy/client"
|
2
|
+
require "frenchy/collection"
|
3
|
+
require "frenchy/instrumentation"
|
4
|
+
require "frenchy/model"
|
5
|
+
require "frenchy/request"
|
6
|
+
require "frenchy/resource"
|
7
|
+
require "frenchy/veneer"
|
8
|
+
require "frenchy/version"
|
9
|
+
|
10
|
+
module Frenchy
|
11
|
+
class Error < ::StandardError; end
|
12
|
+
class NotFound < Error; end
|
13
|
+
class ServerError < Error; end
|
14
|
+
class InvalidResponse < Error; end
|
15
|
+
class InvalidRequest < Error; end
|
16
|
+
class ConfigurationError < Error; end
|
17
|
+
|
18
|
+
def self.register_service(name, options={})
|
19
|
+
@services ||= {}
|
20
|
+
@services[name.to_sym] = Frenchy::Client.new(options)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.find_service(name)
|
24
|
+
if @services.nil?
|
25
|
+
raise(Frenchy::ConfigurationError, "No services have been configured")
|
26
|
+
end
|
27
|
+
|
28
|
+
@services[name.to_sym] || raise(Frenchy::ConfigurationError, "No service with name #{name} registered:")
|
29
|
+
end
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: frenchy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jason Coene
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-02-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activemodel
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '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'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: http
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: json
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: bundler
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.3'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.3'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description: Frenchy's got the goods
|
112
|
+
email:
|
113
|
+
- jcoene@gmail.com
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- .gitignore
|
119
|
+
- Gemfile
|
120
|
+
- LICENSE.txt
|
121
|
+
- README.md
|
122
|
+
- Rakefile
|
123
|
+
- frenchie.gemspec
|
124
|
+
- lib/frenchy.rb
|
125
|
+
- lib/frenchy/client.rb
|
126
|
+
- lib/frenchy/collection.rb
|
127
|
+
- lib/frenchy/instrumentation.rb
|
128
|
+
- lib/frenchy/model.rb
|
129
|
+
- lib/frenchy/request.rb
|
130
|
+
- lib/frenchy/resource.rb
|
131
|
+
- lib/frenchy/veneer.rb
|
132
|
+
- lib/frenchy/version.rb
|
133
|
+
homepage: https://github.com/jcoene/frenchy
|
134
|
+
licenses:
|
135
|
+
- MIT
|
136
|
+
metadata: {}
|
137
|
+
post_install_message:
|
138
|
+
rdoc_options: []
|
139
|
+
require_paths:
|
140
|
+
- lib
|
141
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - '>='
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
147
|
+
requirements:
|
148
|
+
- - '>='
|
149
|
+
- !ruby/object:Gem::Version
|
150
|
+
version: '0'
|
151
|
+
requirements: []
|
152
|
+
rubyforge_project:
|
153
|
+
rubygems_version: 2.0.2
|
154
|
+
signing_key:
|
155
|
+
specification_version: 4
|
156
|
+
summary: Frenchy's got the goods
|
157
|
+
test_files: []
|