storyblok 2.0.7 → 3.0.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: b30efd5b32b379bfc557741df74bcc44885c8d731e943ab94d69f3ebb8b156f6
4
- data.tar.gz: ebaa6003b3badcc1aff1adbdebf6c84e3e1feb3f5d2896d91e578ec8e9aa487f
3
+ metadata.gz: 5b52ba5dae9347e6869eff8ae51d53d3b569eef875132b569bcfd8c88c648650
4
+ data.tar.gz: b15b55e78b254420e70016a9258e4c31f9fcd855a97050ae64972b9ce16aeeeb
5
5
  SHA512:
6
- metadata.gz: 59af62cffd92cd0efc91b1a1c890b57002ed8a86e7e7bd792d87755f4a0030dcd0449d3ed6353bc0c30492545387227c0d1f27266e960ab1a0cec43d849f181a
7
- data.tar.gz: 87779ece18ab08e5f0fc139851403ad79407377fd0fffcca28ed878056f9ff016dbf44e4474348234b89407331c30af678b990daa0d6bec1128b03debf9e966a
6
+ metadata.gz: 5265d40c02b920d73d783306810c0dffd1498574b607dd2af7ac658895efbf428f2812b04abdf56ca0e5107a050483f55f98e6c48a371da037bfcfa1fba9c01d
7
+ data.tar.gz: c7df3f398a7cffe56773d1c286b68ab9be0edcc3791fff45abdc504249b2ba70a0096aa43e6798e38b4b56f374008fc8007fdf9157835ba5a756c9e28a9d7c87
data/.gitignore CHANGED
@@ -6,3 +6,4 @@ Gemfile.lock
6
6
  .idea
7
7
  *.DS_Store
8
8
  .ruby-version
9
+ *.gem
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- storyblok (2.0.6)
4
+ storyblok (2.0.8)
5
5
  rest-client (>= 1.8.0, < 3)
6
6
  storyblok-richtext-renderer (>= 0.0.4, < 1)
7
7
 
@@ -16,7 +16,7 @@ GEM
16
16
  domain_name (~> 0.5)
17
17
  mime-types (3.3)
18
18
  mime-types-data (~> 3.2015)
19
- mime-types-data (3.2019.0904)
19
+ mime-types-data (3.2019.1009)
20
20
  netrc (0.11.0)
21
21
  redis (4.1.0)
22
22
  rest-client (2.1.0)
