remote_record 0.1.2 → 0.6.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 +4 -4
- data/README.md +121 -2
- data/lib/remote_record.rb +1 -0
- data/lib/remote_record/base.rb +13 -3
- data/lib/remote_record/reference.rb +27 -8
- data/lib/remote_record/transformers/base.rb +4 -1
- data/lib/remote_record/transformers/snake_case.rb +8 -3
- data/lib/remote_record/version.rb +1 -1
- metadata +2 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8b0ea2bc5ed71919334a2a22d53f70a38ad06b116c7e76679e76bb0aeeb9ec9c
|
4
|
+
data.tar.gz: 4874762ea7e61fc354e99faa8d4cf6e79ba9b1451dcf5eefb029e2c173911b13
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 603cbf7ecd366f2e3ba00c0be7d1ef793a484948a4940ddae511ab7e99583620d405cee4068958099dcf44aec52bdcc0df612771fc9b9243db530b643d5f1220
|
7
|
+
data.tar.gz: d5edee74efd6afd30db89d237674c5deb6953279636accc3e22b28b95e866f769631dc4a13fa70b30982e8f1422f2707bd9ccac2b0b1bdca2de5824e21e9c732
|
data/README.md
CHANGED
@@ -1,6 +1,20 @@
|
|
1
|
-
|
1
|
+

|
2
2
|
|
3
|
-
|
3
|
+
---
|
4
|
+
|
5
|
+

