jahuty 3.0.0 → 3.3.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: f16f42cd41d86fdcdbd27591e637d82c324828f5cf60b3388afd7f1d439f9bee
4
- data.tar.gz: 53ef3b027c56ac0a00a09ab8ba0a10a3794b0d47bab1975ceb64dfbce74a244c
3
+ metadata.gz: 93d706c72d12cb61d951cc71b8add1ac154ed8b8bc57374e3d558e2b4bd8d420
4
+ data.tar.gz: e64a1355073ba942c5aef652032e8268e4d3a065b8cb3f5a7594d590957edc18
5
5
  SHA512:
6
- metadata.gz: 24affb2a47535cd18148a52a51ed5f67120032ff859f54995433c10f1348cc72db3d17017b58e3e90a473678c7639acecc14832fe6b6cea302342baa46bc8821
7
- data.tar.gz: f4429c45c905b4dd9336d1126d4256dc9bdbf0f0c548f52bc7c93a8dc7371882bafdb11b29e18644816b1378a1d4691c7c7c6796c5a1c9a7cd68a6b90ab445a6
6
+ metadata.gz: 33d77db35616e1ae68f3b1ad0063e51ccfc526e35f2ef04e07851f506e5103e73af9d0d27252abf817de4ce2664206cf3edd7f9972275b96aaa582c8c38d0a7b
7
+ data.tar.gz: b572333f5df0d13b18ecbf29f9d22b06400622cdf9ab5408a6963f21fe154ea0fbb67cee881b3efb89cc7417ef1ef1abcd733b0681f7d72261f8ac357e175bc1
data/.rubocop.yml CHANGED
@@ -4,9 +4,20 @@ require:
4
4
  AllCops:
5
5
  TargetRubyVersion: 2.6
6
6
  NewCops: enable
7
+ Exclude:
8
+ - vendor/bundle/**/*
7
9
  Metrics/BlockLength:
8
10
  Exclude:
9
11
  - 'jahuty.gemspec'
10
12
  - 'Rakefile'
11
13
  - '**/*.rake'
12
14
  - 'spec/**/*.rb'