data/README.md CHANGED
@@ -1,5 +1,11 @@
1
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/76e7fcc8524d4fadeeee/test_coverage)](https://codeclimate.com/github/storyblok/storyblok-ruby/test_coverage)
2
+ ![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/storyblok/storyblok-ruby/RSpec%20Tests/master)
3
+ [![Ruby Gems Downloads](https://img.shields.io/gem/dt/storyblok)](https://rubygems.org/gems/storyblok)
4
+ [![Inline docs](https://inch-ci.org/github/storyblok/storyblok-ruby.svg?branch=master)](https://www.rubydoc.info/gems/storyblok)
5
+ [![Maintainability](https://api.codeclimate.com/v1/badges/76e7fcc8524d4fadeeee/maintainability)](https://codeclimate.com/github/storyblok/storyblok-ruby/maintainability)
6
+
1
7
  # About
2
- This is the Storyblok ruby client for easy access of the management and content delivery api.
8
+ This is the official [Storyblok](https://www.storyblok.com/) ruby client for easy access of the management and content delivery api.
3
9
 
4
10
  ## Install
5
11
 
@@ -120,7 +126,7 @@ client.flush
120
126
  client = Storyblok::Client.new(oauth_token: 'YOUR_OAUTH_TOKEN')
121
127
 
122
128
  # Get your spaces
123
- client.get('spaces')
129
+ client.get('spaces/')
124
130
  ```
125
131
 
126
132
  ### Create a story
@@ -158,12 +164,12 @@ Storyblok's richtext field also let's you insert content blocks. To render these
158
164
  ```ruby
159
165
  # Option 1: Define the resolver when initializing
160
166
  client = Storyblok::Client.new(
161
- component_resolver: ->(component, data) => {
167
+ component_resolver: ->(component, data) {
162
168
  case component
163
169
  when 'button'
164
170
  "<button>#{data['text']}</button>"
165
171
  when 'your_custom_component'
166
- "<div class="welcome">#{data['welcome_text']}</div>"
172
+ "<div class='welcome'>#{data['welcome_text']}</div>"
167
173
  end
168
174
  }
169
175
  )
@@ -183,6 +189,32 @@ gem build storyblok.gemspec
183
189
  gem push storyblok-2.0.X.gem
184
190
  ~~~
185
191
 
192
+ ### Running Tests
193
+ We use [RSpec](http://rspec.info/) for testing.
194
+
195
+ #### To run the whole test suite you will need export the environment variables, ATTENTION when running the test suit with the variable `REDIS_URL` exported, the test suite will remove the keys with this pattern `storyblok:*` from the redis database defined by `REDIS_URL`
196
+
197
+ ```bash
198
+ export REDIS_URL="redis://localhost:6379"
199
+ ```
200
+
201
+ Optionally you can generate the test report coverage by setting the environment variable
202
+
203
+ ```bash
204
+ export COVERAGE=true
205
+ ```
206
+
207
+ To run the whole test suite use the following command:
208
+
209
+ ```bash
210
+ rspec
211
+ ```
212
+
213
+ To run tests without redis cache tests (for when you don't have redis, or to avoid the test suite to remove the keys with this pattern `storyblok:*` ):
214
+
215
+ ```bash
216
+ rspec --tag ~redis_cache:true
217
+ ```
186
218
 
187
219
  ### License
188
220
 
@@ -5,14 +5,14 @@ require 'storyblok'
5
5
  logger = Logger.new(STDOUT)
6
6
 
7
7
  client = Storyblok::Client.new(
8
- token: 'FtyUE8zLpZox3ptNYz3dgQtt',
8
+ token: 't618GfLe1YHICBioAHnMrwtt',
9
9
  api_url: 'localhost:3001',
10
10
  secure: false,
11
11
  logger: logger
12
12
  )
13
13
 
14
- p client.datasources
15
14
  p client.stories(starts_with: 'en/news')
16
15
  p client.story('demo1')
17
16
  p client.datasource_entries(datasource: 'labels', per_page: 10)
18
17
  p client.links
18
+ p client.datasources
data/examples/renderer.rb CHANGED
@@ -1,23 +1,31 @@
1
1
  # bundle exec ruby examples/renderer.rb
2
2
 
3
3
  require_relative '../lib/storyblok'
4
+ require 'redis'
4
5
 
5
6
  logger = Logger.new(STDOUT)
6
7
 
8
+ redis = Redis.new(url: 'redis://localhost:6379')
9
+ cache = Storyblok::Cache::Redis.new(redis: redis)
10
+
7
11
  client = Storyblok::Client.new(
8
12
  token: '6HMYdAjBoONyuS6GIf5PdAtt',
9
13
  logger: logger,
10
14
  component_resolver: ->(component, data) {
11
15
  "Placeholder for #{component}: #{data['text']}"
12
- }
16
+ },
17
+ api_url: 'api-testing.storyblok.com',
18
+ api_version: 2,
19
+ cache: cache
13
20
  )
14
21
 
15
- puts client.render({'type' => 'doc', 'content' => [
16
- {'type' => 'paragraph', 'content' => [{'text' => 'Good', 'type' => 'text'}]},
17
- {'type' => 'blok', 'attrs' => {'body' => [{'component' => 'button', 'text' => 'Click me'}]}}
18
- ]})
19
22
 
20
- res = client.story('article/article-1')
23
+ res = client.flush
24
+ res = client.story('authors/page', {version: 'published'})
25
+ puts client.cache_version
26
+ res = client.story('authors/page', {version: 'published'})
27
+ res = client.story('authors/page', {version: 'published'})
28
+ res = client.story('authors/page', {version: 'published'})
21
29
 
22
- puts res['data']['story']['content']['intro']
23
- puts client.render(res['data']['story']['content']['intro'])
30
+ puts res['data']
31
+ #puts client.render(res['data']['story']['content']['intro'])
@@ -3,7 +3,9 @@ module Storyblok
3
3
  class Redis
4
4
  DEFAULT_CONFIGURATION = {
5
5
  ttl: 60 * 60 * 24
6
- }
6
+ }.freeze
7
+
8
+ attr_reader :redis
7
9
 
8
10
  def initialize(*args)
9
11
  options = args.last.is_a?(::Hash) ? args.pop : {}
@@ -12,16 +12,19 @@ module Storyblok
12
12
  DEFAULT_CONFIGURATION = {
13
13
  secure: true,
14
14
  api_url: 'api.storyblok.com',
15
- api_version: 1,
15
+ api_version: 2,
16
16
  logger: false,
17
17
  log_level: Logger::INFO,
18
18
  version: 'draft',
19
+ # :nocov:
19
20
  component_resolver: ->(component, data) { '' },
21
+ # :nocov:
20
22
  cache_version: Time.now.to_i,
21
23
  cache: nil
22
24
  }
23
25
 
24
26
  attr_reader :configuration, :logger
27
+ attr_accessor :cache_version
25
28
 
26
29
  # @param [Hash] given_configuration
27
30
  # @option given_configuration [String] :token Required if oauth_token is not set
@@ -33,6 +36,7 @@ module Storyblok
33
36
  # @option given_configuration [::Logger::DEBUG, ::Logger::INFO, ::Logger::WARN, ::Logger::ERROR] :log_level
34
37
  def initialize(given_configuration = {})
35
38
  @configuration = default_configuration.merge(given_configuration)
39
+ @cache_version = '0'
36
40
  validate_configuration!
37
41
 
38
42
  if configuration[:oauth_token]
@@ -62,7 +66,7 @@ module Storyblok
62
66
  #
63
67
  # @return [Hash]
64
68
  def space(query = {})
65
- Request.new(self, '/cdn/spaces/me', query).get
69
+ Request.new(self, '/cdn/spaces/me', query, nil, true).get
66
70
  end
67
71
 
68
72
  # Gets a collection of stories
@@ -169,28 +173,34 @@ module Storyblok
169
173
  parse_result(res)
170
174
  end
171
175
 
172
- def cached_get(request)
176
+ def cached_get(request, bypass_cache = false)
173
177
  endpoint = base_url + request.url
174
178
  query = request_query(request.query)
175
179
  query_string = build_nested_query(query)
176
180
 
177
- if cache.nil? or query[:uncached]
181
+ if cache.nil? || bypass_cache || query[:version] == 'draft'
178
182
  result = run_request(endpoint, query_string)
179
183
  else
180
- version = cache.get('storyblok:' + configuration[:token] + ':version') || '0'
181
- cache_key = 'storyblok:' + configuration[:token] + ':v:' + version + ':' + request.url + ':' + Base64.encode64(query_string)
184
+ cache_key = 'storyblok:' + configuration[:token] + ':v:' + query[:cv] + ':' + request.url + ':' + Base64.encode64(query_string)
182
185
 
183
186
  result = cache.cache(cache_key) do
184
187
  run_request(endpoint, query_string)
185
188
  end
186
189
  end
187
190
 
188
- JSON.parse(result)
191
+ result = JSON.parse(result)
192
+
193
+ if !result.dig('data', 'story').nil? || !result.dig('data', 'stories').nil?
194
+ result = resolve_stories(result, query)
195
+ end
196
+
197
+ result
189
198
  end
190
199
 
200
+
191
201
  def flush
192
202
  unless cache.nil?
193
- cache.set('storyblok:' + configuration[:token] + ':version', Time.now.to_i.to_s)
203
+ cache.set('storyblok:' + configuration[:token] + ':version', space['data']['space']['version'])
194
204
  end
195
205
  end
196
206
 
@@ -236,14 +246,27 @@ module Storyblok
236
246
  raise
237
247
  end
238
248
 
239
- {'headers' => res.headers, 'data' => JSON.parse(res.body)}.to_json
249
+ body = JSON.parse(res.body)
250
+ self.cache_version = body['cv'] if body['cv']
251
+
252
+ unless cache.nil?
253
+ cache.set('storyblok:' + configuration[:token] + ':version', cache_version)
254
+ end
255
+
256
+ {'headers' => res.headers, 'data' => body}.to_json
240
257
  end
241
258
 
242
259
  # Patches a query hash with the client configurations for queries
243
260
  def request_query(query)
244
261
  query[:token] = configuration[:token] if query[:token].nil?
245
262
  query[:version] = configuration[:version] if query[:version].nil?
246
- query[:cv] = configuration[:cache_version] if query[:cache_version].nil?
263
+
264
+ unless cache.nil?
265
+ query[:cv] = (cache.get('storyblok:' + configuration[:token] + ':version') or cache_version) if query[:cv].nil?
266
+ else
267
+ query[:cv] = cache_version if query[:cv].nil?
268
+ end
269
+
247
270
  query
248
271
  end
249
272
 
@@ -288,5 +311,86 @@ module Storyblok
288
311
  "#{prefix}=#{URI.encode_www_form_component(value)}"
289
312
  end
290
313
  end
314
+
315
+ def resolve_stories(result, params)
316
+ data = result['data']
317
+ rels = data['rels']
318
+ links = data['links']
319
+
320
+ if data['stories'].nil?
321
+ find_and_fill_relations(data.dig('story', 'content'), params[:resolve_relations], rels)
322
+ find_and_fill_links(data.dig('story', 'content'), links)
323
+ else
324
+ data['stories'].each do |story|
325
+ find_and_fill_relations(story['content'], params[:resolve_relations], rels)
326
+ find_and_fill_links(story['content'], links)
327
+ end
328
+ end
329
+
330
+ result
331
+ end
332
+
333
+ def find_and_fill_links(content, links)
334
+ return if content.nil? || links.nil? || links.size.zero?
335
+
336
+ if content.is_a? Array
337
+ content.each do |item|
338
+ find_and_fill_links(item, links)
339
+ end
340
+ elsif content.is_a? Hash
341
+ content['story'] = nil
342
+ content.each do |_k, value|
343
+ if !content['fieldtype'].nil?
344
+ if content['fieldtype'] == 'multilink' && content['linktype'] == 'story'
345
+ id =
346
+ if content['id'].is_a? String
347
+ content['id']
348
+ elsif content['uuid'].is_a? String
349
+ content['uuid']
350
+ end
351
+
352
+ links.each do |link|
353
+ if link['uuid'] == id
354
+ content['story'] = link
355
+ break
356
+ end
357
+ end
358
+ end
359
+ end
360
+
361
+ find_and_fill_links(value, links)
362
+ end
363
+ content.delete('story') if content['story'].nil?
364
+ end
365
+ end
366
+
367
+ def find_and_fill_relations(content, relation_params, rels)
368
+ return if content.nil? || rels.nil? || rels.size.zero?
369
+
370
+ if content.is_a? Array
371
+ content.each do |item|
372
+ find_and_fill_relations(item, relation_params, rels)
373
+ end
374
+ elsif content.is_a? Hash
375
+ content.each do |_k, value|
376
+ if !content['component'].nil? && !content['_uid'].nil?
377
+ relation_params.split(',').each do |relation|
378
+ component, field_name = relation.split('.')
379
+
380
+ if (content['component'] == component) && !content[field_name].nil?
381
+ rels.each do |rel|
382
+ index = content[field_name].index(rel['uuid'])
383
+ if !index.nil?
384
+ content[field_name][index] = rel
385
+ end
386
+ end
387
+ end
388
+ end
389
+ end
390
+
391
+ find_and_fill_relations(value, relation_params, rels)
392
+ end
393
+ end
394
+ end
291
395
  end
292
396
  end
@@ -5,14 +5,15 @@ module Storyblok
5
5
  class Request
6
6
  attr_reader :client, :type, :query, :id, :endpoint
7
7
 
8
- def initialize(client, endpoint, query = {}, id = nil)
8
+ def initialize(client, endpoint, query = {}, id = nil, bypass_cache = false)
9
9
  @client = client
10
10
  @endpoint = endpoint
11
11
  @query = query
12
+ @bypass_cache = bypass_cache
12
13
 
13
14
  if id
14
15
  @type = :single
15
- @id = URI.escape(id)
16
+ @id = id
16
17
  else
17
18
  @type = :multi
18
19
  @id = nil
@@ -26,7 +27,7 @@ module Storyblok
26
27
 
27
28
  # Delegates the actual HTTP work to the client
28
29
  def get
29
- client.cached_get(self)
30
+ client.cached_get(self, @bypass_cache)
30
31
  end
31
32
 
32
33
  # Returns a new Request object with the same data
@@ -1,4 +1,4 @@
1
1
  module Storyblok
2
2
  # Gem Version
3
- VERSION = '2.0.7'
3
+ VERSION = '3.0.0'
4
4
  end
@@ -0,0 +1,91 @@
1
+ require 'redis'
2
+ require_relative '../../lib/storyblok/cache/redis'
3
+ require 'spec_helper'
4
+
5
+ describe Storyblok::Cache::Redis, redis_cache: true do
6
+ let(:redis_client) {
7
+ raise StandardError, "Environment variable 'REDIS_URL' is not defined" if ENV['REDIS_URL'].nil?
8
+ Redis.new(url: ENV['REDIS_URL'])
9
+ }
10
+
11
+ context "When '::Redis' from redis-rb gem is not available" do
12
+ let!(:redis_constant_copy) { ::Redis }
13
+ before { Object.send(:remove_const, :Redis) }
14
+ after { ::Redis = redis_constant_copy }
15
+ it "raises Redis.current could not be found" do
16
+ expect{ subject }.to raise_error(RuntimeError, 'Redis.current could not be found. Supply :redis option or make sure Redis.current is available.')
17
+ end
18
+ end
19
+
20
+ context "When Storyblok::Cache::Redis is initialized without 'redis' arg" do
21
+ it "asks the redis to '::Redis.current'" do
22
+ redis_current = nil
23
+ expect{ redis_current = subject.redis }.not_to raise_error
24
+ expect(redis_current.object_id).to eq(::Redis.current.object_id)
25
+ end
26
+ end
27
+
28
+ describe "#cache" do
29
+ before { redis_client.keys("storyblok:*").each { |e| redis_client.del(e) } }
30
+
31
+ let(:key) { "storyblok:my_cache_key" }
32
+ context "When is passed an block" do
33
+ context "When the result it's not cached" do
34
+ it "caches the block result and returns the block call result" do
35
+ expect{
36
+ subject.cache(key){ "my_value" }
37
+ }.to change { redis_client.keys("#{key}*").size }.from(0).to(1)
38
+
39
+ expect(redis_client.get(key)).to eq("my_value")
40
+ end
41
+ end
42
+
43
+ context "When the arg expire as '0' is passed" do
44
+ let(:expire) { 0 }
45
+ it "returns the result of the block" do
46
+ expect(subject.cache(key, expire){ "my_value" } ).to eq("my_value")
47
+ end
48
+
49
+ it "doesn't cache anything" do
50
+ expect{ subject.cache(key, expire){ "my_value" } }.not_to change { redis_client.keys.size }
51
+ end
52
+ end
53
+
54
+ context "When the key has value on cache" do
55
+ before { subject.cache(key){ "my_value_cached" } }
56
+ it "the block it's no called and the cache stays untouched and the cached value is returned" do
57
+ expect{
58
+ subject.cache(key){ "my_new_value" }
59
+ }.not_to change { redis_client.keys("#{key}*").size }
60
+
61
+ expect(redis_client.get(key)).to eq("my_value_cached")
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ describe "#set" do
68
+ subject { super().set(key, value, expire) }
69
+ before { redis_client.keys("storyblok:*").each { |e| redis_client.del(e) } }
70
+ let(:key) {'storyblok:my_key'}
71
+ let(:value) {'my_value'}
72
+
73
+ context "When arg 'expire' is false" do
74
+ let(:expire) { false }
75
+ it "store the key value at Redis database WITHOUT expiration" do
76
+ expect{ subject }.to change { redis_client.keys("#{key}*").size }.from(0).to(1)
77
+ expect(redis_client.get(key)).to eq(value)
78
+ expect(redis_client.ttl(key)).to eq(-1) # -1 means no expiration for redis key
79
+ end
80
+ end
81
+
82
+ context "When args 'expire' is valid integer" do
83
+ let(:expire) { 60 * 60 }
84
+ it "store the key value at Redis database WITH expiration" do
85
+ expect{ subject }.to change { redis_client.keys("#{key}*").size }.from(0).to(1)
86
+ expect(redis_client.get(key)).to eq(value)
87
+ expect(redis_client.ttl(key)).to be > 1
88
+ end
89
+ end
90
+ end
91
+ end