|
6
|
+
[](https://badge.fury.io/rb/remote_record)
|
7
|
+
|
8
|
+
Every API speaks a different language. Maybe it's REST, maybe it's SOAP, maybe
|
9
|
+
it's GraphQL. Maybe it's got its own Ruby client, or maybe you need to roll your
|
10
|
+
own. But what if you could just pretend it existed in your database?
|
11
|
+
|
12
|
+
RemoteRecord provides a consistent ActiveRecord inspired interface for all of
|
13
|
+
your application's APIs. Store remote resources by ID, and RemoteRecord will
|
14
|
+
auto-populate instances of your ActiveRecord model with their attributes from
|
15
|
+
the API. Whether you're dealing with a user on GitHub, a track on Spotify, a
|
16
|
+
place on Google Maps, or a resource on your internal infrastructure, you can use
|
17
|
+
RemoteRecord to wrap fetching it.
|
4
18
|
|
5
19
|
## Setup
|
6
20
|
|
@@ -110,7 +124,112 @@ caching by way of expiry or ETags, I recommend using `faraday-http-cache` for
|
|
110
124
|
your clients and setting `memoize` to `false`. Remote Record will eventually
|
111
125
|
gain support for caching.
|
112
126
|
|
127
|
+
### `remote_all` and `remote_where`
|
128
|
+
|
129
|
+
If you're able to fetch multiple records at once from the API, implement the
|
130
|
+
`self.all` method on your remote record class. This should return an array of
|
131
|
+
hashes that can be used to initialize a set of references.
|
132
|
+
|
133
|
+
This can optionally take a block
|
134
|
+
for authorization - note that it won't use the auth you've configured and that
|
135
|
+
you'll always have to supply that inline. For example:
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
module RemoteRecord
|
139
|
+
module GitHub
|
140
|
+
# :nodoc:
|
141
|
+
class User < RemoteRecord::Base
|
142
|
+
def get
|
143
|
+
client.user(remote_resource_id)
|
144
|
+
end
|
145
|
+
|
146
|
+
def self.all
|
147
|
+
Octokit::Client.new(access_token: yield).users
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
def client
|
153
|
+
Octokit::Client.new(access_token: authorization)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
```
|
159
|
+
|
160
|
+
Now you can call `remote_all` on remote reference classes that use
|
161
|
+
`RemoteRecord::GitHub::User`, like this:
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
GitHub::UserReference.remote_all { GITHUB_PERSONAL_ACCESS_TOKEN }
|
165
|
+
```
|
166
|
+
|
167
|
+
`remote_where` works in the same way, but with a parameter:
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
module RemoteRecord
|
171
|
+
module GitHub
|
172
|
+
# :nodoc:
|
173
|
+
class User < RemoteRecord::Base
|
174
|
+
def get
|
175
|
+
client.user(remote_resource_id)
|
176
|
+
end
|
177
|
+
|
178
|
+
def self.all
|
179
|
+
Octokit::Client.new(access_token: yield).users
|
180
|
+
end
|
181
|
+
|
182
|
+
def self.where(query)
|
183
|
+
Octokit::Client.new(access_token: yield).search_users(query)
|
184
|
+
end
|
185
|
+
|
186
|
+
private
|
187
|
+
|
188
|
+
def client
|
189
|
+
Octokit::Client.new(access_token: authorization)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
```
|
195
|
+
|
196
|
+
Now you can call `remote_where` on remote reference classes that use
|
197
|
+
`RemoteRecord::GitHub::User`, like this:
|
198
|
+
|
199
|
+
```ruby
|
200
|
+
GitHub::UserReference.remote_where('q=tom+repos:%3E42+followers:%3E1000') { GITHUB_PERSONAL_ACCESS_TOKEN }
|
201
|
+
```
|
202
|
+
|
203
|
+
It's recommended that you include something in `self.where` to filter incoming
|
204
|
+
params. Ideally, you want to expose an interface that's as ActiveRecord-like as
|
205
|
+
possible, e.g.:
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
GitHub::UserReference.remote_where(q: 'tom', repos: '>42', followers: '>1000') { GITHUB_PERSONAL_ACCESS_TOKEN }
|
209
|
+
```
|
210
|
+
|
211
|
+
It's recommended that you write a `Transformer` to do this. Check out
|
212
|
+
`RemoteRecord::Transformers::SnakeCase` for an example.
|
213
|
+
|
214
|
+
### `initial_attrs`
|
215
|
+
|
216
|
+
Behind the scenes, `remote_all` initializes references with a set of
|
217
|
+
`initial_attrs`. You can do the same! If you've already fetched the data for an
|
218
|
+
object, just pass it to `new` for your reference class under the
|
219
|
+
`initial_attrs:` keyword parameter, like this:
|
220
|
+
|
221
|
+
```ruby
|
222
|
+
todo = { id: 1, title: 'Hello world' }
|
223
|
+
TodoReference.new(remote_resource_id: todo[:id], initial_attrs: todo)
|
224
|
+
```
|
225
|
+
|
113
226
|
### Forcing a fresh request
|
114
227
|
|
115
228
|
You might want to force a fresh request in some instances, even if you're using
|
116
229
|
`memoize`. To do this, call `fresh` on a reference, and it'll be repopulated.
|
230
|
+
|
231
|
+
### Skip fetching
|
232
|
+
|
233
|
+
You might not want to make a request on initialize sometimes. In this case, pass
|
234
|
+
`fetching: false` to your query or `new` to make sure the resource isn't
|
235
|
+
fetched.
|
data/lib/remote_record.rb
CHANGED
data/lib/remote_record/base.rb
CHANGED
@@ -3,14 +3,16 @@
|
|
3
3
|
module RemoteRecord
|
4
4
|
# Remote record classes should inherit from this class and define #get.
|
5
5
|
class Base
|
6
|
+
include ActiveSupport::Rescuable
|
7
|
+
|
6
8
|
def self.default_config
|
7
9
|
Config.defaults.merge(remote_record_class: self)
|
8
10
|
end
|
9
11
|
|
10
|
-
def initialize(reference, options)
|
12
|
+
def initialize(reference, options = default_config, initial_attrs = {})
|
11
13
|
@reference = reference
|
12
|
-
@options = options
|
13
|
-
@attrs = HashWithIndifferentAccess.new
|
14
|
+
@options = options
|
15
|
+
@attrs = HashWithIndifferentAccess.new(initial_attrs)
|
14
16
|
end
|
15
17
|
|
16
18
|
def method_missing(method_name, *_args, &_block)
|
@@ -27,6 +29,14 @@ module RemoteRecord
|
|
27
29
|
raise NotImplementedError.new, '#get should return a hash of data that represents the remote record.'
|
28
30
|
end
|
29
31
|
|
32
|
+
def self.all
|
33
|
+
raise NotImplementedError.new, '#all should return an array of hashes of data that represent remote records.'
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.where(_params)
|
37
|
+
raise NotImplementedError.new, '#where should return an array of hashes of data that represent remote records.'
|
38
|
+
end
|
39
|
+
|
30
40
|
def fetch
|
31
41
|
@attrs.update(get)
|
32
42
|
end
|
@@ -23,15 +23,37 @@ module RemoteRecord
|
|
23
23
|
def remote_record_config
|
24
24
|
Config.new
|
25
25
|
end
|
26
|
+
|
27
|
+
def remote_all(&authz_proc)
|
28
|
+
remote_record_class.all(&authz_proc).map do |remote_resource|
|
29
|
+
where(remote_resource_id: remote_resource['id']).first_or_initialize(initial_attrs: remote_resource)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def remote_where(params, &authz_proc)
|
34
|
+
remote_record_class.where(params, &authz_proc).map do |remote_resource|
|
35
|
+
where(remote_resource_id: remote_resource['id']).first_or_initialize(initial_attrs: remote_resource)
|
36
|
+
end
|
37
|
+
end
|
26
38
|
end
|
27
39
|
|
28
40
|
# rubocop:disable Metrics/BlockLength
|
29
41
|
included do
|
42
|
+
include ActiveSupport::Rescuable
|
43
|
+
attr_accessor :fetching
|
44
|
+
attr_accessor :initial_attrs
|
45
|
+
|
30
46
|
after_initialize do |reference|
|
47
|
+
reference.fetching = true if reference.fetching.nil?
|
48
|
+
reference.fetching = false if reference.initial_attrs.present?
|
31
49
|
config = reference.class.remote_record_class.default_config.merge(
|
32
50
|
reference.class.remote_record_config.to_h
|
33
51
|
)
|
34
52
|
reference.instance_variable_set('@remote_record_config', config)
|
53
|
+
reference.instance_variable_set('@instance',
|
54
|
+
@remote_record_config.remote_record_class.new(
|
55
|
+
self, @remote_record_config, reference.initial_attrs.presence || {}
|
56
|
+
))
|
35
57
|
reference.fetch_remote_resource
|
36
58
|
end
|
37
59
|
|
@@ -40,24 +62,21 @@ module RemoteRecord
|
|
40
62
|
def method_missing(method_name, *_args, &_block)
|
41
63
|
fetch_remote_resource unless @remote_record_config.memoize
|
42
64
|
|
43
|
-
|
65
|
+
instance.public_send(method_name)
|
44
66
|
end
|
45
67
|
|
46
68
|
def respond_to_missing?(method_name, _include_private = false)
|
47
69
|
instance.respond_to?(method_name, false)
|
48
70
|
end
|
49
71
|
|
50
|
-
def initialize(**args)
|
51
|
-
@attrs = HashWithIndifferentAccess.new
|
52
|
-
super
|
53
|
-
end
|
54
|
-
|
55
72
|
def fetch_remote_resource
|
56
|
-
instance.fetch
|
73
|
+
instance.fetch if fetching
|
74
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
75
|
+
rescue_with_handler(e) || raise
|
57
76
|
end
|
58
77
|
|
59
78
|
def fresh
|
60
|
-
|
79
|
+
instance.fetch
|
61
80
|
self
|
62
81
|
end
|
63
82
|
|
@@ -4,8 +4,11 @@ module RemoteRecord
|
|
4
4
|
module Transformers
|
5
5
|
# Base transformer class. Inherit from this and implement `#transform`.
|
6
6
|
class Base
|
7
|
-
def initialize(data)
|
7
|
+
def initialize(data, direction = :up)
|
8
|
+
raise ArgumentError, 'The direction should be one of :up or :down.' unless %i[up down].include? direction
|
9
|
+
|
8
10
|
@data = data
|
11
|
+
@direction = direction
|
9
12
|
end
|
10
13
|
|
11
14
|
def transform
|
@@ -15,14 +15,19 @@ module RemoteRecord
|
|
15
15
|
when Array
|
16
16
|
value.map { |v| convert_hash_keys(v) }
|
17
17
|
when Hash
|
18
|
-
Hash[value.map { |k, v| [
|
18
|
+
Hash[value.map { |k, v| [transform_key(k), convert_hash_keys(v)] }]
|
19
19
|
else
|
20
20
|
value
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
-
def
|
25
|
-
|
24
|
+
def transform_key(key)
|
25
|
+
case @direction
|
26
|
+
when :up
|
27
|
+
key.to_s.underscore.to_sym
|
28
|
+
when :down
|
29
|
+
key.to_s.camelize(:lower).to_sym
|
30
|
+
end
|
26
31
|
end
|
27
32
|
end
|
28
33
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: remote_record
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Simon Fish
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2021-02-04 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -39,20 +39,6 @@ dependencies:
|
|
39
39
|
- - ">="
|
40
40
|
- !ruby/object:Gem::Version
|
41
41
|
version: '0'
|
42
|
-
- !ruby/object:Gem::Dependency
|
43
|
-
name: database_cleaner
|
44
|
-
requirement: !ruby/object:Gem::Requirement
|
45
|
-
requirements:
|
46
|
-
- - ">="
|
47
|
-
- !ruby/object:Gem::Version
|
48
|
-
version: '0'
|
49
|
-
type: :development
|
50
|
-
prerelease: false
|
51
|
-
version_requirements: !ruby/object:Gem::Requirement
|
52
|
-
requirements:
|
53
|
-
- - ">="
|
54
|
-
- !ruby/object:Gem::Version
|
55
|
-
version: '0'
|
56
42
|
- !ruby/object:Gem::Dependency
|
57
43
|
name: faraday
|
58
44
|
requirement: !ruby/object:Gem::Requirement
|