lazy_resource 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.rspec +3 -0
- data/.rvmrc +1 -0
- data/Gemfile +14 -0
- data/Guardfile +5 -0
- data/LICENSE +22 -0
- data/README.md +127 -0
- data/Rakefile +6 -0
- data/examples/github.rb +64 -0
- data/lazy_resource.gemspec +22 -0
- data/lib/lazy_resource.rb +39 -0
- data/lib/lazy_resource/attributes.rb +131 -0
- data/lib/lazy_resource/errors.rb +72 -0
- data/lib/lazy_resource/http_mock.rb +23 -0
- data/lib/lazy_resource/mapping.rb +73 -0
- data/lib/lazy_resource/relation.rb +132 -0
- data/lib/lazy_resource/request.rb +63 -0
- data/lib/lazy_resource/resource.rb +197 -0
- data/lib/lazy_resource/resource_queue.rb +40 -0
- data/lib/lazy_resource/types.rb +53 -0
- data/lib/lazy_resource/url_generation.rb +98 -0
- data/lib/lazy_resource/version.rb +3 -0
- data/spec/fixtures/comment.rb +6 -0
- data/spec/fixtures/post.rb +6 -0
- data/spec/fixtures/user.rb +9 -0
- data/spec/lazy_resource/attributes_spec.rb +157 -0
- data/spec/lazy_resource/errors_spec.rb +48 -0
- data/spec/lazy_resource/mapping_spec.rb +237 -0
- data/spec/lazy_resource/relation_spec.rb +169 -0
- data/spec/lazy_resource/request_spec.rb +143 -0
- data/spec/lazy_resource/resource_queue_spec.rb +66 -0
- data/spec/lazy_resource/resource_spec.rb +463 -0
- data/spec/lazy_resource/types_spec.rb +109 -0
- data/spec/lazy_resource/url_generation_spec.rb +155 -0
- data/spec/spec_helper.rb +25 -0
- metadata +165 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use 1.9.3@lazy_resource --create
|
data/Gemfile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in lazy_resource.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
# Development gems
|
7
|
+
gem 'pry'
|
8
|
+
gem 'debugger'
|
9
|
+
gem 'rake'
|
10
|
+
|
11
|
+
gem 'rspec'
|
12
|
+
gem 'simplecov', :require => false
|
13
|
+
gem 'guard-rspec'
|
14
|
+
gem 'growl'
|
data/Guardfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Andrew Latimer
|
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,127 @@
|
|
1
|
+
# LazyResource
|
2
|
+
|
3
|
+
[ActiveResource](http://github.com/rails/activeresource) with its feet
|
4
|
+
up. The block less, do more consumer of delicious APIs.
|
5
|
+
|
6
|
+
LazyResource is `ActiveRecord` made less blocking. Built on top of
|
7
|
+
[Typhoeus](https://github.com/typhoeus/typhoeus), it queues up your requests to make your
|
8
|
+
API consumer a whole lot quicker. Work smarter, not harder.
|
9
|
+
|
10
|
+
It also has a simple, readable, easy-to-use API, borrowing some of the
|
11
|
+
best parts of ActiveResource with a bit of ActiveRecord method-chaining
|
12
|
+
flair. Not only is it faster, it's better-looking, too.
|
13
|
+
|
14
|
+
Don't believe me? Check out some of the examples in the `examples` directory
|
15
|
+
to see for yourself.
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Add this line to your application's Gemfile:
|
20
|
+
|
21
|
+
gem 'lazy_resource'
|
22
|
+
|
23
|
+
And then execute:
|
24
|
+
|
25
|
+
$ bundle
|
26
|
+
|
27
|
+
Or install it yourself as:
|
28
|
+
|
29
|
+
$ gem install lazy_resource
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
### Define a model:
|
34
|
+
|
35
|
+
class User
|
36
|
+
include LazyResource::Resource
|
37
|
+
|
38
|
+
self.site = 'http://example.com'
|
39
|
+
|
40
|
+
attribute :id, Integer
|
41
|
+
attribute :first_name, String
|
42
|
+
attribute :last_name, String
|
43
|
+
end
|
44
|
+
|
45
|
+
### Then use it:
|
46
|
+
|
47
|
+
me = User.find(1) # => GET /users/1
|
48
|
+
bobs = User.where(:first_name => 'Bob') # => GET /users?first_name=Bob
|
49
|
+
sam = User.find_by_first_name('Sam') # => GET /users?first_name=Sam
|
50
|
+
terry = User.new(:first_name => 'Terry', :last_name => 'Simpson')
|
51
|
+
terry.save # => POST /users
|
52
|
+
terry.last_name = 'Jackson'
|
53
|
+
terry.save # => PUT /users/4
|
54
|
+
terry.destroy # => DELETE /users/4
|
55
|
+
|
56
|
+
### What about associations?
|
57
|
+
|
58
|
+
class Post
|
59
|
+
include LazyResource::Resource
|
60
|
+
|
61
|
+
self.site = 'http://example.com'
|
62
|
+
|
63
|
+
attribute :id, Integer
|
64
|
+
attribute :title, String
|
65
|
+
attribute :body, String
|
66
|
+
attribute :user, User
|
67
|
+
end
|
68
|
+
|
69
|
+
class User
|
70
|
+
include LazyResource::Resource
|
71
|
+
# Attributes that have a type in an array are has-many
|
72
|
+
attribute :posts, [Post]
|
73
|
+
end
|
74
|
+
|
75
|
+
me = User.find(1)
|
76
|
+
me.posts.all # => GET /users/1/posts
|
77
|
+
|
78
|
+
### That's cool, but what if my end-point doesn't map with my association name?
|
79
|
+
|
80
|
+
class Photo
|
81
|
+
include LazyResource::Resource
|
82
|
+
|
83
|
+
attribute :id, Integer
|
84
|
+
attribute :urls, Hash
|
85
|
+
attribute :photographer, User, :from => 'users'
|
86
|
+
end
|
87
|
+
|
88
|
+
### I thought you said this was non-blocking?
|
89
|
+
|
90
|
+
It is. That original example above with me, the Bobs, Sam, and Terry? Those
|
91
|
+
first four requests would all get executed at the same time, when Terry
|
92
|
+
was saved. Pretty neat, eh?
|
93
|
+
|
94
|
+
### That's great, but could you show me some examples that are a bit more complex?
|
95
|
+
|
96
|
+
Sure thing! Take a look at the files in the `examples` directory, or
|
97
|
+
read through the specs.
|
98
|
+
|
99
|
+
## Contributing
|
100
|
+
|
101
|
+
1. Fork it
|
102
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
103
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
104
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
105
|
+
5. Create new Pull Request
|
106
|
+
|
107
|
+
Make sure you have some decent test coverage, and please don't bump up
|
108
|
+
the version number. If you want to maintain your own version, go for it,
|
109
|
+
but put it in a separate commit so I can ignore it when I merge the rest
|
110
|
+
of your stuff in.
|
111
|
+
|
112
|
+
## Recognition
|
113
|
+
|
114
|
+
Thanks to:
|
115
|
+
|
116
|
+
* [Typhoeus](http://github.com/typhoeus/typhoeus) for the http request
|
117
|
+
queuing code that forms the foundation of LazyResource.
|
118
|
+
* [ActiveResource](http://github.com/rails/activeresource) for the idea
|
119
|
+
(and a bit of code).
|
120
|
+
* [Get Satisfaction](http://getsatisfaction.com) for putting food on my
|
121
|
+
table.
|
122
|
+
|
123
|
+
## TODO
|
124
|
+
|
125
|
+
* Clean up `LazyResource::Attributes#create_setter`
|
126
|
+
* Add more specs for associations
|
127
|
+
|
data/Rakefile
ADDED
data/examples/github.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'lazy_resource'
|
2
|
+
require 'benchmark'
|
3
|
+
|
4
|
+
LazyResource.configure do |config|
|
5
|
+
config.site = "https://api.github.com"
|
6
|
+
end
|
7
|
+
|
8
|
+
class User
|
9
|
+
include LazyResource::Resource
|
10
|
+
end
|
11
|
+
|
12
|
+
class Repo
|
13
|
+
include LazyResource::Resource
|
14
|
+
|
15
|
+
attribute :id, Fixnum
|
16
|
+
attribute :pushed_at, DateTime
|
17
|
+
attribute :owner, User
|
18
|
+
attribute :clone_url, String
|
19
|
+
attribute :name, String
|
20
|
+
attribute :description, String
|
21
|
+
end
|
22
|
+
|
23
|
+
class User
|
24
|
+
include LazyResource::Resource
|
25
|
+
|
26
|
+
self.primary_key_name = 'login'
|
27
|
+
|
28
|
+
attribute :id, Fixnum
|
29
|
+
attribute :login, String
|
30
|
+
attribute :name, String
|
31
|
+
attribute :bio, String
|
32
|
+
attribute :company, String
|
33
|
+
attribute :number_of_public_repos, Fixnum, :from => 'public_repos'
|
34
|
+
attribute :repos, [Repo]
|
35
|
+
end
|
36
|
+
|
37
|
+
dhh = User.find('dhh')
|
38
|
+
dhh.repos.each do |repo|
|
39
|
+
puts "#{dhh.name} pushed to #{repo.name} on #{repo.pushed_at.strftime("%D")}"
|
40
|
+
end
|
41
|
+
|
42
|
+
puts "Fetching 10 users serially..."
|
43
|
+
Benchmark.bm do |x|
|
44
|
+
x.report do
|
45
|
+
names = []
|
46
|
+
10.times do |i|
|
47
|
+
u = User.find(i + 1)
|
48
|
+
names << u.name
|
49
|
+
end
|
50
|
+
|
51
|
+
puts names
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
puts "\nFetching 10 users in parallel..."
|
56
|
+
Benchmark.bm do |x|
|
57
|
+
x.report do
|
58
|
+
users = []
|
59
|
+
10.times do |i|
|
60
|
+
users << User.find(i + 1)
|
61
|
+
end
|
62
|
+
puts users.map { |user| user.name }
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/lazy_resource/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Andrew Latimer"]
|
6
|
+
gem.email = ["andrew@elpasoera.com"]
|
7
|
+
gem.description = %q{ActiveResource with it's feet up. The write less, do more consumer of delicious APIs.}
|
8
|
+
gem.summary = %q{ActiveResource with it's feet up. The write less, do more consumer of delicious APIs.}
|
9
|
+
gem.homepage = "http://github.com/ahlatimer/lazy_resource"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "lazy_resource"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = LazyResource::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency 'activemodel', '>= 3.1.0'
|
19
|
+
gem.add_dependency 'activesupport', '>= 3.1.0'
|
20
|
+
gem.add_dependency 'json', '>= 1.5.2'
|
21
|
+
gem.add_dependency 'typhoeus', '>= 0.2.4'
|
22
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
require 'active_support'
|
3
|
+
require 'json'
|
4
|
+
require 'typhoeus'
|
5
|
+
|
6
|
+
require 'active_support'
|
7
|
+
require 'active_support/core_ext/class/attribute_accessors'
|
8
|
+
require 'active_support/core_ext/class/attribute'
|
9
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
10
|
+
require 'active_support/core_ext/kernel/reporting'
|
11
|
+
require 'active_support/core_ext/module/delegation'
|
12
|
+
require 'active_support/core_ext/module/aliasing'
|
13
|
+
require 'active_support/core_ext/object/blank'
|
14
|
+
require 'active_support/core_ext/object/to_query'
|
15
|
+
require 'active_support/core_ext/object/duplicable'
|
16
|
+
require 'set'
|
17
|
+
require 'uri'
|
18
|
+
|
19
|
+
require 'active_support/core_ext/uri'
|
20
|
+
|
21
|
+
require 'lazy_resource/version'
|
22
|
+
require 'lazy_resource/errors'
|
23
|
+
|
24
|
+
module LazyResource
|
25
|
+
extend ActiveSupport::Autoload
|
26
|
+
|
27
|
+
autoload :Attributes
|
28
|
+
autoload :Mapping
|
29
|
+
autoload :Relation
|
30
|
+
autoload :Request
|
31
|
+
autoload :Resource
|
32
|
+
autoload :ResourceQueue
|
33
|
+
autoload :Types
|
34
|
+
autoload :UrlGeneration
|
35
|
+
|
36
|
+
def self.configure(&block)
|
37
|
+
yield Resource
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
module LazyResource
|
2
|
+
module Attributes
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
include ActiveModel::Dirty
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def attribute(name, type, options={})
|
8
|
+
attributes[name] = { :type => type, :options => options }
|
9
|
+
|
10
|
+
create_getter(name, type, options) unless options[:skip_getter]
|
11
|
+
create_setter(name, type, options) unless options[:skip_setter]
|
12
|
+
create_question(name, type, options) unless options[:skip_question] || options[:skip_getter]
|
13
|
+
|
14
|
+
@attribute_methods_generated = false
|
15
|
+
define_attribute_methods [name]
|
16
|
+
end
|
17
|
+
|
18
|
+
def fetch_all
|
19
|
+
self.resource_queue.send_to_request_queue! if self.respond_to?(:resource_queue)
|
20
|
+
self.request_queue.run if self.respond_to?(:request_queue)
|
21
|
+
end
|
22
|
+
|
23
|
+
def attributes
|
24
|
+
@attributes ||= {}
|
25
|
+
end
|
26
|
+
|
27
|
+
def primary_key_name
|
28
|
+
@primary_key_name ||= 'id'
|
29
|
+
end
|
30
|
+
|
31
|
+
def primary_key_name=(pk)
|
32
|
+
@primary_key_name = pk
|
33
|
+
end
|
34
|
+
|
35
|
+
attr_writer :element_name
|
36
|
+
def element_name
|
37
|
+
@element_name ||= model_name.element
|
38
|
+
end
|
39
|
+
|
40
|
+
attr_writer :collection_name
|
41
|
+
def collection_name
|
42
|
+
@collection_name ||= ActiveSupport::Inflector.pluralize(element_name)
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
def create_setter(name, type, options={})
|
47
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
48
|
+
def #{name}=(value)
|
49
|
+
self.class.fetch_all if !fetched?
|
50
|
+
|
51
|
+
#{name}_will_change! unless @#{name} == value || (!fetched? && persisted?)
|
52
|
+
@#{name} = value
|
53
|
+
end
|
54
|
+
RUBY
|
55
|
+
end
|
56
|
+
|
57
|
+
def create_getter(name, type, options={})
|
58
|
+
method = <<-RUBY
|
59
|
+
def #{name}
|
60
|
+
self.class.fetch_all if !fetched
|
61
|
+
RUBY
|
62
|
+
|
63
|
+
if type.is_a?(Array) && type.first.include?(LazyResource::Resource)
|
64
|
+
if options[:using].nil?
|
65
|
+
method << <<-RUBY
|
66
|
+
if @#{name}.nil?
|
67
|
+
@#{name} = #{type.first}.where(:"\#{self.class.element_name}_id" => self.primary_key)
|
68
|
+
end
|
69
|
+
RUBY
|
70
|
+
else
|
71
|
+
method << <<-RUBY
|
72
|
+
return [] if self.#{options[:using]}.nil?
|
73
|
+
|
74
|
+
if @#{name}.nil?
|
75
|
+
@#{name} = LazyResource::Relation.new(#{type.first}, :fetched => true)
|
76
|
+
@#{name}.fetched = false
|
77
|
+
request = LazyResource::Request.new(self.#{options[:using]}, @#{name})
|
78
|
+
self.class.request_queue.queue(request)
|
79
|
+
end
|
80
|
+
|
81
|
+
@#{name}
|
82
|
+
RUBY
|
83
|
+
end
|
84
|
+
elsif type.include?(LazyResource::Resource)
|
85
|
+
if options[:using].nil?
|
86
|
+
method << <<-RUBY
|
87
|
+
if @#{name}.nil?
|
88
|
+
@#{name} = #{type}.where(:"\#{self.class.element_name}_id" => self.primary_key)
|
89
|
+
end
|
90
|
+
RUBY
|
91
|
+
else
|
92
|
+
method << <<-RUBY
|
93
|
+
return [] if self.#{options[:using]}.nil?
|
94
|
+
|
95
|
+
if @#{name}.nil?
|
96
|
+
@#{name} = #{type}.new
|
97
|
+
request = LazyResource::Request.new(self.#{options[:using]}, @#{name})
|
98
|
+
self.class.request_queue.queue(request)
|
99
|
+
end
|
100
|
+
|
101
|
+
@#{name}
|
102
|
+
RUBY
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
method << <<-RUBY
|
107
|
+
@#{name}
|
108
|
+
end
|
109
|
+
RUBY
|
110
|
+
|
111
|
+
class_eval method, __FILE__, __LINE__ + 1
|
112
|
+
end
|
113
|
+
|
114
|
+
def create_question(name, type, options={})
|
115
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
116
|
+
def #{name}?
|
117
|
+
!!self.#{name}
|
118
|
+
end
|
119
|
+
RUBY
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def primary_key
|
124
|
+
self.send(self.class.primary_key_name)
|
125
|
+
end
|
126
|
+
|
127
|
+
included do
|
128
|
+
extend ActiveModel::Naming
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|