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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b7549ee998e94aff531c514ec0b95041d659624a8996cd0966d1769c32ee9e2f
4
- data.tar.gz: 95220700e2ccdd2159f3be5929b0329cf177278cf8e346889deee4f55747fb50
3
+ metadata.gz: 8b0ea2bc5ed71919334a2a22d53f70a38ad06b116c7e76679e76bb0aeeb9ec9c
4
+ data.tar.gz: 4874762ea7e61fc354e99faa8d4cf6e79ba9b1451dcf5eefb029e2c173911b13
5
5
  SHA512:
6
- metadata.gz: 045011fafc339148f07ad89834b86a36b46d1a8fa963a7922628eb5bb927a6e719fb4fa3189ebb6abafff3782141685a242cc77a1e6fec7eb5df7b571fdb7b2e
7
- data.tar.gz: 5a37f46bf4eab3a9aee940447e8e6b34f0692064073333cc228799f46e1164e2e098a96374d11e7e71dc678158e3f78abf5975d6f44c9079e7b78194dead297f
6
+ metadata.gz: 603cbf7ecd366f2e3ba00c0be7d1ef793a484948a4940ddae511ab7e99583620d405cee4068958099dcf44aec52bdcc0df612771fc9b9243db530b643d5f1220
7
+ data.tar.gz: d5edee74efd6afd30db89d237674c5deb6953279636accc3e22b28b95e866f769631dc4a13fa70b30982e8f1422f2707bd9ccac2b0b1bdca2de5824e21e9c732
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,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
@@ -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,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
- @instance.public_send(method_name)
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
- fetch_remote_resource
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| [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.1.2'
4
+ VERSION = '0.6.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.1.2
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: 2020-12-03 00:00:00.000000000 Z
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