storyblok 2.0.7 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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