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 +4 -4
- data/.rubocop.yml +11 -0
- data/CHANGELOG.md +25 -0
- data/README.md +191 -17
- data/jahuty.gemspec +1 -0
- data/lib/jahuty.rb +7 -1
- data/lib/jahuty/action/index.rb +9 -0
- data/lib/jahuty/api/client.rb +2 -1
- data/lib/jahuty/cache/facade.rb +46 -0
- data/lib/jahuty/client.rb +21 -11
- data/lib/jahuty/request/factory.rb +15 -3
- data/lib/jahuty/resource/factory.rb +5 -28
- data/lib/jahuty/resource/problem.rb +8 -0
- data/lib/jahuty/resource/render.rb +11 -5
- data/lib/jahuty/response/handler.rb +60 -0
- data/lib/jahuty/service/snippet.rb +85 -5
- data/lib/jahuty/util.rb +25 -0
- data/lib/jahuty/version.rb +1 -1
- metadata +21 -4
- data/lib/jahuty/service/factory.rb +0 -46
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 93d706c72d12cb61d951cc71b8add1ac154ed8b8bc57374e3d558e2b4bd8d420
|
4
|
+
data.tar.gz: e64a1355073ba942c5aef652032e8268e4d3a065b8cb3f5a7594d590957edc18
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
13
|
+
To install, add this line to your application's `Gemfile` and run `bundle install`:
|
14
14
|
|
15
15
|
```ruby
|
16
|
-
gem 'jahuty', '~> 3.
|
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
|
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
|
26
|
+
puts jahuty.snippets.render YOUR_SNIPPET_ID
|
27
27
|
```
|
28
28
|
|
29
|
-
You can
|
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
|
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
|
-
|
53
|
+
<%== jahuty.snippets.render YOUR_SNIPPET_ID %>
|
56
54
|
</body>
|
57
55
|
```
|
58
56
|
|
59
|
-
|
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
|
-
|
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
|
100
|
+
jahuty.snippets.render YOUR_SNIPPET_ID, params: { foo: 'bar' }
|
67
101
|
```
|
68
102
|
|
69
|
-
The parameters above would be equivalent to
|
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
|
-
|
109
|
+
### Collection parameters
|
76
110
|
|
77
|
-
|
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
|
-
|
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
|
data/lib/jahuty/api/client.rb
CHANGED
@@ -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
|
-
|
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
|
8
|
-
@
|
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
|
16
|
+
# Allows services to be accessed as properties (e.g., jahuty.snippets).
|
12
17
|
def method_missing(name, *args, &block)
|
13
|
-
if args.empty? &&
|
14
|
-
@services.
|
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
|
-
@
|
39
|
+
@responses ||= Response::Handler.new
|
30
40
|
|
31
|
-
|
41
|
+
result = @responses.call(action, response)
|
32
42
|
|
33
|
-
raise Exception::Error.new(
|
43
|
+
raise Exception::Error.new(result), 'API problem' if result.is_a?(Resource::Problem)
|
34
44
|
|
35
|
-
|
45
|
+
result
|
36
46
|
end
|
37
47
|
|
38
48
|
def respond_to_missing?(name, include_private = false)
|
39
|
-
|
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.
|
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:
|
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
|
-
#
|
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(
|
16
|
-
|
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
|
-
|
15
|
+
raise ArgumentError, "#{resource_name} missing" if klass.nil?
|
27
16
|
|
28
|
-
Object.const_get(
|
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.
|
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
|
8
|
-
|
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:
|
91
|
+
id: snippet_id,
|
12
92
|
resource: 'render',
|
13
|
-
params:
|
93
|
+
params: request_params
|
14
94
|
)
|
15
95
|
|
16
|
-
@client.request
|
96
|
+
@client.request action
|
17
97
|
end
|
18
98
|
end
|
19
99
|
end
|
data/lib/jahuty/util.rb
ADDED
@@ -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
|
data/lib/jahuty/version.rb
CHANGED
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.
|
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:
|
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.
|
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
|