15
+ Metrics/ModuleLength:
16
+ Exclude:
17
+ - 'spec/**/*.rb'
18
+ RSpec/ExampleLength:
19
+ Exclude:
20
+ - 'spec/jahuty/system_spec.rb'
21
+ RSpec/MultipleExpectations:
22
+ Exclude:
23
+ - 'spec/jahuty/system_spec.rb'
data/CHANGELOG.md CHANGED
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## 3.3.0 - 2020-04-29
9
+
10
+ - Add the `prefer_latest` configuration option to the client and render methods. This setting allows you to render a snippet's _latest_ content instead of the default _published_ content.
11
+
12
+ ## 3.2.1 - 2020-03-16
13
+
14
+ * Fix issue with double-enveloping the `params` query string parameter.
15
+ * Add system tests for params, caching, problems, and collections.
16
+
17
+ ## 3.2.0 - 2020-03-08
18
+
19
+ * Added collections to the library with `all_renders` method. This was a rather large change and required adding new objects like `Action::Show`, refactoring old ones like `Resource::Factory`, and removing some objects like `Cache::Manager` and `Service::Factory` which added unnecessary complexity.
20
+ * Added `snippet_id` to `Resource::Render` to help keep track of a render's parent snippet.
21
+
22
+ ## 3.1.1 - 2020-02-26
23
+
24
+ - Add support for extra, unused attributes returned by the API to support evolution.
25
+ - Fix the `method redefined` warnings in `cache/manager_spec.rb` and `cache/facade_spec.rb`.
26
+ - Fix the `expect { }.not_to raise_error(SpecificErrorClass)` false positives warnings in `resource/render_spec.rb`.
27
+
28
+ ## 3.1.0 - 2020-01-04
29
+
30
+ - Add caching support for any cache implementation that supports `get/set` or `read/write` methods.
31
+ - Default to using in-memory [mini-cache](https://github.com/derrickreimer/mini_cache) storage.
32
+
8
33
  ## 3.0.0 - 2020-12-30
9
34
 
10
35
  - Change from a static-based architecture (e.g., `Jahuty::Snippet.render(1)`) to an instance-based one (e.g., `jahuty.snippets.render(1)`) to make the library easier to develop, test, and use.
data/README.md CHANGED
@@ -10,28 +10,28 @@ This library requires [Ruby 2.6+](https://www.ruby-lang.org/en/downloads/release
10
10
 
11
11
  It is multi-platform, and we strive to make it run equally well on Windows, Linux, and OSX.
12
12
 
13
- Add this line to your application's `Gemfile`:
13
+ To install, add this line to your application's `Gemfile` and run `bundle install`:
14
14
 
15
15
  ```ruby
16
- gem 'jahuty', '~> 3.0'
16
+ gem 'jahuty', '~> 3.3'
17
17
  ```
18
18
 
19
19
  ## Usage
20
20
 
21
- Instantiate the client with your [API key](https://docs.jahuty.com/api#authentication) and use `snippets.render()` to render your snippet:
21
+ Instantiate the client with your [API key](https://docs.jahuty.com/api#authentication) and use `snippets.render` to render your snippet:
22
22
 
23
23
  ```ruby
24
24
  jahuty = Jahuty::Client.new(api_key: 'YOUR_API_KEY')
25
25
 
26
- puts jahuty.snippets.render(YOUR_SNIPPET_ID)
26
+ puts jahuty.snippets.render YOUR_SNIPPET_ID
27
27
  ```
28
28
 
29
- You can also access the render's content with `to_s` or `content`:
29
+ You can access the render's content with `to_s` or `content`:
30
30
 
31
31
  ```ruby
32
32
  jahuty = Jahuty::Client.new(api_key: 'YOUR_API_KEY')
33
33
 
34
- render = jahuty.snippets.render(YOUR_SNIPPET_ID)
34
+ render = jahuty.snippets.render YOUR_SNIPPET_ID
35
35
 
36
36
  a = render.to_s
37
37
 
@@ -43,42 +43,216 @@ a == b # returns true
43
43
  In an HTML view:
44
44
 
45
45
  ```html+erb
46
- <%-
47
- jahuty = Jahuty::Client.new(api_key: 'YOUR_API_KEY')
48
- %>
46
+ <%- jahuty = Jahuty::Client.new(api_key: 'YOUR_API_KEY') -%>
49
47
  <!doctype html>
50
48
  <html>
51
49
  <head>
52
50
  <title>Awesome example</title>
53
51
  </head>
54
52
  <body>
55
- <%= jahuty.snippets.render YOUR_SNIPPET_ID %>
53
+ <%== jahuty.snippets.render YOUR_SNIPPET_ID %>
56
54
  </body>
57
55
  ```
58
56
 
59
- ## Parameters
57
+ You can also use tags to render a collection of snippets with the `snippets.all_renders` method:
58
+
59
+ ```ruby
60
+ jahuty = Jahuty::Client.new(api_key: 'YOUR_API_KEY')
61
+
62
+ renders = jahuty.snippets.all_renders 'YOUR_TAG'
63
+
64
+ renders.each { |render| puts render }
65
+ ```
66
+
67
+ ## Rendering content
68
+
69
+ You can use the `prefer_latest` configuration option to render a snippet's _latest_ content to your team in _development_ and its _published_ content to your customers in _production_.
60
70
 
61
- You can [pass parameters](https://docs.jahuty.com/liquid/parameters) into your snippet using the `params` option:
71
+ By default, Jahuty will render a snippet's _published_ content, the content that existed the last time someone clicked the "Publish" button, to avoid exposing your creative process to customers.
72
+
73
+ To render a snippet's _latest_ content, the content that currently exists in the editor, you can use the `prefer_latest` configuration option at the library or render level:
74
+
75
+ ```ruby
76
+ jahuty = Jahuty::Client.new api_key: 'YOUR_API_KEY', prefer_latest: true
77
+ ```
78
+
79
+ You can also prefer the latest content (or not) for a single render:
80
+
81
+ ```ruby
82
+ # Render the _published_ content for all snippets...
83
+ jahuty = Jahuty::Client.new api_key: 'YOUR_API_KEY'
84
+
85
+ # ... except, render the _latest_ content for this one.
86
+ jahuty.snippets.render YOUR_SNIPPET_ID, prefer_latest: true
87
+ ```
88
+
89
+ ## Passing dynamic parameters
90
+
91
+ You can use the _same_ snippet to generate _different_ content by defining [variables](https://docs.jahuty.com/liquid/variables) in your snippets and setting their values via [parameters](https://docs.jahuty.com/liquid/parameters).
92
+
93
+ ### Snippet parameters
94
+
95
+ Use the `params` option to pass parameters into your snippet:
62
96
 
63
97
  ```ruby
64
98
  jahuty = Jahuty::Client.new(api_key: 'YOUR_API_KEY')
65
99
 
66
- jahuty.snippets.render(YOUR_SNIPPET_ID, params: { foo: 'bar' });
100
+ jahuty.snippets.render YOUR_SNIPPET_ID, params: { foo: 'bar' }
67
101
  ```
68
102
 
69
- The parameters above would be equivalent to [assigning the variable](https://docs.jahuty.com/liquid/variables) below in your snippet:
103
+ The parameters above would be equivalent to assigning the following variable in your snippet:
70
104
 
71
105
  ```html
72
106
  {% assign foo = "bar" %}
73
107
  ```
74
108
 
75
- ## Errors
109
+ ### Collection parameters
76
110
 
77
- If an error occurs with [Jahuty's API](https://docs.jahuty.com/api#errors), a `Jahuty::Exception::Error` will be raised:
111
+ Collection parameters use a slightly different syntax.
112
+
113
+ If you're rendering a collection, the first dimension of the `params` key determines the parameters' scope. Use an asterisk key (`*`) to pass the same parameters to all snippets, or use a snippet id as key to pass parameters to a specific snippet.
114
+
115
+ ```ruby
116
+ jahuty = Jahuty::Client.new(api_key: 'YOUR_API_KEY')
117
+
118
+ jahuty.snippets.all_renders 'YOUR_TAG', params: {
119
+ '*' => { foo: 'bar' },
120
+ '1' => { baz: 'qux' }
121
+ }
122
+ ```
123
+
124
+ This will pass the params `{ foo: 'bar' }` to all snippets, except for snippet `1`, which will be passed `{ foo: 'bar', baz: 'qux' }`.
125
+
126
+ The two parameter lists will be merged recursively, and parameters for a specific snippet will take precedence over parameters for all snippets. For example, the parameter `foo` will be assigned the value `"bar"` for all snippets, except for snippet `1`, where it will be assigned the value `"qux"`:
78
127
 
79
128
  ```ruby
80
- require 'jahuty'
129
+ jahuty = Jahuty::Client.new(api_key: 'YOUR_API_KEY')
130
+
131
+ jahuty.snippets.all_renders 'YOUR_TAG', params: {
132
+ '*' => { foo: 'bar' },
133
+ '1' => { foo: 'qux' }
134
+ }
135
+ ```
136
+
137
+ ## Caching for performance
138
+
139
+ You can use caching to control how frequently this library requests the latest content from Jahuty's API.
140
+
141
+ * When content is in _development_ (i.e., frequently changing and low traffic), you can use the default in-memory store to view content changes instantaneously with slower response times.
142
+ * When content is in _production_ (i.e., more stable and high traffic), you can use persistent caching to update content less frequently and improve your application's response time.
143
+
144
+ ### Caching in memory (default)
145
+
146
+ By default, this library uses an in-memory cache to avoid requesting the same render more than once during the same request lifecycle. For example:
147
+
148
+ ```ruby
149
+ jahuty = Jahuty::Client.new(api_key: 'YOUR_API_KEY')
150
+
151
+ # This call will send a synchronous API request; cache the result in memory;
152
+ # and, return the result to the caller.
153
+ render1 = jahuty.snippets.render YOUR_SNIPPET_ID
154
+
155
+ # This call skips sending an API request and uses the cached value instead.
156
+ render2 = jahuty.snippets.render YOUR_SNIPPET_ID
157
+ ```
158
+
159
+ The in-memory cache only persists for the duration of the original request, however. At the end of the request's lifecycle, the cache will be discarded. To store renders across requests, you need a persistent cache.
81
160
 
161
+ ### Caching persistently
162
+
163
+ A persistent cache allows renders to be cached across multiple requests. This reduces the number of synchronous network requests to Jahuty's API and improves your application's average response time.
164
+
165
+ To configure Jahuty to use your persistent cache, pass a cache implementation to the client via the `cache` configuration option:
166
+
167
+ ```ruby
168
+ jahuty = new Jahuty::Client.new(
169
+ api_key: 'YOUR_API_KEY',
170
+ cache: cache
171
+ )
172
+ ```
173
+
174
+ The persistent cache implementation you choose and configure is up to you. There are many libraries available, and most frameworks provide their own. At this time, we support any object which responds to `get(key)`/`set(key, value, expires_in:)` or `read(key)`/`write(key, value, expires_in:)` including [ActiveSupport::Cache::Store](https://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html#method-i-fetch).
175
+
176
+ ### Expiring
177
+
178
+ There are three methods for configuring this library's `:expires_in`, the amount of time between when a render is stored and when it's considered stale. From lowest-to-highest precedence, the methods are:
179
+
180
+ 1. configuring your caching implementation,
181
+ 1. configuring this library's default `:expires_in`, and
182
+ 1. configuring a render's `:expires_in`.
183
+
184
+ #### Configuring your caching implementation
185
+
186
+ You can usually configure your caching implementation with a default `:expires_in`. If no other `:expires_in` is configured, this library will defer to the caching implementation's default `:expires_in`.
187
+
188
+ #### Configuring this library's default `:expires_in`
189
+
190
+ You can configure a default `:expires_in` for all of this library's renders by passing an integer number of seconds via the client's `:expires_in` configuration option:
191
+
192
+ ```ruby
193
+ jahuty = Jahuty::Client.new(
194
+ api_key: 'YOUR_API_KEY',
195
+ cache: cache,
196
+ expires_in: 60 # <- Cache all renders for sixty seconds
197
+ )
198
+ ```
199
+
200
+ If this library's default `:expires_in` is set, it will take precedence over the default `:expires_in` of the caching implementation.
201
+
202
+ #### Configuring a render's `:expires_in`
203
+
204
+ You can configure `:expires_in` for individual renders by passing an integer number of seconds via the render method's `:expires_in` configuration option:
205
+
206
+ ```ruby
207
+ # Cache all renders 300 seconds (five minutes).
208
+ jahuty = Jahuty::Client.new(api_key: 'YOUR_API_KEY', cache: cache, expires_in: 300)
209
+
210
+ # Except, cache this render for 60 seconds.
211
+ render = jahuty.snippets.render YOUR_SNIPPET_ID, expires_in: 60
212
+
213
+ # Except, cache the renders in this collection for 120 seconds.
214
+ render = jahuty.snippets.all_renders 'YOUR_TAG', expires_in: 120
215
+ ```
216
+
217
+ If a render's `:expires_in` is set, it will take precedence over the library's default `:expires_in` and the caching implementation's `:expires_in`.
218
+
219
+ ### Caching collections
220
+
221
+ By default, this library will cache each render returned by `all_renders`:
222
+
223
+ ```ruby
224
+ jahuty = Jahuty::Client.new(api_key: 'YOUR_API_KEY', cache: cache)
225
+
226
+ # Sends a network request, caches each render, and returns the collection.
227
+ jahuty.snippets.all_renders 'YOUR_TAG';
228
+
229
+ # If this reder exists in the collection, the cached value will be used instead
230
+ # of sending a network request for the latest version.
231
+ jahuty.snippets.render YOUR_SNIPPET_ID;
232
+ ```
233
+
234
+ This is a powerful feature, especially when combined with a persistent cache. Using the `all_renders` method, you can render and cache an arbitrarily large chunk of content with a single network request. Because any subsequent call to `render` a snippet in the collection will use its cached version, you can reduce the number of network requests to load your content.
235
+
236
+ This method is even more powerful when combined with an asynchronous background job. When `all_renders` can be called outside your request cycle periodically, you can turn your cache into your content storage mechanism. You can render and cache dynamic content as frequently as you like without any hit to your application's response time.
237
+
238
+ ### Disabling caching
239
+
240
+ You can disable caching, even the default in-memory caching, by passing an `:expires_in` of zero (`0`) or a negative integer (e.g., `-1`) via any of the methods described above. For example:
241
+
242
+ ```ruby
243
+ # Disable all caching.
244
+ jahuty1 = Jahuty::Client.new(api_key: 'YOUR_API_KEY', expires_in: 0)
245
+
246
+ # Disable caching for this render.
247
+ jahuty2 = Jahuty::Client.new(api_key: 'YOUR_API_KEY', expires_in: 60)
248
+ jahuty2.snippets.render 1, expires_in: 0
249
+ ```
250
+
251
+ ## Handling errors
252
+
253
+ If an error occurs with [Jahuty's API](https://docs.jahuty.com/api#errors), a `Jahuty::Exception::Error` will be raised:
254
+
255
+ ```ruby
82
256
  begin
83
257
  jahuty = Jahuty::Client.new(api_key: 'YOUR_API_KEY')
84
258
  jahuty.snippets.render YOUR_SNIPPET_ID
data/jahuty.gemspec CHANGED
@@ -32,6 +32,7 @@ Gem::Specification.new do |spec|
32
32
  spec.required_ruby_version = '~> 2.6'
33
33
 
34
34
  spec.add_dependency 'faraday', '~> 1.0'
35
+ spec.add_dependency 'mini_cache', '~> 1.1'
35
36
 
36
37
  spec.add_development_dependency 'bundler', '~> 2.0'
37
38
  spec.add_development_dependency 'rake', '~> 12.3'
data/lib/jahuty.rb CHANGED
@@ -3,10 +3,13 @@
3
3
  require 'jahuty/version'
4
4
 
5
5
  require 'jahuty/action/base'
6
+ require 'jahuty/action/index'
6
7
  require 'jahuty/action/show'
7
8
 
8
9
  require 'jahuty/api/client'
9
10
 
11
+ require 'jahuty/cache/facade'
12
+
10
13
  require 'jahuty/exception/error'
11
14
 
12
15
  require 'jahuty/request/base'
@@ -16,12 +19,15 @@ require 'jahuty/resource/problem'
16
19
  require 'jahuty/resource/render'
17
20
  require 'jahuty/resource/factory'
18
21
 
22
+ require 'jahuty/response/handler'
23
+
19
24
  require 'jahuty/service/base'
20
25
  require 'jahuty/service/snippet'
21
- require 'jahuty/service/factory'
22
26
 
23
27
  require 'jahuty/client'
24
28
 
29
+ require 'jahuty/util'
30
+
25
31
  module Jahuty
26
32
  BASE_URI = 'https://api.jahuty.com'
27
33
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jahuty
4
+ module Action
5
+ # Displays a collection of resources.
6
+ class Index < Base
7
+ end
8
+ end
9
+ end
@@ -20,10 +20,11 @@ module Jahuty
20
20
  def send(request)
21
21
  @client ||= Faraday.new(url: ::Jahuty::BASE_URI, headers: headers)
22
22
 
23
+ # Cnvert the action's string method to Faraday's verb-based methods.
23
24
  @client.send(
24
25
  request.method.to_sym,
25
26
  request.path,
26
- { params: request.params }
27
+ request.params
27
28
  )
28
29
  end
29
30
 
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jahuty
4
+ module Cache
5
+ # Abstracts away the differences in cache implementation methods and
6
+ # argument lists.
7
+ class Facade
8
+ def initialize(cache)
9
+ @cache = cache
10
+ end
11
+
12
+ def delete(key)
13
+ if @cache.respond_to? :delete
14
+ @cache.delete key
15
+ elsif @cache.respond_to? :unset
16
+ @cache.unset key
17
+ else
18
+ raise NoMethodError, 'Cache must respond to :delete or :unset'
19
+ end
20
+ end
21
+
22
+ def read(key)
23
+ if @cache.respond_to? :read
24
+ @cache.read key
25
+ elsif @cache.respond_to? :get
26
+ @cache.get key
27
+ else
28
+ raise NoMethodError, 'Cache must respond to :read or :get'
29
+ end
30
+ end
31
+
32
+ def write(key, value, expires_in: nil)
33
+ if Object.const_defined?('::ActiveSupport::Cache::Store') &&
34
+ @cache.is_a?(::ActiveSupport::Cache::Store)
35
+ @cache.write key, value, expires_in: expires_in, race_condition_ttl: 10
36
+ elsif @cache.respond_to? :write
37
+ @cache.write key, value, expires_in: expires_in
38
+ elsif @cache.respond_to? :set
39
+ @cache.set key, value, expires_in: expires_in
40
+ else
41
+ raise NoMethodError, 'Cache must respond to :write or :set'
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
data/lib/jahuty/client.rb CHANGED
@@ -1,17 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'mini_cache'
4
+
3
5
  module Jahuty
4
6
  # Executes requests against Jahuty's API and returns resources.
5
7
  class Client
6
- def initialize(api_key:)
7
- @api_key = api_key
8
- @services = Service::Factory.new(client: self)
8
+ def initialize(api_key:, cache: nil, expires_in: nil, prefer_latest: false)
9
+ @api_key = api_key
10
+ @cache = Cache::Facade.new(cache || ::MiniCache::Store.new)
11
+ @expires_in = expires_in
12
+ @services = {}
13
+ @prefer_latest = prefer_latest
9
14
  end
10
15
 
11
- # Allows services to appear as properties (e.g., jahuty.snippets).
16
+ # Allows services to be accessed as properties (e.g., jahuty.snippets).
12
17
  def method_missing(name, *args, &block)
13
- if args.empty? && @services.respond_to?(name)
14
- @services.send(name)
18
+ if args.empty? && name == :snippets
19
+ unless @services.key?(name)
20
+ @services[name] = Service::Snippet.new(
21
+ client: self, cache: @cache, expires_in: @expires_in, prefer_latest: @prefer_latest
22
+ )
23
+ end
24
+ @services[name]
15
25
  else
16
26
  super
17
27
  end
@@ -26,17 +36,17 @@ module Jahuty
26
36
 
27
37
  response = @client.send(request)
28
38
 
29
- @resources ||= Resource::Factory.new
39
+ @responses ||= Response::Handler.new
30
40
 
31
- resource = @resources.call(action, response)
41
+ result = @responses.call(action, response)
32
42
 
33
- raise Exception::Error.new(resource), 'API responded with a problem' if resource.is_a?(Resource::Problem)
43
+ raise Exception::Error.new(result), 'API problem' if result.is_a?(Resource::Problem)
34
44
 
35
- resource
45
+ result
36
46
  end
37
47
 
38
48
  def respond_to_missing?(name, include_private = false)
39
- @services.respond_to?(name, include_private) || super
49
+ name == :snippets || super
40
50
  end
41
51
  end
42
52
  end
@@ -2,16 +2,28 @@
2
2
 
3
3
  module Jahuty
4
4
  module Request
5
- # Instantiates a request from an action. Currently, this is elementary. As
6
- # we add actions, it will become more complicated.
5
+ # Instantiates a request from an action.
7
6
  class Factory
8
7
  def call(action)
9
8
  Base.new(
10
9
  method: 'get',
11
- path: "snippets/#{action.id}/render",
10
+ path: path(action),
12
11
  params: action.params
13
12
  )
14
13
  end
14
+
15
+ private
16
+
17
+ def path(action)
18
+ case action
19
+ when ::Jahuty::Action::Show
20
+ "snippets/#{action.id}/render"
21
+ when ::Jahuty::Action::Index
22
+ 'snippets/render'
23
+ else
24
+ raise ArgumentError, 'Action is not supported'
25
+ end
26
+ end
15
27
  end
16
28
  end
17
29
  end
@@ -1,31 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
-
5
3
  module Jahuty
6
4
  module Resource
7
- # Negotiates the resource to return given the requested action and the
8
- # server's response.
5
+ # Instantiates and returns a resource.
9
6
  class Factory
10
7
  CLASSES = {
11
8
  problem: Problem.name,
12
9
  render: Render.name
13
10
  }.freeze
14
11
 
15
- def call(action, response)
16
- if success? response
17
- resource_name = action.resource
18
- elsif problem? response
19
- resource_name = 'problem'
20
- else
21
- raise ArgumentError, 'Unexpected response'
22
- end
23
-
24
- resource_class = class_name(resource_name.to_sym)
12
+ def call(resource_name, payload)
13
+ klass = class_name(resource_name.to_sym)
25
14
 
26
- payload = parse(response)
15
+ raise ArgumentError, "#{resource_name} missing" if klass.nil?
27
16
 
28
- Object.const_get(resource_class).send(:new, **payload)
17
+ Object.const_get(klass).send(:from, **payload)
29
18
  end
30
19
 
31
20
  private
@@ -33,18 +22,6 @@ module Jahuty
33
22
  def class_name(resource_name)
34
23
  CLASSES[resource_name.to_sym]
35
24
  end
36
-
37
- def problem?(response)
38
- response.headers['Content-Type'] == 'application/problem+json'
39
- end
40
-
41
- def parse(response)
42
- JSON.parse(response.body, symbolize_names: true)
43
- end
44
-
45
- def success?(response)
46
- response.status.between?(200, 299)
47
- end
48
25
  end
49
26
  end
50
27
  end
@@ -12,6 +12,14 @@ module Jahuty
12
12
  @type = type
13
13
  @detail = detail
14
14
  end
15
+
16
+ def self.from(data)
17
+ raise ArgumentError.new, 'Key :status missing' unless data.key?(:status)
18
+ raise ArgumentError.new, 'Key :type missing' unless data.key?(:type)
19
+ raise ArgumentError.new, 'Key :detail missing' unless data.key?(:detail)
20
+
21
+ Problem.new(data.slice(:status, :type, :detail))
22
+ end
15
23
  end
16
24
  end
17
25
  end
@@ -2,14 +2,20 @@
2
2
 
3
3
  module Jahuty
4
4
  module Resource
5
- # A snippet's rendered content. Remember, renders are unique by the
6
- # combination of id and params (i.e., the same id can produce different
7
- # renders with different params).
5
+ # A snippet's rendered content.
8
6
  class Render
9
- attr_accessor :content
7
+ attr_accessor :content, :snippet_id
10
8
 
11
- def initialize(content:)
9
+ def initialize(content:, snippet_id:)
12
10
  @content = content
11
+ @snippet_id = snippet_id
12
+ end
13
+
14
+ def self.from(data)
15
+ raise ArgumentError.new, 'Key :content missing' unless data.key?(:content)
16
+ raise ArgumentError.new, 'Key :snippet_id missing' unless data.key?(:snippet_id)
17
+
18
+ Render.new(data.slice(:content, :snippet_id))
13
19
  end
14
20
 
15
21
  def to_s
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Jahuty
6
+ module Response
7
+ # Inspects the response and returns the appropriate resource or collection.
8
+ class Handler
9
+ def call(action, response)
10
+ resource_name = name_resource action, response
11
+
12
+ payload = parse response
13
+
14
+ @resources ||= ::Jahuty::Resource::Factory.new
15
+
16
+ if collection?(payload)
17
+ payload.map { |data| @resources.call resource_name, data }
18
+ elsif resource?(payload)
19
+ @resources.call resource_name, payload
20
+ else
21
+ raise ArgumentError, 'Action and payload mismatch'
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def collection?(payload)
28
+ payload.is_a?(::Array)
29
+ end
30
+
31
+ def name_resource(action, response)
32
+ if success? response
33
+ action.resource
34
+ elsif problem? response
35
+ 'problem'
36
+ else
37
+ raise ArgumentError, 'Unexpected response'
38
+ end
39
+ end
40
+
41
+ def parse(response)
42
+ JSON.parse(response.body, symbolize_names: true)
43
+ end
44
+
45
+ def problem?(response)
46
+ response.headers['Content-Type'].include?('application/problem+json') &&
47
+ (response.status < 200 || response.status >= 300)
48
+ end
49
+
50
+ def resource?(payload)
51
+ payload.is_a?(::Object)
52
+ end
53
+
54
+ def success?(response)
55
+ response.headers['Content-Type'].include?('application/json') &&
56
+ response.status.between?(200, 299)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -4,16 +4,96 @@ module Jahuty
4
4
  module Service
5
5
  # A service for interacting with snippets.
6
6
  class Snippet < Base
7
- def render(id, options = {})
8
- params = { params: options[:params].to_json } unless options[:params].nil?
7
+ def initialize(client:, cache:, expires_in: nil, prefer_latest: false)
8
+ super(client: client)
9
+
10
+ @cache = cache
11
+ @expires_in = expires_in
12
+ @prefer_latest = prefer_latest
13
+ end
14
+
15
+ def all_renders(tag, params: {}, expires_in: @expires_in, prefer_latest: @prefer_latest)
16
+ renders = index_renders tag: tag, params: params, prefer_latest: prefer_latest
17
+
18
+ cache_renders renders: renders, params: params, expires_in: expires_in
19
+
20
+ renders
21
+ end
22
+
23
+ def render(snippet_id, params: {}, expires_in: @expires_in, prefer_latest: @prefer_latest)
24
+ key = cache_key snippet_id: snippet_id, params: params
25
+
26
+ render = @cache.read(key)
27
+
28
+ @cache.delete key unless render.nil? || cacheable?(expires_in)
29
+
30
+ if render.nil?
31
+ render = show_render snippet_id: snippet_id, params: params, prefer_latest: prefer_latest
32
+
33
+ @cache.write key, render, expires_in: expires_in if cacheable?(expires_in)
34
+ end
35
+
36
+ render
37
+ end
38
+
39
+ private
40
+
41
+ def cache_key(snippet_id:, params: {})
42
+ fingerprint = Digest::MD5.new
43
+ fingerprint << "snippets/#{snippet_id}/render/"
44
+ fingerprint << params.to_json
45
+
46
+ "jahuty_#{fingerprint.hexdigest}"
47
+ end
48
+
49
+ def cache_renders(renders:, params:, expires_in: nil)
50
+ expires_in ||= @expires_in
51
+
52
+ return if renders.nil?
53
+
54
+ return unless cacheable?(expires_in)
55
+
56
+ global_params = params['*'] || {}
57
+
58
+ renders.each do |render|
59
+ local_params = params[render.snippet_id.to_s] || {}
60
+ render_params = ::Jahuty::Util.deep_merge global_params, local_params
61
+
62
+ key = cache_key snippet_id: render.snippet_id, params: render_params
63
+
64
+ @cache.write key, render, expires_in: expires_in
65
+ end
66
+ end
67
+
68
+ def cacheable?(expires_in)
69
+ expires_in.nil? || expires_in.positive?
70
+ end
71
+
72
+ def index_renders(tag:, params: {}, prefer_latest: false)
73
+ request_params = { tag: tag }
74
+ request_params[:params] = params.to_json unless params.empty?
75
+ request_params[:latest] = 1 if prefer_latest
76
+
77
+ action = ::Jahuty::Action::Index.new(
78
+ resource: 'render',
79
+ params: request_params
80
+ )
81
+
82
+ @client.request action
83
+ end
84
+
85
+ def show_render(snippet_id:, params: {}, prefer_latest: false)
86
+ request_params = {}
87
+ request_params[:params] = params.to_json unless params.empty?
88
+ request_params[:latest] = 1 if prefer_latest
9
89
 
10
90
  action = ::Jahuty::Action::Show.new(
11
- id: id,
91
+ id: snippet_id,
12
92
  resource: 'render',
13
- params: params || {}
93
+ params: request_params
14
94
  )
15
95
 
16
- @client.request(action)
96
+ @client.request action
17
97
  end
18
98
  end
19
99
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jahuty
4
+ # Utility methods.
5
+ class Util
6
+ # Deeply merges two hashes like Rails.
7
+ #
8
+ # Ideally, the API and this library could use the same method to merge
9
+ # parameters. This library's method just needs to be deterministic and not
10
+ # collide distinct combinations.
11
+ #
12
+ # @see https://github.com/rails/rails/blob/main/activesupport/lib/active_support/core_ext/hash/deep_merge.rb
13
+ def self.deep_merge(first_hash, other_hash, &block)
14
+ first_hash.merge!(other_hash) do |key, first_val, other_val|
15
+ if first_val.is_a?(Hash) && other_val.is_a?(Hash)
16
+ deep_merge(first_val, other_val, &block)
17
+ elsif block
18
+ yield(key, first_val, other_val)
19
+ else
20
+ other_val
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Jahuty
4
- VERSION = '3.0.0'
4
+ VERSION = '3.3.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jahuty
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jack Clayton
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-12-30 00:00:00.000000000 Z
11
+ date: 2021-04-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: mini_cache
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.1'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: bundler
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -185,8 +199,10 @@ files:
185
199
  - jahuty.gemspec
186
200
  - lib/jahuty.rb
187
201
  - lib/jahuty/action/base.rb
202
+ - lib/jahuty/action/index.rb
188
203
  - lib/jahuty/action/show.rb
189
204
  - lib/jahuty/api/client.rb
205
+ - lib/jahuty/cache/facade.rb
190
206
  - lib/jahuty/client.rb
191
207
  - lib/jahuty/exception/error.rb
192
208
  - lib/jahuty/request/base.rb
@@ -194,9 +210,10 @@ files:
194
210
  - lib/jahuty/resource/factory.rb
195
211
  - lib/jahuty/resource/problem.rb
196
212
  - lib/jahuty/resource/render.rb
213
+ - lib/jahuty/response/handler.rb
197
214
  - lib/jahuty/service/base.rb
198
- - lib/jahuty/service/factory.rb
199
215
  - lib/jahuty/service/snippet.rb
216
+ - lib/jahuty/util.rb
200
217
  - lib/jahuty/version.rb
201
218
  homepage: https://www.jahuty.com
202
219
  licenses:
@@ -221,7 +238,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
221
238
  - !ruby/object:Gem::Version
222
239
  version: '0'
223
240
  requirements: []
224
- rubygems_version: 3.1.4
241
+ rubygems_version: 3.1.6
225
242
  signing_key:
226
243
  specification_version: 4
227
244
  summary: Jahuty's Ruby SDK.
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Jahuty
4
- module Service
5
- # Instantiates the requested service and memoizes it for subsequent
6
- # requests.
7
- class Factory
8
- CLASSES = {
9
- snippets: Snippet.name
10
- }.freeze
11
-
12
- def initialize(client:)
13
- @client = client
14
- @services = {}
15
- end
16
-
17
- def method_missing(name, *args, &block)
18
- if args.empty? && class_name?(name)
19
- unless @services.key?(name)
20
- klass = class_name(name)
21
- service = Object.const_get(klass).send(:new, client: @client)
22
- @services[name] = service
23
- end
24
-
25
- @services[name]
26
- else
27
- super
28
- end
29
- end
30
-
31
- def respond_to_missing?(name, include_private = false)
32
- class_name(name) || super
33
- end
34
-
35
- private
36
-
37
- def class_name(service_name)
38
- CLASSES[service_name]
39
- end
40
-
41
- def class_name?(service_name)
42
- CLASSES.key?(service_name)
43
- end
44
- end
45
- end
46
- end