remote_record 0.5.0 → 0.8.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 +4 -4
- data/README.md +136 -1
- data/lib/remote_record/base.rb +15 -3
- data/lib/remote_record/reference.rb +92 -5
- 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: b0cee90c4d3ebf7acd0d02efebc3285ed4f568e8d42dae1b591a49d6a243bc9c
|
4
|
+
data.tar.gz: b2810727cc559a069b0905f55fb89fe5d3e6d63887cf6a5ef7b4bf8b0b738990
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 86e914f8f66772ae586c17dca8d68e1a0a9b238a11461ff9a302bf8b14f8d9512496a0d0eb516c59746ba2f4dd963314452fe35ff5a16aa6d008016114538e79
|
7
|
+
data.tar.gz: d6c3207d6bf433b98174e1ba87304d6271b8242cdd27960cfe406b71cd16b07e6881371b8c1318d7c5b6be7f9f7917a63dfdc0064ef7db79e9f482eaf41a5056
|
data/README.md
CHANGED
@@ -124,6 +124,105 @@ caching by way of expiry or ETags, I recommend using `faraday-http-cache` for
|
|
124
124
|
your clients and setting `memoize` to `false`. Remote Record will eventually
|
125
125
|
gain support for caching.
|
126
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
|
+
|
127
226
|
### Forcing a fresh request
|
128
227
|
|
129
228
|
You might want to force a fresh request in some instances, even if you're using
|
@@ -131,4 +230,40 @@ You might want to force a fresh request in some instances, even if you're using
|
|
131
230
|
|
132
231
|
### Skip fetching
|
133
232
|
|
134
|
-
You might not want to make a request on initialize sometimes. In this case, pass
|
233
|
+
You might not want to make a request on initialize sometimes. In this case, pass
|
234
|
+
`fetching: false` when creating or initializing references to make sure the
|
235
|
+
resource isn't fetched.
|
236
|
+
|
237
|
+
When querying for records using ActiveRecord alone, you might want to do so
|
238
|
+
within a `no_fetching` context:
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
TodoReference.no_fetching { |model| model.where(remote_resource_id: 1) }
|
242
|
+
```
|
243
|
+
|
244
|
+
Any records initialized within a `no_fetching` context won't be requested. It's
|
245
|
+
sort of like a `Faraday` cage, pun entirely intended.
|
246
|
+
|
247
|
+
If you're using `remote_all` or `remote_where` to fetch using your API, that'll
|
248
|
+
automatically use this behind the scenes, then set `attrs` to the response
|
249
|
+
value.
|
250
|
+
|
251
|
+
### Finding a record without having its canonical ID
|
252
|
+
|
253
|
+
On many platforms, you might find yourself searching for users by email or
|
254
|
+
username. Those aren't canonical IDs - they could change. But searching for them
|
255
|
+
by either of those things is a safe bet as a user, nine times out of ten.
|
256
|
+
|
257
|
+
Similarly, you (or your users) might not always have a remote resource's ID
|
258
|
+
upfront. You might, however, have something unique enough to discern it from
|
259
|
+
other records, like a user-facing ID. A good example is a pull request reference
|
260
|
+
on GitHub - using the repo name, owner's username, and pull request ID, you can
|
261
|
+
find a pull request.
|
262
|
+
|
263
|
+
Of course, that shouldn't be your canonical source, because two of those things
|
264
|
+
could change. You could change your username, and you could rename the repo. But
|
265
|
+
it's useful to be able to search by those things, right?
|
266
|
+
|
267
|
+
Implement `find_by` on your remote_record class, and RemoteRecord will use it.
|
268
|
+
If you don't, RemoteRecord will fall back to `remote_where`. This takes the same
|
269
|
+
params as other class-level RemoteRecord methods, including an auth proc.
|
data/lib/remote_record/base.rb
CHANGED
@@ -9,10 +9,10 @@ module RemoteRecord
|
|
9
9
|
Config.defaults.merge(remote_record_class: self)
|
10
10
|
end
|
11
11
|
|
12
|
-
def initialize(reference, options)
|
12
|
+
def initialize(reference, options = default_config, initial_attrs = {})
|
13
13
|
@reference = reference
|
14
|
-
@options = options
|
15
|
-
@attrs = HashWithIndifferentAccess.new
|
14
|
+
@options = options
|
15
|
+
@attrs = HashWithIndifferentAccess.new(initial_attrs)
|
16
16
|
end
|
17
17
|
|
18
18
|
def method_missing(method_name, *_args, &_block)
|
@@ -29,10 +29,22 @@ module RemoteRecord
|
|
29
29
|
raise NotImplementedError.new, '#get should return a hash of data that represents the remote record.'
|
30
30
|
end
|
31
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
|
+
|
32
40
|
def fetch
|
33
41
|
@attrs.update(get)
|
34
42
|
end
|
35
43
|
|
44
|
+
def attrs=(new_attrs)
|
45
|
+
@attrs.update(new_attrs)
|
46
|
+
end
|
47
|
+
|
36
48
|
private
|
37
49
|
|
38
50
|
def transform(data)
|
@@ -6,10 +6,12 @@ module RemoteRecord
|
|
6
6
|
# record class (a descendant of RemoteRecord::Base). This is done on
|
7
7
|
# initialize by calling #get on an instance of the remote record class. These
|
8
8
|
# attributes are then accessible on the reference thanks to #method_missing.
|
9
|
-
module Reference
|
9
|
+
module Reference # rubocop:disable Metrics/ModuleLength
|
10
10
|
extend ActiveSupport::Concern
|
11
11
|
|
12
|
-
class_methods do
|
12
|
+
class_methods do # rubocop:disable Metrics/BlockLength
|
13
|
+
attr_accessor :fetching
|
14
|
+
|
13
15
|
def remote_record_class
|
14
16
|
ClassLookup.new(self).remote_record_class(
|
15
17
|
remote_record_config.to_h[:remote_record_class]&.to_s
|
@@ -23,19 +25,104 @@ module RemoteRecord
|
|
23
25
|
def remote_record_config
|
24
26
|
Config.new
|
25
27
|
end
|
28
|
+
|
29
|
+
def fetching
|
30
|
+
@fetching = true if @fetching.nil?
|
31
|
+
@fetching
|
32
|
+
end
|
33
|
+
|
34
|
+
# Disable fetching for all records initialized in the block.
|
35
|
+
def no_fetching
|
36
|
+
self.fetching = false
|
37
|
+
block_return_value = yield(self)
|
38
|
+
self.fetching = true
|
39
|
+
block_return_value
|
40
|
+
end
|
41
|
+
|
42
|
+
def remote_all(&authz_proc)
|
43
|
+
find_or_initialize_all(remote_record_class.all(&authz_proc))
|
44
|
+
end
|
45
|
+
|
46
|
+
def remote_where(params, &authz_proc)
|
47
|
+
find_or_initialize_all(remote_record_class.where(params, &authz_proc))
|
48
|
+
end
|
49
|
+
|
50
|
+
def remote_find_by(params, &authz_proc)
|
51
|
+
return remote_where(params, &authz_proc).first unless remote_record_class.respond_to?(:find_by)
|
52
|
+
|
53
|
+
resource = remote_record_class.find_by(params, &authz_proc)
|
54
|
+
new(remote_resource_id: resource['id'], initial_attrs: resource)
|
55
|
+
end
|
56
|
+
|
57
|
+
def remote_find_or_initialize_by(params, &authz_proc)
|
58
|
+
return remote_where(params, &authz_proc).first unless remote_record_class.respond_to?(:find_by)
|
59
|
+
|
60
|
+
resource = remote_record_class.find_by(params, &authz_proc)
|
61
|
+
find_or_initialize_one(id: resource['id'], initial_attrs: resource)
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def find_or_initialize_one(id:, initial_attrs:)
|
67
|
+
existing_record = no_fetching { find_by(remote_resource_id: id) }
|
68
|
+
return existing_record.tap { |r| r.attrs = initial_attrs } if existing_record.present?
|
69
|
+
|
70
|
+
new(remote_resource_id: id, initial_attrs: initial_attrs)
|
71
|
+
end
|
72
|
+
|
73
|
+
def find_or_initialize_all(remote_resources)
|
74
|
+
no_fetching do
|
75
|
+
pair_remote_resources_with_records(remote_resources) do |unsaved_resources, relation|
|
76
|
+
new_resources = unsaved_resources.map do |resource|
|
77
|
+
new(remote_resource_id: resource['id']).tap { |record| record.attrs = resource }
|
78
|
+
end
|
79
|
+
relation.to_a + new_resources
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def pair_remote_resources_with_records(remote_resources)
|
85
|
+
# get resource ids
|
86
|
+
ids = remote_resource_ids(remote_resources)
|
87
|
+
# get what exists in the database
|
88
|
+
relation = where(remote_resource_id: ids)
|
89
|
+
# for each record, set its attrs
|
90
|
+
relation.map do |record|
|
91
|
+
record.attrs = remote_resources.find do |r|
|
92
|
+
r['id'].to_s == record.remote_resource_id.to_s
|
93
|
+
end
|
94
|
+
end
|
95
|
+
unsaved_resources = resources_without_persisted_references(remote_resources, relation)
|
96
|
+
yield(unsaved_resources, relation)
|
97
|
+
end
|
98
|
+
|
99
|
+
def remote_resource_ids(remote_resources)
|
100
|
+
remote_resources.map { |remote_resource| remote_resource['id'] }
|
101
|
+
end
|
102
|
+
|
103
|
+
def resources_without_persisted_references(remote_resources, relation)
|
104
|
+
remote_resources.reject do |resource|
|
105
|
+
relation.pluck(:remote_resource_id).include? resource['id']
|
106
|
+
end
|
107
|
+
end
|
26
108
|
end
|
27
109
|
|
28
110
|
# rubocop:disable Metrics/BlockLength
|
29
111
|
included do
|
30
112
|
include ActiveSupport::Rescuable
|
31
|
-
|
113
|
+
attribute :fetching, :boolean, default: -> { fetching }
|
114
|
+
attr_accessor :initial_attrs
|
32
115
|
|
33
116
|
after_initialize do |reference|
|
34
|
-
reference.fetching =
|
117
|
+
reference.fetching = false if reference.initial_attrs.present?
|
35
118
|
config = reference.class.remote_record_class.default_config.merge(
|
36
119
|
reference.class.remote_record_config.to_h
|
37
120
|
)
|
38
121
|
reference.instance_variable_set('@remote_record_config', config)
|
122
|
+
reference.instance_variable_set('@instance',
|
123
|
+
@remote_record_config.remote_record_class.new(
|
124
|
+
self, @remote_record_config, reference.initial_attrs.presence || {}
|
125
|
+
))
|
39
126
|
reference.fetch_remote_resource
|
40
127
|
end
|
41
128
|
|
@@ -62,7 +149,7 @@ module RemoteRecord
|
|
62
149
|
self
|
63
150
|
end
|
64
151
|
|
65
|
-
|
152
|
+
delegate :attrs=, to: :@instance
|
66
153
|
|
67
154
|
def instance
|
68
155
|
@instance ||= @remote_record_config.remote_record_class.new(self, @remote_record_config)
|
@@ -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.8.1
|
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: 2021-
|
12
|
+
date: 2021-02-24 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
|