remote_record 0.7.0 → 0.9.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 +97 -102
- data/lib/remote_record.rb +3 -0
- data/lib/remote_record/base.rb +26 -20
- data/lib/remote_record/collection.rb +53 -0
- data/lib/remote_record/config.rb +13 -2
- data/lib/remote_record/dsl.rb +40 -11
- data/lib/remote_record/reference.rb +0 -99
- 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: a3b327ba09a62e4e8f611fd0cca0fc924069fdd374998a84bbe67a9c9435aa4a
|
4
|
+
data.tar.gz: dc7b5a360c8090219ebf6408940c0ef32a71cb1d06f60987e4a71bd6658d777d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8c7b4e58bbb751dd40c8f4ecea1828f5930ada30bb370f393fdbfa5f3711cea2720fae4d631112126bc70f2b58f6dbcfd8ccf3d46e7e2fb833572185413b8b3b
|
7
|
+
data.tar.gz: 0e8d8988b94f9135be23ca340eea7b4ee4c117eeb82131620695df35d607846f9229b09782c30fd3be885eeb137889c9b57c6bfd093ce335ab4d780d7bd075e6
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-

|
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,53 @@
|
|
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
|
+
record.remote.attrs = response.find do |resource|
|
43
|
+
yield(resource).to_s == record.public_send(@id_field).remote_resource_id
|
44
|
+
end || {}
|
45
|
+
record
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def match_remote_resources_by_id(response)
|
50
|
+
match_remote_resources(response) { |resource| resource['id'] }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
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,107 +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
|
-
no_fetching do
|
44
|
-
remote_record_class.all(&authz_proc).map do |remote_resource|
|
45
|
-
where(remote_resource_id: remote_resource['id']).first_or_initialize.tap do |record|
|
46
|
-
record.attrs = remote_resource
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
def remote_where(params, &authz_proc)
|
53
|
-
no_fetching do
|
54
|
-
remote_record_class.where(params, &authz_proc).map do |remote_resource|
|
55
|
-
where(remote_resource_id: remote_resource['id']).first_or_initialize.tap do |record|
|
56
|
-
record.attrs = remote_resource
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
# rubocop:disable Metrics/BlockLength
|
64
12
|
included do
|
65
13
|
include ActiveSupport::Rescuable
|
66
|
-
attribute :fetching, :boolean, default: -> { fetching }
|
67
|
-
attr_accessor :initial_attrs
|
68
|
-
|
69
|
-
after_initialize do |reference|
|
70
|
-
reference.fetching = false if reference.initial_attrs.present?
|
71
|
-
config = reference.class.remote_record_class.default_config.merge(
|
72
|
-
reference.class.remote_record_config.to_h
|
73
|
-
)
|
74
|
-
reference.instance_variable_set('@remote_record_config', config)
|
75
|
-
reference.instance_variable_set('@instance',
|
76
|
-
@remote_record_config.remote_record_class.new(
|
77
|
-
self, @remote_record_config, reference.initial_attrs.presence || {}
|
78
|
-
))
|
79
|
-
reference.fetch_remote_resource
|
80
|
-
end
|
81
|
-
|
82
|
-
# This doesn't call `super` because it delegates to @instance in all
|
83
|
-
# cases.
|
84
|
-
def method_missing(method_name, *_args, &_block)
|
85
|
-
fetch_remote_resource unless @remote_record_config.memoize
|
86
|
-
|
87
|
-
instance.public_send(method_name)
|
88
|
-
end
|
89
|
-
|
90
|
-
def respond_to_missing?(method_name, _include_private = false)
|
91
|
-
instance.respond_to?(method_name, false)
|
92
|
-
end
|
93
|
-
|
94
|
-
def fetch_remote_resource
|
95
|
-
instance.fetch if fetching
|
96
|
-
rescue Exception => e # rubocop:disable Lint/RescueException
|
97
|
-
rescue_with_handler(e) || raise
|
98
|
-
end
|
99
|
-
|
100
|
-
def fresh
|
101
|
-
instance.fetch
|
102
|
-
self
|
103
|
-
end
|
104
|
-
|
105
|
-
private
|
106
|
-
|
107
|
-
delegate :attrs=, to: :@instance
|
108
|
-
|
109
|
-
def instance
|
110
|
-
@instance ||= @remote_record_config.remote_record_class.new(self, @remote_record_config)
|
111
|
-
end
|
112
14
|
end
|
113
|
-
# rubocop:enable Metrics/BlockLength
|
114
15
|
end
|
115
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.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-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.
|