remote-resource 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +77 -58
- data/lib/remote-resource.rb +4 -0
- data/lib/remote_resource/association_builder.rb +1 -0
- data/lib/remote_resource/storage/redis.rb +13 -4
- data/lib/remote_resource/storage/serializers/{marshal.rb → marshal_serializer.rb} +1 -1
- data/lib/remote_resource/version.rb +1 -1
- data/lib/remote_resource.rb +26 -0
- metadata +4 -5
- data/lib/remote_resource/storage/db_cache.rb +0 -36
- data/lib/remote_resource/storage/db_cache_factory.rb +0 -38
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0615325b89bdbae524b7e54dc0360774ba453e55
|
4
|
+
data.tar.gz: f9441fa6d760b8f0f4e49e038498cdc0b249ffb8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5b73cc3191733078a6589fdda37bf1c0e45d2eb2fcd13e990f53c66ace41860dcf7a001fa078e4e70031503633c5adf9ae1a49bab721192964a3c55459820ec0
|
7
|
+
data.tar.gz: 3b8e1950c93ea03a50b95d00e0a4f75ef3faf5f2528fdfc3db55b3a87878f2169306619f011709a165bed7ada0dca83a009d7e248f0dc83a8c1312d592558bc7
|
data/README.md
CHANGED
@@ -3,35 +3,36 @@
|
|
3
3
|
[![Build Status](https://travis-ci.org/mkcode/remote-resource.svg?branch=master)](https://travis-ci.org/mkcode/remote-resource)
|
4
4
|
[![Code Climate](https://codeclimate.com/github/mkcode/remote-resource/badges/gpa.svg)](https://codeclimate.com/github/mkcode/remote-resource)
|
5
5
|
[![Test Coverage](https://codeclimate.com/github/mkcode/remote-resource/badges/coverage.svg)](https://codeclimate.com/github/mkcode/remote-resource/coverage)
|
6
|
-
[![Inline docs](http://inch-ci.org/github/mkcode/remote_resource.svg?branch=master)](http://inch-ci.org/github/mkcode/remote_resource)
|
7
6
|
|
8
|
-
|
7
|
+
__RemoteResource__ allows you to easily create `ActiveRecord` style domain
|
8
|
+
objects that represent a foreign API. These `remote resources` can be mixed into
|
9
|
+
or associated with other ActiveRecord models in the same way you work with all
|
10
|
+
your other models. Using these conventions yields some major performance gains
|
11
|
+
through caching and fast and simple development through familiarity.
|
9
12
|
|
10
|
-
|
11
|
-
* Work with foreign APIs in the same way you work with ActiveRecord
|
12
|
-
associations.
|
13
|
-
* Transparently caches API responses for major performance gains.
|
14
|
-
* Don't fail when APIs your app relies on are momentarily down.
|
15
|
-
* Respect your APIs Cache-Control header. Or don't. It's up to you.
|
16
|
-
* Configurable logging and error reporting.
|
17
|
-
* Trivial to add support for your new API client.
|
13
|
+
## Why RemoteResource
|
18
14
|
|
19
|
-
|
15
|
+
* Familiar - The DSL used to wrap foreign APIs is simple and intuitive. Using
|
16
|
+
the remote resource will be familiar to anyone who has worked with
|
17
|
+
ActiveRecord models.
|
20
18
|
|
21
|
-
|
22
|
-
|
23
|
-
be mixed in and associated with other ActiveRecord models in the same way you
|
24
|
-
work with all your other models. Using this pattern and these conventions yields
|
25
|
-
some major performance gains through caching and fast and simple development
|
26
|
-
through familiarity.
|
19
|
+
* Reusable - Write your API interface once. Associate it with an ActiveRecord
|
20
|
+
object, embed it into a value object, or instantiate it for use in a service.
|
27
21
|
|
28
|
-
|
22
|
+
* Performant - API responses are transparently cached. Subsequent calls move at
|
23
|
+
the speed of redis. Etag based cache expiring, which you may override. Makes
|
24
|
+
detailed list pages possible.
|
25
|
+
|
26
|
+
* Resiliant - Easy to configure error handling, just like ActionContoller. Use
|
27
|
+
cached values to rescue momentary network failures.
|
28
|
+
|
29
|
+
## Getting started
|
29
30
|
|
30
31
|
Create a `remote_resource`, such as:
|
31
32
|
|
32
33
|
```ruby
|
33
|
-
# in
|
34
|
-
class GithubUser <
|
34
|
+
# in app/remote_resources/github_user.rb
|
35
|
+
class GithubUser < RemoteResource::Base
|
35
36
|
client { Octokit::Client.new }
|
36
37
|
resource { |client, scope| client.user(scope[:github_login]) }
|
37
38
|
|
@@ -45,7 +46,7 @@ end
|
|
45
46
|
Associate it with your ActiveRecord `User` model:
|
46
47
|
|
47
48
|
```ruby
|
48
|
-
# in
|
49
|
+
# in app/models/user.rb
|
49
50
|
class User < ActiveRecord::Base
|
50
51
|
has_remote :github_user, scope: :github_login
|
51
52
|
|
@@ -59,7 +60,7 @@ local models.
|
|
59
60
|
```ruby
|
60
61
|
user = User.find(1)
|
61
62
|
|
62
|
-
user.github_user.
|
63
|
+
user.github_user.id
|
63
64
|
user.github_user.avatar_url
|
64
65
|
```
|
65
66
|
|
@@ -81,7 +82,7 @@ And then execute:
|
|
81
82
|
|
82
83
|
Or install it yourself as:
|
83
84
|
|
84
|
-
$ gem install
|
85
|
+
$ gem install remote-resource
|
85
86
|
|
86
87
|
## Defining an RemoteResource
|
87
88
|
|
@@ -90,7 +91,7 @@ This folder is automatically added to your Rails eager loaded paths.
|
|
90
91
|
|
91
92
|
```ruby
|
92
93
|
# In `app/remote_resources/github_user.rb
|
93
|
-
class
|
94
|
+
class GithubUser < RemoteResource::Base
|
94
95
|
client { Octokit::Client.new }
|
95
96
|
|
96
97
|
resource { |client, scope| client.user(scope[:github_login]) }
|
@@ -103,9 +104,11 @@ class GithubUserAttributes < RemoteResource::Base
|
|
103
104
|
end
|
104
105
|
```
|
105
106
|
|
106
|
-
The are 4 class methods that are available to help define an (API) remote
|
107
|
+
The are 4 class methods that are available to help define an (API) remote
|
108
|
+
resource. They are:
|
107
109
|
|
108
|
-
* __client__: You return an instance of the web client that the API uses in a
|
110
|
+
* __client__: You return an instance of the web client that the API uses in a
|
111
|
+
block. That block yields the scope. (More on scope later.)
|
109
112
|
|
110
113
|
* __resource__: Supply a block to the resource method that returns a a remote
|
111
114
|
resource. For example, the 'show user' response (GET /user/:github_login)
|
@@ -117,10 +120,10 @@ The are 4 class methods that are available to help define an (API) remote resour
|
|
117
120
|
This will be mapped to a method later. Optionally takes a second symbol
|
118
121
|
argument referring to a non-default resource (with an argument).
|
119
122
|
|
120
|
-
* __rescue_from__: Works in the same way that
|
123
|
+
* __rescue_from__: Works in the same way that ActionController's rescue_from
|
121
124
|
works. It takes one or many Error class(es), and either a block of a `:with`
|
122
|
-
option that refers to an instance method on this class. The block
|
123
|
-
|
125
|
+
option that refers to an instance method on this class. The block or instance
|
126
|
+
method receive the error and an additional context hash as arguments.
|
124
127
|
|
125
128
|
Remote resource allows you to define any instance method you like on it, which
|
126
129
|
may be used by being instantiated itself or from an associated model.
|
@@ -144,7 +147,7 @@ The following instance methods are available within a RemoteResource::Base class
|
|
144
147
|
|
145
148
|
```ruby
|
146
149
|
# In `app/remote_resources/github_user.rb
|
147
|
-
class
|
150
|
+
class GithubUser < RemoteResource::Base
|
148
151
|
client { Octokit::Client.new }
|
149
152
|
resource { |client, scope| client.user(scope[:github_login]) }
|
150
153
|
attribute :name
|
@@ -200,7 +203,7 @@ github_user.markdown_summary
|
|
200
203
|
#=> "<h1>A big hello to Chris Ewald!!!</h1>"
|
201
204
|
```
|
202
205
|
|
203
|
-
We also may call any of our defined attributes.
|
206
|
+
We also may call any of our defined attributes.
|
204
207
|
|
205
208
|
```ruby
|
206
209
|
github_user.name
|
@@ -238,19 +241,20 @@ Two methods are available for your model classes. `has_remote` and
|
|
238
241
|
`embeds_remote`. They take all the same options and do mostly the same thing;
|
239
242
|
create a method on the calling object, which returns that records associated
|
240
243
|
RemoteResource instance. `embeds_remote` will go one step further and define all
|
241
|
-
of the attribute getter methods on the
|
242
|
-
create
|
243
|
-
is largely related to Inhertance vs Composition
|
244
|
-
welcome to look up on your own time.
|
245
|
-
through `embeds_remote` and 'has'
|
246
|
-
|
247
|
-
|
244
|
+
of the attribute getter methods on the calling class as well. This can be used
|
245
|
+
to create flat domain objects, or possibly value_objects, which are backed by
|
246
|
+
values from a remote API. This is largely related to Inhertance vs Composition
|
247
|
+
in programming theory which you are welcome to look up on your own time.
|
248
|
+
RemoteResource supports both styles; 'Is' through `embeds_remote` and 'has'
|
249
|
+
through `has_remote`. If unsure, it is best to prefer composition and use
|
250
|
+
`has_remote` over `embeds_remote` to create a clear distinction between your
|
251
|
+
local and remote domain.
|
248
252
|
|
249
253
|
## Extending other domain objects
|
250
254
|
|
251
|
-
If you do not use ActiveRecord in your app, you may still use
|
252
|
-
simply extending the Bridge module onto
|
253
|
-
|
255
|
+
If you do not use ActiveRecord in your app, you may still use remote-resource by
|
256
|
+
simply extending the Bridge module onto whatever class you use. The `has_remote`
|
257
|
+
and `embed_remote` methods will then be available. For example:
|
254
258
|
|
255
259
|
```ruby
|
256
260
|
class MyPoro
|
@@ -261,54 +265,69 @@ end
|
|
261
265
|
|
262
266
|
## Configuration
|
263
267
|
|
264
|
-
In a initializer, like `config/initializers/remote_resource.rb`, you may
|
268
|
+
In a initializer, like `config/initializers/remote_resource.rb`, you may
|
269
|
+
override the following options:
|
265
270
|
|
266
271
|
```ruby
|
267
|
-
# Setup global storages. For now there
|
268
|
-
# Memory store.
|
269
|
-
|
270
|
-
|
272
|
+
# Setup global storages. For now there are Redis and Memory stores available.
|
273
|
+
# Default is Memory store.
|
274
|
+
|
275
|
+
# Storage::Redis takes an instance of redis client and the following options:
|
276
|
+
#
|
277
|
+
# expires_in - Time in seconds for to keys ttl. (Default is 1 day)
|
278
|
+
# serializer - Instance of the serializer to load and dump the response.
|
279
|
+
# (Default is MarshalSerializer.new)
|
280
|
+
#
|
271
281
|
RemoteResource.storages = [
|
272
|
-
RemoteResource::Storage::Redis.new(
|
282
|
+
RemoteResource::Storage::Redis.new(Redis.new(url:nil), expires_in: 7.days)
|
273
283
|
]
|
274
284
|
|
275
|
-
#
|
285
|
+
# Specify the logger RemoteResource should use:
|
276
286
|
|
277
287
|
RemoteResource.logger = Logger.new(STDOUT)
|
278
288
|
|
279
|
-
# Setup a lookup method. Only default for now, but the `
|
289
|
+
# Setup a lookup method. Only default for now, but the `validate` option
|
280
290
|
# may be changed to true or false. True will always revalidate. False will never
|
281
291
|
# revalidate. :cache_control respects the Cache-Control header.
|
282
292
|
|
283
|
-
require 'remote_resource/lookup/default'
|
284
293
|
RemoteResource.lookup_method = RemoteResource::Lookup::Default.new(validate: true)
|
285
294
|
```
|
286
295
|
|
287
296
|
## Notifications
|
288
297
|
|
289
|
-
There are
|
298
|
+
There are 3 ActiveSupport notifications that you may subscribe to, to do in
|
299
|
+
depth profiling of this gem:
|
290
300
|
|
291
301
|
* find.remote_resource
|
292
302
|
* storage_lookup.remote_resource
|
293
|
-
* http_head.remote_resource
|
294
303
|
* http_get.remote_resource
|
295
304
|
|
305
|
+
```ruby
|
296
306
|
ActiveSupport::Notifications.subscribe('http_get.remote_resource') do |name, _start, _fin, _id, _payload|
|
297
307
|
puts "HTTP_GET #{name}"
|
298
308
|
end
|
309
|
+
```
|
299
310
|
|
300
311
|
## Development
|
301
312
|
|
302
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
313
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
314
|
+
`rake spec` to run the tests. You can also run `bin/console` for an interactive
|
315
|
+
prompt that will allow you to experiment.
|
303
316
|
|
304
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To
|
317
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To
|
318
|
+
release a new version, update the version number in `version.rb`, and then run
|
319
|
+
`bundle exec rake release`, which will create a git tag for the version, push
|
320
|
+
git commits and tags, and push the `.gem` file to
|
321
|
+
[rubygems.org](https://rubygems.org).
|
305
322
|
|
306
323
|
## Contributing
|
307
324
|
|
308
|
-
Bug reports and pull requests are welcome on GitHub at
|
309
|
-
|
325
|
+
Bug reports and pull requests are welcome on GitHub at
|
326
|
+
https://github.com/[USERNAME]/remote_resource. This project is intended to be a
|
327
|
+
safe, welcoming space for collaboration, and contributors are expected to adhere
|
328
|
+
to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
310
329
|
|
311
330
|
## License
|
312
331
|
|
313
|
-
The gem is available as open source under the terms of the
|
314
|
-
|
332
|
+
The gem is available as open source under the terms of the
|
333
|
+
[MIT License](http://opensource.org/licenses/MIT).
|
@@ -29,6 +29,7 @@ module RemoteResource
|
|
29
29
|
|
30
30
|
def define_association_method(method_name, target_class)
|
31
31
|
scope = @options[:scope]
|
32
|
+
scope = ":#{@options[:scope]}" if @options[:scope].is_a?(Symbol)
|
32
33
|
target_class.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
33
34
|
def #{method_name}
|
34
35
|
scope_evaluator = RemoteResource::ScopeEvaluator.new(#{scope})
|
@@ -1,12 +1,18 @@
|
|
1
|
-
require '
|
1
|
+
require 'active_support/core_ext/hash/reverse_merge'
|
2
|
+
|
3
|
+
require 'remote_resource/storage/serializers/marshal_serializer'
|
2
4
|
require 'remote_resource/storage/storage_entry'
|
3
5
|
|
4
6
|
module RemoteResource
|
5
7
|
module Storage
|
6
8
|
class Redis
|
7
|
-
def initialize(redis,
|
9
|
+
def initialize(redis, options = {})
|
8
10
|
@redis = redis
|
9
|
-
@
|
11
|
+
@options = options.reverse_merge(
|
12
|
+
serializer: Serializers::MarshalSerializer.new,
|
13
|
+
expires_in: 1 * (60 * 60 * 24)
|
14
|
+
)
|
15
|
+
@serializer = @options[:serializer]
|
10
16
|
end
|
11
17
|
|
12
18
|
def read_key(key)
|
@@ -20,7 +26,10 @@ module RemoteResource
|
|
20
26
|
storage_entry.to_hash.each_pair do |key, value|
|
21
27
|
write_args.concat([key, @serializer.dump(value)]) unless value.empty?
|
22
28
|
end
|
23
|
-
@redis.
|
29
|
+
@redis.multi do |multi|
|
30
|
+
multi.hmset storage_key, *write_args
|
31
|
+
multi.expire storage_key, @options[:expires_in]
|
32
|
+
end
|
24
33
|
end
|
25
34
|
end
|
26
35
|
end
|
data/lib/remote_resource.rb
CHANGED
@@ -31,4 +31,30 @@ module RemoteResource
|
|
31
31
|
autoload :Bridge
|
32
32
|
autoload :LogSubscriber
|
33
33
|
autoload :ScopeEvaluator
|
34
|
+
|
35
|
+
module Lookup
|
36
|
+
extend ActiveSupport::Autoload
|
37
|
+
|
38
|
+
autoload :Default
|
39
|
+
end
|
40
|
+
|
41
|
+
autoload_under 'storage' do
|
42
|
+
autoload :CacheControl
|
43
|
+
autoload :NullStorageEntry
|
44
|
+
autoload :StorageEntry
|
45
|
+
end
|
46
|
+
|
47
|
+
module Storage
|
48
|
+
extend ActiveSupport::Autoload
|
49
|
+
|
50
|
+
autoload :Memory
|
51
|
+
autoload :Redis
|
52
|
+
autoload :Serializer
|
53
|
+
|
54
|
+
module Serializers
|
55
|
+
extend ActiveSupport::Autoload
|
56
|
+
|
57
|
+
autoload :MarshalSerializer
|
58
|
+
end
|
59
|
+
end
|
34
60
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: remote-resource
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Ewald
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-04-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -114,6 +114,7 @@ files:
|
|
114
114
|
- Rakefile
|
115
115
|
- bin/console
|
116
116
|
- bin/setup
|
117
|
+
- lib/remote-resource.rb
|
117
118
|
- lib/remote_resource.rb
|
118
119
|
- lib/remote_resource/association_builder.rb
|
119
120
|
- lib/remote_resource/attribute_http_client.rb
|
@@ -137,13 +138,11 @@ files:
|
|
137
138
|
- lib/remote_resource/railtie.rb
|
138
139
|
- lib/remote_resource/scope_evaluator.rb
|
139
140
|
- lib/remote_resource/storage/cache_control.rb
|
140
|
-
- lib/remote_resource/storage/db_cache.rb
|
141
|
-
- lib/remote_resource/storage/db_cache_factory.rb
|
142
141
|
- lib/remote_resource/storage/memory.rb
|
143
142
|
- lib/remote_resource/storage/null_storage_entry.rb
|
144
143
|
- lib/remote_resource/storage/redis.rb
|
145
144
|
- lib/remote_resource/storage/serializer.rb
|
146
|
-
- lib/remote_resource/storage/serializers/
|
145
|
+
- lib/remote_resource/storage/serializers/marshal_serializer.rb
|
147
146
|
- lib/remote_resource/storage/storage_entry.rb
|
148
147
|
- lib/remote_resource/version.rb
|
149
148
|
- remote-resource.gemspec
|
@@ -1,36 +0,0 @@
|
|
1
|
-
require 'remote_resource/storage/serializers/marshal'
|
2
|
-
|
3
|
-
module RemoteResource
|
4
|
-
class DBCache
|
5
|
-
ADAPTERS = %i(active_record)
|
6
|
-
|
7
|
-
attr_reader :column_name
|
8
|
-
attr_accessor :target_instance
|
9
|
-
|
10
|
-
def initialize(_adapter, column_name, _serializer = :marshal)
|
11
|
-
@adapter = :active_record
|
12
|
-
@column_name = column_name.to_sym
|
13
|
-
@serializer = Serializers::MarshalSerializer.new
|
14
|
-
end
|
15
|
-
|
16
|
-
# always returns a hash
|
17
|
-
def read_column
|
18
|
-
raw = @target_instance.read_attribute(column_name)
|
19
|
-
raw ? @serializer.load(raw) : {}
|
20
|
-
end
|
21
|
-
|
22
|
-
def write_column(hash)
|
23
|
-
fail ArgumentError 'must be a hash!' unless hash.is_a? Hash
|
24
|
-
raw = @serializer.dump(hash)
|
25
|
-
@target_instance.update_attribute(column_name, raw)
|
26
|
-
end
|
27
|
-
|
28
|
-
def read_key(key)
|
29
|
-
read_column[key.to_sym]
|
30
|
-
end
|
31
|
-
|
32
|
-
def write_key(key, value)
|
33
|
-
write_column(read_column.merge(key.to_sym => value))
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
@@ -1,38 +0,0 @@
|
|
1
|
-
require 'remote_resource/storage/db_cache'
|
2
|
-
|
3
|
-
module RemoteResource
|
4
|
-
class UnsupportedDatabase < StandardError; end
|
5
|
-
|
6
|
-
class DBCacheFactory
|
7
|
-
def initialize(base_class, options)
|
8
|
-
@base_class = base_class
|
9
|
-
@options = options
|
10
|
-
end
|
11
|
-
|
12
|
-
def create_for_class(target_class)
|
13
|
-
db_cache_adapter = db_cache_adapter_for(target_class)
|
14
|
-
if db_cache_adapter
|
15
|
-
column_name = @options[:cache_column] ||
|
16
|
-
default_or_magic_column_name_for(@base_class)
|
17
|
-
DBCache.new(db_cache_adapter, column_name)
|
18
|
-
else
|
19
|
-
fail UnsupportedDatabase
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
private
|
24
|
-
|
25
|
-
def default_or_magic_column_name_for(base_class)
|
26
|
-
"#{base_class.underscore}_cache"
|
27
|
-
end
|
28
|
-
|
29
|
-
def db_cache_adapter_for(klass)
|
30
|
-
DBCache::ADAPTERS.detect do |adapter_name|
|
31
|
-
klass.ancestors.any? do |parent_class|
|
32
|
-
next unless (class_name = parent_class.name)
|
33
|
-
class_name.split('::').first.underscore.to_sym == adapter_name
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|