remote_record 0.7.1 → 0.9.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +97 -102
- data/lib/remote_record.rb +3 -0
- data/lib/remote_record/base.rb +26 -20
- data/lib/remote_record/collection.rb +54 -0
- data/lib/remote_record/config.rb +13 -2
- data/lib/remote_record/dsl.rb +40 -11
- data/lib/remote_record/reference.rb +0 -123
- data/lib/remote_record/type.rb +45 -0
- data/lib/remote_record/version.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ffa98f2c80cd8bb5cf1c297a30e878a09a056daa139d8109a7179d9076c56f21
|
4
|
+
data.tar.gz: df9f87372e8ad4e348db5980acfd7912ed9cbd91fd4c06f18718627b58357c31
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9f522c6b252935a1340de6ea9ebdc6a8825610199a71ecbdfb6cbc4de9202167d34326b239bed5b80f14cca2353572a7af96d7f61a703d4dd746056ed6111e99
|
7
|
+
data.tar.gz: b5875964305b1cad3a96765f5e0e19714032d47c093701c63ededd93e9aaa7389c907fcfc4d7949588fa6f8a6a3103d7cfb5074714de1ddd18cb29d4b59a8d6a
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
![
|
1
|
+
![Remote Record: Ready-made remote resource structures.](doc/header.svg)
|
2
2
|
|
3
3
|
---
|
4
4
|
|
@@ -9,12 +9,12 @@ Every API speaks a different language. Maybe it's REST, maybe it's SOAP, maybe
|
|
9
9
|
it's GraphQL. Maybe it's got its own Ruby client, or maybe you need to roll your
|
10
10
|
own. But what if you could just pretend it existed in your database?
|
11
11
|
|
12
|
-
|
13
|
-
your application's APIs. Store remote resources by ID, and
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
12
|
+
Remote Record provides a consistent Active Record-inspired interface for all of
|
13
|
+
your application's APIs. Store remote resources by ID, and Remote Record will
|
14
|
+
let you access objects containing their attributes from the API. Whether you're
|
15
|
+
dealing with a user on GitHub, a track on Spotify, a place on Google Maps, or a
|
16
|
+
resource on your internal infrastructure, you can use Remote Record to wrap
|
17
|
+
fetching it.
|
18
18
|
|
19
19
|
## Setup
|
20
20
|
|
@@ -31,7 +31,7 @@ remote resource. In this example, it's `RemoteRecord::GitHub::User`.
|
|
31
31
|
|
32
32
|
### Creating a remote record class
|
33
33
|
|
34
|
-
A standard
|
34
|
+
A standard Remote Record class looks like this. It should have a `get` method,
|
35
35
|
which returns a hash of data you'd like to query on the user.
|
36
36
|
|
37
37
|
`RemoteRecord::Base` exposes private methods for the `remote_resource_id` and
|
@@ -46,6 +46,8 @@ module RemoteRecord
|
|
46
46
|
client.user(remote_resource_id)
|
47
47
|
end
|
48
48
|
|
49
|
+
# Implement the Collection class here for fetching multiple records.
|
50
|
+
|
49
51
|
private
|
50
52
|
|
51
53
|
def client
|
@@ -56,10 +58,22 @@ module RemoteRecord
|
|
56
58
|
end
|
57
59
|
```
|
58
60
|
|
61
|
+
These classes can be used in isolation and don't directly depend on Active
|
62
|
+
Record. You can use them outside of the context of Active Record or Rails:
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
RemoteRecord::GitHub::User.new(1)
|
66
|
+
=> <RemoteRecord::GitHub::User attrs={}>
|
67
|
+
```
|
68
|
+
|
69
|
+
If you call `fresh` or try to access an attribute, Remote Record will fetch the
|
70
|
+
resource and put its data in this instance.
|
71
|
+
|
59
72
|
### Creating a remote reference
|
60
73
|
|
61
|
-
To start using your remote record class, `include RemoteRecord` into your
|
62
|
-
you initialize an instance of your class, it'll be
|
74
|
+
To start using your remote record class, `include RemoteRecord` into your
|
75
|
+
reference. Now, whenever you initialize an instance of your class, it'll be
|
76
|
+
fetched.
|
63
77
|
|
64
78
|
Calling `remote_record` in addition to this lets you set some options:
|
65
79
|
|
@@ -69,7 +83,7 @@ Calling `remote_record` in addition to this lets you set some options:
|
|
69
83
|
| id_field | `:remote_resource_id` | The field on the reference that contains the remote resource ID |
|
70
84
|
| authorization | `''` | An object that can be used by the remote record class to authorize a request. This can be a value, or a proc that returns a value that can be used within the remote record class. |
|
71
85
|
| memoize | true | Whether reference instances should memoize the response that populates them |
|
72
|
-
| transform | [] | Whether the response should be put through a transformer (under RemoteRecord::Transformers).
|
86
|
+
| transform | [] | Whether the response should be put through a transformer (under `RemoteRecord::Transformers`). See `lib/remote_record/transformers` for options. |
|
73
87
|
|
74
88
|
```ruby
|
75
89
|
module GitHub
|
@@ -89,161 +103,142 @@ module GitHub
|
|
89
103
|
end
|
90
104
|
```
|
91
105
|
|
92
|
-
If
|
93
|
-
configure it. So at its best,
|
106
|
+
If the default behavior suits you just fine, you don't even need to
|
107
|
+
configure it. So at its best, Remote Record can be as lightweight as:
|
94
108
|
|
95
109
|
```ruby
|
96
110
|
class JsonPlaceholderAPIReference < ApplicationRecord
|
97
111
|
include RemoteRecord
|
98
|
-
|
99
|
-
# remote_record do |c|
|
100
|
-
# c.authorization proc { }
|
101
|
-
# c.id_field :remote_resource_id
|
102
|
-
# c.klass RemoteRecord::JsonPlaceholderAPI, # Inferred from module and class name
|
103
|
-
# c.memoize true
|
104
|
-
# c.transform []
|
105
|
-
# end
|
112
|
+
remote_record
|
106
113
|
end
|
107
114
|
```
|
108
115
|
|
109
116
|
## Usage
|
110
117
|
|
111
|
-
Now you've got
|
118
|
+
Now you've got the basics lined up to start using your remote reference.
|
112
119
|
|
113
|
-
Whenever a `GitHub::UserReference
|
120
|
+
Whenever you call `remote` on a `GitHub::UserReference`:
|
114
121
|
|
115
122
|
```ruby
|
116
|
-
user.github_user_references.first
|
123
|
+
user.github_user_references.first.remote
|
117
124
|
```
|
118
125
|
|
119
|
-
...
|
120
|
-
|
126
|
+
...you'll be able to use the GitHub user's data on an instance of
|
127
|
+
`RemoteRecord::GitHub::User`. You can call methods that return attributes on the
|
128
|
+
user, like `#login` or `#html_url`.
|
129
|
+
|
130
|
+
For services that manage caching by way of expiry or ETags, I recommend using
|
131
|
+
`faraday-http-cache` for your clients and setting `memoize` to `false`. Remote
|
132
|
+
Record may eventually gain native support for caching your records to the
|
133
|
+
database.
|
121
134
|
|
122
|
-
|
123
|
-
caching by way of expiry or ETags, I recommend using `faraday-http-cache` for
|
124
|
-
your clients and setting `memoize` to `false`. Remote Record will eventually
|
125
|
-
gain support for caching.
|
135
|
+
### `remote` scopes
|
126
136
|
|
127
|
-
|
137
|
+
Remote Record also provides extensions to Active Record scopes. You can call
|
138
|
+
`remote` on a scope to fetch all the remote resources at once. By default, this
|
139
|
+
will use a single request per resource, which isn't often optimal.
|
128
140
|
|
129
|
-
|
130
|
-
|
131
|
-
|
141
|
+
Implement the `Collection` class under your remote record class to fetch
|
142
|
+
multiple records from the API in fewer requests. `all` should return an array
|
143
|
+
of references.
|
132
144
|
|
133
|
-
|
134
|
-
|
135
|
-
|
145
|
+
Inheriting from `RemoteRecord::Collection` grants you some convenience methods
|
146
|
+
you can use to pair the remote resources from the response with your existing
|
147
|
+
references. Check out the class file under `lib/remote_record` for more details.
|
136
148
|
|
137
149
|
```ruby
|
138
150
|
module RemoteRecord
|
139
151
|
module GitHub
|
140
152
|
# :nodoc:
|
141
153
|
class User < RemoteRecord::Base
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
+
# ...
|
155
|
+
class Collection < RemoteRecord::Collection
|
156
|
+
def all
|
157
|
+
response = client.all_users
|
158
|
+
match_remote_resources_by_id(response)
|
159
|
+
end
|
160
|
+
|
161
|
+
private
|
162
|
+
|
163
|
+
def client
|
164
|
+
Octokit::Client.new
|
165
|
+
end
|
154
166
|
end
|
155
167
|
end
|
156
168
|
end
|
157
169
|
end
|
158
170
|
```
|
159
171
|
|
160
|
-
Now you
|
161
|
-
`RemoteRecord::GitHub::User`, like this:
|
172
|
+
Now you're ready to fetch all your resources at once:
|
162
173
|
|
163
174
|
```ruby
|
164
|
-
GitHub::UserReference.
|
175
|
+
GitHub::UserReference.remote.all
|
165
176
|
```
|
166
177
|
|
167
|
-
`
|
178
|
+
`remote.where` works in the same way, but with a parameter:
|
168
179
|
|
169
180
|
```ruby
|
170
181
|
module RemoteRecord
|
171
182
|
module GitHub
|
172
183
|
# :nodoc:
|
173
184
|
class User < RemoteRecord::Base
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
185
|
+
# ...
|
186
|
+
class Collection < RemoteRecord::Collection
|
187
|
+
def all
|
188
|
+
response = client.all_users
|
189
|
+
match_remote_resources_by_id(response)
|
190
|
+
end
|
191
|
+
|
192
|
+
def where(query)
|
193
|
+
response = client.search_users(query)
|
194
|
+
match_remote_resources_by_id(response)
|
195
|
+
end
|
196
|
+
|
197
|
+
private
|
198
|
+
|
199
|
+
def client
|
200
|
+
Octokit::Client.new
|
201
|
+
end
|
190
202
|
end
|
191
203
|
end
|
192
204
|
end
|
193
205
|
end
|
194
206
|
```
|
195
207
|
|
196
|
-
Now you can call `
|
208
|
+
Now you can call `remote.where` on remote reference classes that use
|
197
209
|
`RemoteRecord::GitHub::User`, like this:
|
198
210
|
|
199
211
|
```ruby
|
200
|
-
GitHub::UserReference.
|
212
|
+
GitHub::UserReference.remote.where('q=tom+repos:%3E42+followers:%3E1000')
|
201
213
|
```
|
202
214
|
|
203
|
-
|
215
|
+
*Note that the query we're expecting here comes from the Octokit gem. Your API
|
216
|
+
client might have a nicer interface.*
|
217
|
+
|
218
|
+
It's recommended that you include something in `where` to filter incoming
|
204
219
|
params. Ideally, you want to expose an interface that's as ActiveRecord-like as
|
205
220
|
possible, e.g.:
|
206
221
|
|
207
222
|
```ruby
|
208
|
-
GitHub::UserReference.remote_where(q: 'tom', repos: '>42', followers: '>1000')
|
223
|
+
GitHub::UserReference.remote_where(q: 'tom', repos: '>42', followers: '>1000')
|
209
224
|
```
|
210
225
|
|
211
|
-
|
212
|
-
`RemoteRecord::Transformers
|
226
|
+
You can use or write a `Transformer` to do this. Check out the
|
227
|
+
`RemoteRecord::Transformers` module for examples.
|
213
228
|
|
214
229
|
### `initial_attrs`
|
215
230
|
|
216
|
-
Behind the scenes, `
|
217
|
-
|
218
|
-
|
219
|
-
`initial_attrs:` keyword parameter, like this:
|
231
|
+
Behind the scenes, `match_remote_resources` sets the remote instance's `attrs`.
|
232
|
+
You can do the same! If you've already fetched the data for an object, set it
|
233
|
+
via `attrs`, like this:
|
220
234
|
|
221
235
|
```ruby
|
222
236
|
todo = { id: 1, title: 'Hello world' }
|
223
|
-
TodoReference.new(remote_resource_id: todo[:id]
|
237
|
+
todo_reference = TodoReference.new(remote_resource_id: todo[:id])
|
238
|
+
todo_reference.remote.attrs = todo
|
224
239
|
```
|
225
240
|
|
226
241
|
### Forcing a fresh request
|
227
242
|
|
228
|
-
You might want to force a fresh request in some instances
|
229
|
-
`
|
230
|
-
|
231
|
-
### Skip fetching
|
232
|
-
|
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.
|
243
|
+
You might want to force a fresh request in some instances. To do this, call
|
244
|
+
`fresh` on a reference, and it'll be repopulated.
|
data/lib/remote_record.rb
CHANGED
@@ -2,8 +2,11 @@
|
|
2
2
|
|
3
3
|
require 'active_support/concern'
|
4
4
|
require 'active_support/rescuable'
|
5
|
+
require 'active_record/type'
|
6
|
+
require 'remote_record/type'
|
5
7
|
require 'remote_record/base'
|
6
8
|
require 'remote_record/class_lookup'
|
9
|
+
require 'remote_record/collection'
|
7
10
|
require 'remote_record/config'
|
8
11
|
require 'remote_record/dsl'
|
9
12
|
require 'remote_record/reference'
|
data/lib/remote_record/base.rb
CHANGED
@@ -5,17 +5,27 @@ module RemoteRecord
|
|
5
5
|
class Base
|
6
6
|
include ActiveSupport::Rescuable
|
7
7
|
|
8
|
-
|
9
|
-
|
8
|
+
# When you inherit from `Base`, it'll set up an Active Record Type for you
|
9
|
+
# available on its Type constant. It'll also have a Collection.
|
10
|
+
def self.inherited(subclass)
|
11
|
+
subclass.const_set :Type, RemoteRecord::Type.for(subclass)
|
12
|
+
subclass.const_set :Collection, Class.new(RemoteRecord::Collection) unless subclass.const_defined? :Collection
|
13
|
+
super
|
10
14
|
end
|
15
|
+
attr_reader :remote_resource_id
|
16
|
+
attr_accessor :remote_record_config
|
11
17
|
|
12
|
-
def initialize(
|
13
|
-
|
14
|
-
|
18
|
+
def initialize(remote_resource_id,
|
19
|
+
remote_record_config = Config.defaults,
|
20
|
+
initial_attrs = {})
|
21
|
+
@remote_resource_id = remote_resource_id
|
22
|
+
@remote_record_config = remote_record_config
|
15
23
|
@attrs = HashWithIndifferentAccess.new(initial_attrs)
|
24
|
+
@fetched = initial_attrs.present?
|
16
25
|
end
|
17
26
|
|
18
27
|
def method_missing(method_name, *_args, &_block)
|
28
|
+
fetch unless @remote_record_config.memoize && @fetched
|
19
29
|
transform(@attrs).fetch(method_name)
|
20
30
|
rescue KeyError
|
21
31
|
super
|
@@ -29,25 +39,25 @@ module RemoteRecord
|
|
29
39
|
raise NotImplementedError.new, '#get should return a hash of data that represents the remote record.'
|
30
40
|
end
|
31
41
|
|
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
|
-
|
40
42
|
def fetch
|
41
43
|
@attrs.update(get)
|
44
|
+
@fetched = true
|
42
45
|
end
|
43
46
|
|
44
47
|
def attrs=(new_attrs)
|
45
48
|
@attrs.update(new_attrs)
|
46
49
|
end
|
47
50
|
|
51
|
+
def fresh
|
52
|
+
fetch
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
48
56
|
private
|
49
57
|
|
50
58
|
def transform(data)
|
59
|
+
return data unless transformers.any?
|
60
|
+
|
51
61
|
transformers.reduce(data) do |transformed_data, transformer|
|
52
62
|
transformer.new(transformed_data).transform
|
53
63
|
end
|
@@ -55,18 +65,14 @@ module RemoteRecord
|
|
55
65
|
|
56
66
|
# Robots in disguise.
|
57
67
|
def transformers
|
58
|
-
@
|
68
|
+
@remote_record_config.transform.map do |transformer_name|
|
59
69
|
"RemoteRecord::Transformers::#{transformer_name.to_s.camelize}".constantize
|
60
70
|
end
|
61
71
|
end
|
62
72
|
|
63
73
|
def authorization
|
64
|
-
authz = @
|
65
|
-
authz.respond_to?(:call) ? authz.call(@
|
66
|
-
end
|
67
|
-
|
68
|
-
def remote_resource_id
|
69
|
-
@reference.send(@options.id_field)
|
74
|
+
authz = @remote_record_config.authorization
|
75
|
+
authz.respond_to?(:call) ? authz.call(@remote_record_config.authorization_source) : authz
|
70
76
|
end
|
71
77
|
end
|
72
78
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RemoteRecord
|
4
|
+
# Wraps operations on collections of remote references. By calling #remote on
|
5
|
+
# on an ActiveRecord relation, you'll get a RemoteRecord::Collection you can
|
6
|
+
# use to more easily fetch multiple records at once.
|
7
|
+
#
|
8
|
+
# The default implementation is naive and sends a request per object.
|
9
|
+
class Collection
|
10
|
+
delegate :length, to: :@relation
|
11
|
+
|
12
|
+
def initialize(active_record_relation, config = nil, id: :remote_resource_id)
|
13
|
+
@relation = active_record_relation
|
14
|
+
@config = config
|
15
|
+
@id_field = id
|
16
|
+
end
|
17
|
+
|
18
|
+
def all
|
19
|
+
fetch_all_scoped_records(@relation)
|
20
|
+
end
|
21
|
+
|
22
|
+
def where
|
23
|
+
raise NotImplementedError.new,
|
24
|
+
"Implement #where on #{self.class.name} to filter records using the API."
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# Override this to define more succinct ways to request all records at once.
|
30
|
+
# If your API has a search endpoint, you may want to use that. Otherwise,
|
31
|
+
# list all objects and leave it to Remote Record to pick out the ones you
|
32
|
+
# have in your database.
|
33
|
+
def fetch_all_scoped_records(relation)
|
34
|
+
relation.map do |record|
|
35
|
+
record.remote.remote_record_config.merge!(@config)
|
36
|
+
record.tap { |r| r.remote.fresh }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def match_remote_resources(response)
|
41
|
+
@relation.map do |record|
|
42
|
+
prefetched_record = response.find do |resource|
|
43
|
+
yield(resource).to_s == record.public_send(@id_field).remote_resource_id
|
44
|
+
end
|
45
|
+
record.remote.attrs = prefetched_record if prefetched_record.present?
|
46
|
+
record
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def match_remote_resources_by_id(response)
|
51
|
+
match_remote_resources(response) { |resource| resource['id'] }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/remote_record/config.rb
CHANGED
@@ -7,7 +7,7 @@ module RemoteRecord
|
|
7
7
|
# defaults of the remote record class and the overrides set when
|
8
8
|
# `remote_record` is called.
|
9
9
|
class Config
|
10
|
-
OPTIONS = %i[
|
10
|
+
OPTIONS = %i[authorization authorization_source memoize id_field transform].freeze
|
11
11
|
|
12
12
|
def initialize(**options)
|
13
13
|
@options = options
|
@@ -16,6 +16,7 @@ module RemoteRecord
|
|
16
16
|
def self.defaults
|
17
17
|
new(
|
18
18
|
authorization: '',
|
19
|
+
authorization_source: nil,
|
19
20
|
memoize: true,
|
20
21
|
id_field: :remote_resource_id,
|
21
22
|
transform: []
|
@@ -41,9 +42,19 @@ module RemoteRecord
|
|
41
42
|
@options
|
42
43
|
end
|
43
44
|
|
44
|
-
def merge(**overrides)
|
45
|
+
def merge(config = nil, **overrides)
|
46
|
+
@options.yield_self { |options| options.merge(**(config || {}).to_h) }
|
47
|
+
.yield_self { |options| options.merge(**overrides) }
|
48
|
+
end
|
49
|
+
|
50
|
+
def merge!(config = nil, **overrides)
|
51
|
+
@options.merge!(**config.to_h) if config.present?
|
45
52
|
@options.merge!(**overrides)
|
46
53
|
self
|
47
54
|
end
|
55
|
+
|
56
|
+
def ==(other)
|
57
|
+
other.to_h == @options
|
58
|
+
end
|
48
59
|
end
|
49
60
|
end
|
data/lib/remote_record/dsl.rb
CHANGED
@@ -8,12 +8,16 @@ module RemoteRecord
|
|
8
8
|
module DSL
|
9
9
|
extend ActiveSupport::Concern
|
10
10
|
class_methods do
|
11
|
-
def remote_record(remote_record_class: nil)
|
12
|
-
klass =
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
11
|
+
def remote_record(remote_record_class: nil, field: :remote_resource_id)
|
12
|
+
klass = DSLPrivate.lookup_and_validate_class(self, remote_record_class)
|
13
|
+
base_config = RemoteRecord::Config.defaults
|
14
|
+
base_config = yield(base_config) if block_given?
|
15
|
+
# Register the field as an Active Record attribute of the remote record
|
16
|
+
# class's type
|
17
|
+
attribute field, klass::Type[base_config].new
|
18
|
+
|
19
|
+
DSLPrivate.define_remote_scope(self, klass, field)
|
20
|
+
DSLPrivate.define_remote_accessor(self, field)
|
17
21
|
end
|
18
22
|
end
|
19
23
|
end
|
@@ -21,15 +25,40 @@ module RemoteRecord
|
|
21
25
|
# Methods private to the DSL module.
|
22
26
|
module DSLPrivate
|
23
27
|
class << self
|
24
|
-
def
|
25
|
-
klass.
|
28
|
+
def lookup_and_validate_class(klass, override)
|
29
|
+
RemoteRecord::ClassLookup.new(klass).remote_record_class(override).tap do |found_klass|
|
30
|
+
validate_responds_to_get(found_klass)
|
31
|
+
end
|
26
32
|
end
|
27
33
|
|
28
|
-
|
29
|
-
|
30
|
-
|
34
|
+
# Define the #remote scope, which returns a Collection for the given
|
35
|
+
# Remote Record class
|
36
|
+
def define_remote_scope(base, klass, field_name)
|
37
|
+
return if base.respond_to?(:remote)
|
38
|
+
|
39
|
+
base.define_singleton_method(:remote) do |id_field = field_name, config: nil|
|
40
|
+
klass::Collection.new(all, config, id: id_field)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Define the #remote accessor for instances - this uses the Active
|
45
|
+
# Record type, but adds a reference to the parent object into the config
|
46
|
+
# to be used in authorization.
|
47
|
+
def define_remote_accessor(base, field_name)
|
48
|
+
return if base.instance_methods(false).include?(:remote)
|
49
|
+
|
50
|
+
base.define_method(:remote) do |id_field = field_name|
|
51
|
+
self[id_field].tap { |record| record.remote_record_config.merge!(authorization_source: self) }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def validate_responds_to_get(klass)
|
31
56
|
raise NotImplementedError.new, 'The remote record does not implement #get.' unless responds_to_get?(klass)
|
32
57
|
end
|
58
|
+
|
59
|
+
def responds_to_get?(klass)
|
60
|
+
klass.instance_methods(false).include? :get
|
61
|
+
end
|
33
62
|
end
|
34
63
|
end
|
35
64
|
end
|
@@ -9,131 +9,8 @@ module RemoteRecord
|
|
9
9
|
module Reference
|
10
10
|
extend ActiveSupport::Concern
|
11
11
|
|
12
|
-
class_methods do # rubocop:disable Metrics/BlockLength
|
13
|
-
attr_accessor :fetching
|
14
|
-
|
15
|
-
def remote_record_class
|
16
|
-
ClassLookup.new(self).remote_record_class(
|
17
|
-
remote_record_config.to_h[:remote_record_class]&.to_s
|
18
|
-
)
|
19
|
-
end
|
20
|
-
|
21
|
-
# Default to an empty config, which falls back to the remote record
|
22
|
-
# class's default config and leaves the remote record class to be inferred
|
23
|
-
# from the reference class name
|
24
|
-
# This method is overridden using RemoteRecord::DSL#remote_record.
|
25
|
-
def remote_record_config
|
26
|
-
Config.new
|
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
|
-
private
|
51
|
-
|
52
|
-
def find_or_initialize_all(remote_resources)
|
53
|
-
no_fetching do
|
54
|
-
pair_remote_resources_with_records(remote_resources) do |unsaved_resources, relation|
|
55
|
-
new_resources = unsaved_resources.map do |resource|
|
56
|
-
new(remote_resource_id: resource['id']).tap { |record| record.attrs = resource }
|
57
|
-
end
|
58
|
-
relation.to_a + new_resources
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
def pair_remote_resources_with_records(remote_resources)
|
64
|
-
# get resource ids
|
65
|
-
ids = remote_resource_ids(remote_resources)
|
66
|
-
# get what exists in the database
|
67
|
-
relation = where(remote_resource_id: ids)
|
68
|
-
# for each record, set its attrs
|
69
|
-
relation.map do |record|
|
70
|
-
record.attrs = remote_resources.find do |r|
|
71
|
-
r['id'].to_s == record.remote_resource_id.to_s
|
72
|
-
end
|
73
|
-
end
|
74
|
-
unsaved_resources = resources_without_persisted_references(remote_resources, relation)
|
75
|
-
yield(unsaved_resources, relation)
|
76
|
-
end
|
77
|
-
|
78
|
-
def remote_resource_ids(remote_resources)
|
79
|
-
remote_resources.map { |remote_resource| remote_resource['id'] }
|
80
|
-
end
|
81
|
-
|
82
|
-
def resources_without_persisted_references(remote_resources, relation)
|
83
|
-
remote_resources.reject do |resource|
|
84
|
-
relation.pluck(:remote_resource_id).include? resource['id']
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
# rubocop:disable Metrics/BlockLength
|
90
12
|
included do
|
91
13
|
include ActiveSupport::Rescuable
|
92
|
-
attribute :fetching, :boolean, default: -> { fetching }
|
93
|
-
attr_accessor :initial_attrs
|
94
|
-
|
95
|
-
after_initialize do |reference|
|
96
|
-
reference.fetching = false if reference.initial_attrs.present?
|
97
|
-
config = reference.class.remote_record_class.default_config.merge(
|
98
|
-
reference.class.remote_record_config.to_h
|
99
|
-
)
|
100
|
-
reference.instance_variable_set('@remote_record_config', config)
|
101
|
-
reference.instance_variable_set('@instance',
|
102
|
-
@remote_record_config.remote_record_class.new(
|
103
|
-
self, @remote_record_config, reference.initial_attrs.presence || {}
|
104
|
-
))
|
105
|
-
reference.fetch_remote_resource
|
106
|
-
end
|
107
|
-
|
108
|
-
# This doesn't call `super` because it delegates to @instance in all
|
109
|
-
# cases.
|
110
|
-
def method_missing(method_name, *_args, &_block)
|
111
|
-
fetch_remote_resource unless @remote_record_config.memoize
|
112
|
-
|
113
|
-
instance.public_send(method_name)
|
114
|
-
end
|
115
|
-
|
116
|
-
def respond_to_missing?(method_name, _include_private = false)
|
117
|
-
instance.respond_to?(method_name, false)
|
118
|
-
end
|
119
|
-
|
120
|
-
def fetch_remote_resource
|
121
|
-
instance.fetch if fetching
|
122
|
-
rescue Exception => e # rubocop:disable Lint/RescueException
|
123
|
-
rescue_with_handler(e) || raise
|
124
|
-
end
|
125
|
-
|
126
|
-
def fresh
|
127
|
-
instance.fetch
|
128
|
-
self
|
129
|
-
end
|
130
|
-
|
131
|
-
delegate :attrs=, to: :@instance
|
132
|
-
|
133
|
-
def instance
|
134
|
-
@instance ||= @remote_record_config.remote_record_class.new(self, @remote_record_config)
|
135
|
-
end
|
136
14
|
end
|
137
|
-
# rubocop:enable Metrics/BlockLength
|
138
15
|
end
|
139
16
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './config'
|
4
|
+
|
5
|
+
module RemoteRecord
|
6
|
+
# RemoteRecord uses the Active Record Types system to serialize to and from a
|
7
|
+
# remote resource.
|
8
|
+
class Type < ActiveRecord::Type::Value
|
9
|
+
class_attribute :config, default: RemoteRecord::Config.defaults, instance_writer: false, instance_predicate: false
|
10
|
+
class_attribute :parent, instance_writer: false, instance_predicate: false
|
11
|
+
|
12
|
+
def type
|
13
|
+
:string
|
14
|
+
end
|
15
|
+
|
16
|
+
def cast(_remote_resource_id)
|
17
|
+
raise 'cast not defined'
|
18
|
+
end
|
19
|
+
|
20
|
+
def deserialize(value)
|
21
|
+
cast(value)
|
22
|
+
end
|
23
|
+
|
24
|
+
def serialize(representation)
|
25
|
+
return representation.remote_resource_id if representation.respond_to? :remote_resource_id
|
26
|
+
|
27
|
+
representation.to_s
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.for(remote_record_class)
|
31
|
+
Class.new(self) do |type|
|
32
|
+
type.parent = remote_record_class
|
33
|
+
def self.[](config_override)
|
34
|
+
Class.new(self).tap { |configured_type| configured_type.config = config_override }
|
35
|
+
end
|
36
|
+
|
37
|
+
def cast(remote_resource_id)
|
38
|
+
return remote_resource_id if remote_resource_id.is_a?(parent)
|
39
|
+
|
40
|
+
parent.new(remote_resource_id, config)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
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.9.2
|
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-04-21 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -178,12 +178,14 @@ files:
|
|
178
178
|
- lib/remote_record.rb
|
179
179
|
- lib/remote_record/base.rb
|
180
180
|
- lib/remote_record/class_lookup.rb
|
181
|
+
- lib/remote_record/collection.rb
|
181
182
|
- lib/remote_record/config.rb
|
182
183
|
- lib/remote_record/dsl.rb
|
183
184
|
- lib/remote_record/reference.rb
|
184
185
|
- lib/remote_record/transformers.rb
|
185
186
|
- lib/remote_record/transformers/base.rb
|
186
187
|
- lib/remote_record/transformers/snake_case.rb
|
188
|
+
- lib/remote_record/type.rb
|
187
189
|
- lib/remote_record/version.rb
|
188
190
|
homepage: https://github.com/raisedevs/remote_record
|
189
191
|
licenses:
|
@@ -206,7 +208,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
206
208
|
- !ruby/object:Gem::Version
|
207
209
|
version: '0'
|
208
210
|
requirements: []
|
209
|
-
rubygems_version: 3.1.
|
211
|
+
rubygems_version: 3.1.6
|
210
212
|
signing_key:
|
211
213
|
specification_version: 4
|
212
214
|
summary: Ready-made remote resource structures.
|