frenchy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
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
+ ![Frenchy](http://i.imgur.com/vQcCQfK.png)
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
@@ -0,0 +1,3 @@
1
+ module Frenchy
2
+ VERSION = "0.0.1"
3
+ 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: []