remote_record 0.2.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50ea0ab82ff4f54ea0a877f85d2077f8e735da22b607d191dee6dad31a592a5c
4
- data.tar.gz: d9a28e40ed1b61915e77d80e544aab33f9acf934c941a6fee3a38d08429928a4
3
+ metadata.gz: d657d0ec1a8c4606cc656d2c557e592637fa50d165cd95e118a8efa3094bc7a7
4
+ data.tar.gz: 6bd9609c2617e49b9701e84cd1011236978233856864791e7126c221228addf3
5
5
  SHA512:
6
- metadata.gz: b1d370cfea855f1144e77ef2baacb4fcff519a00455e865b6beb6d8a280534e59f075b947c2775859a0f3d50d70d8c812463c4ce3d31857a00918a3c0f0c7072
7
- data.tar.gz: 35c957d7a2e7f2a1075e375ca75d9ed2266b399d0c24d7ea83a9248f1ba40fe3f68e2cabd3901d92e302f92987e289c96ebe851672d143eeeb3f369e54908b76
6
+ metadata.gz: 4a115d1e2e29d138e67a70c574d66cb7efca0c1ab239d9f47146d8e157410c68d700c8c4bf2c19db647d45a3831f669984117db4823b16691d53af07245ccec1
7
+ data.tar.gz: 7325785485edb863a7d5c91e10b80b8119c93c1ec8b5ff7b1f570ffd498ac297b4c1ba8e3e86549d19ba654e6a578dc2f705a0470fbc2c19aac9e6f5bf49d488
data/README.md CHANGED
@@ -1,6 +1,20 @@
1
- # RemoteRecord
1
+ ![RemoteRecord: Ready-made remote resource structures.](doc/header.svg)
2
2
 
3
- Ready-made remote resource structures.
3
+ ---
4
+
5
+ ![Remote Record](https://github.com/raisedevs/remote_record/workflows/Remote%20Record/badge.svg)
6
+ [![Gem Version](https://badge.fury.io/rb/remote_record.svg)](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,126 @@ 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` 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.
data/lib/remote_record.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_support/concern'
4
+ require 'active_support/rescuable'
4
5
  require 'remote_record/base'
5
6
  require 'remote_record/class_lookup'
6
7
  require 'remote_record/config'
@@ -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.presence || default_config
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,10 +29,22 @@ 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
33
43
 
44
+ def attrs=(new_attrs)
45
+ @attrs.update(new_attrs)
46
+ end
47
+
34
48
  private
35
49
 
36
50
  def transform(data)
@@ -9,7 +9,9 @@ module RemoteRecord
9
9
  module Reference
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,17 +25,57 @@ 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
+ 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
26
61
  end
27
62
 
28
63
  # rubocop:disable Metrics/BlockLength
29
64
  included do
30
- attr_accessor :fetching
65
+ include ActiveSupport::Rescuable
66
+ attribute :fetching, :boolean, default: -> { fetching }
67
+ attr_accessor :initial_attrs
31
68
 
32
69
  after_initialize do |reference|
70
+ reference.fetching = false if reference.initial_attrs.present?
33
71
  config = reference.class.remote_record_class.default_config.merge(
34
72
  reference.class.remote_record_config.to_h
35
73
  )
36
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
+ ))
37
79
  reference.fetch_remote_resource
38
80
  end
39
81
 
@@ -42,20 +84,17 @@ module RemoteRecord
42
84
  def method_missing(method_name, *_args, &_block)
43
85
  fetch_remote_resource unless @remote_record_config.memoize
44
86
 
45
- @instance.public_send(method_name)
87
+ instance.public_send(method_name)
46
88
  end
47
89
 
48
90
  def respond_to_missing?(method_name, _include_private = false)
49
91
  instance.respond_to?(method_name, false)
50
92
  end
51
93
 
52
- def initialize(fetching: true, **args)
53
- @fetching = fetching
54
- super
55
- end
56
-
57
94
  def fetch_remote_resource
58
- instance.fetch if @fetching
95
+ instance.fetch if fetching
96
+ rescue Exception => e # rubocop:disable Lint/RescueException
97
+ rescue_with_handler(e) || raise
59
98
  end
60
99
 
61
100
  def fresh
@@ -65,6 +104,8 @@ module RemoteRecord
65
104
 
66
105
  private
67
106
 
107
+ delegate :attrs=, to: :@instance
108
+
68
109
  def instance
69
110
  @instance ||= @remote_record_config.remote_record_class.new(self, @remote_record_config)
70
111
  end
@@ -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| [underscore_key(k), convert_hash_keys(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 underscore_key(key)
25
- key.to_s.underscore.to_sym
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RemoteRecord
4
- VERSION = '0.2.0'
4
+ VERSION = '0.7.0'
5
5
  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.2.0
4
+ version: 0.7.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: 2020-12-15 00:00:00.000000000 Z
12
+ date: 2021-02-10 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