contentful 2.3.0 → 2.4.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
  SHA1:
3
- metadata.gz: e169a58eaeb582e61a250dc95e7bf980c632d906
4
- data.tar.gz: 615244247ac26f6f3be2afa7e430f12aa225687d
3
+ metadata.gz: 033f5efe6223ef9a1e09d693c298977d21235209
4
+ data.tar.gz: bbe60ca4295d8c60bebfeed1e9362d5f60e937dd
5
5
  SHA512:
6
- metadata.gz: f074a6327c040944e729763b8003899c452ff7b31fe2a52998ab2364d55cb426b3e00b10fa4eaeb925293656afb2f85c3c9385c343c22a5563cbbe97d004bf36
7
- data.tar.gz: 721607153d1085113cd632081be420a17f2c835521116f15f3cc450f4c710f88a580d44b05541c5dc2df777afadde5faa6ed63467dfe331306cb4ecc59802616
6
+ metadata.gz: 59e84824fe37fd0410211e2a8ad6eaa52316d2ecefa53c8d75c2b0e2f4aec93cbb1222e0ebcda1e3ed9bba0b6af1311fbc3cb6eddf2c44a0d9c27e75532c3041
7
+ data.tar.gz: 562af572939b7615e4de51483eac5718b820815ac757e0dd9821320f846a73be056eaae7eccc9807f869f9cc69dca052321db263e2d34a67b508eca879a9ee62
data/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 2.4.0
6
+ ### Added
7
+ * Added `reuse_entries` option to client configuration. This is a performance improvement, which is disabled by default due to backwards compatibility. All users are highly encouraged to enable it and test it in their applications. [#164](https://github.com/contentful/contentful.rb/pull/164)
8
+
5
9
  ## 2.3.0
6
10
  ### Added
7
11
  * Support for the new query parameters to find incoming links [to a specific entry](https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/links-to-entry) or [to a specific asset](https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/links-to-asset) in a space.
data/README.md CHANGED
@@ -242,12 +242,18 @@ Maximum time to wait for next available request (in seconds). Default value is 6
242
242
  can have up to 60 minutes of blocked requests. It is set to a default of 60 seconds in order to avoid blocking processes for too long, as rate limit retry behaviour
243
243
  is blocking per execution thread.
244
244
 
245
+ ### :reuse_entries
246
+
247
+ When true, reuse hydrated Entry and Asset objects within the same request when possible. Can result in a large speed increase and better handles cyclical object graphs.
248
+ This can be a good alternative to `max_include_resolution_depth` if your content model contains (or can contain) circular references.
249
+
245
250
  ### :max_include_resolution_depth
246
251
 
247
252
  Maximum amount of levels to resolve includes for SDK entities (this is independent of API-level includes - it represents the maximum depth the include resolution
248
253
  tree is allowed to resolved before falling back to `Link` objects). This include resolution strategy is in place in order to avoid having infinite circular recursion
249
254
  on resources with circular dependencies. Defaults to 20. _Note_: If you're using something like `Rails::cache` it's advisable to considerably lower this value
250
255
  (around 5 has proven to be a good compromise - but keep it higher or equal than your maximum API-level include parameter if you need the entire tree resolution).
256
+ Note that when `reuse_entries` is enabled, the max include resolution depth only affects deep chains of unique objects (ie, not simple circular references).
251
257
 
252
258
  ### :use_camel_case
253
259
 
@@ -5,7 +5,9 @@ module Contentful
5
5
  class BaseResource
6
6
  attr_reader :raw, :default_locale, :sys
7
7
 
8
- def initialize(item, configuration = {}, _localized = false, _includes = [], depth = 0)
8
+ # rubocop:disable Metrics/ParameterLists
9
+ def initialize(item, configuration = {}, _localized = false, _includes = [], entries = {}, depth = 0)
10
+ entries["#{item['sys']['type']}:#{item['sys']['id']}"] = self if entries && item.key?('sys')
9
11
  @raw = item
10
12
  @default_locale = configuration[:default_locale]
11
13
  @depth = depth
@@ -15,9 +15,9 @@ module Contentful
15
15
 
16
16
  private
17
17
 
18
- def coerce(field_id, value, includes)
19
- return build_nested_resource(value, includes) if Support.link?(value)
20
- return coerce_link_array(value, includes) if Support.link_array?(value)
18
+ def coerce(field_id, value, includes, entries = {})
19
+ return build_nested_resource(value, includes, entries) if Support.link?(value)
20
+ return coerce_link_array(value, includes, entries) if Support.link_array?(value)
21
21
 
22
22
  content_type_key = Support.snakify('contentType', @configuration[:use_camel_case])
23
23
  content_type = ContentTypeCache.cache_get(sys[:space].id, sys[content_type_key.to_sym].id)
@@ -27,13 +27,13 @@ module Contentful
27
27
  return content_type_field.coerce(value) unless content_type_field.nil?
28
28
  end
29
29
 
30
- super(field_id, value, includes)
30
+ super(field_id, value, includes, entries)
31
31
  end
32
32
 
33
- def coerce_link_array(value, includes)
33
+ def coerce_link_array(value, includes, entries)
34
34
  items = []
35
35
  value.each do |link|
36
- items << build_nested_resource(link, includes)
36
+ items << build_nested_resource(link, includes, entries)
37
37
  end
38
38
 
39
39
  items
@@ -43,23 +43,24 @@ module Contentful
43
43
  # in case one of the included items has a reference in an upper level,
44
44
  # so we can keep the include chain for that object as well
45
45
  # Any included object after the maximum include resolution depth will be just a Link
46
- def build_nested_resource(value, includes)
46
+ def build_nested_resource(value, includes, entries)
47
47
  if @depth < @configuration.fetch(:max_include_resolution_depth, 20)
48
48
  resource = Support.resource_for_link(value, includes)
49
- return resolve_include(resource, includes) unless resource.nil?
49
+ return resolve_include(resource, includes, entries) unless resource.nil?
50
50
  end
51
51
 
52
52
  build_link(value)
53
53
  end
54
54
 
55
- def resolve_include(resource, includes)
55
+ def resolve_include(resource, includes, entries)
56
56
  require_relative 'resource_builder'
57
57
 
58
58
  ResourceBuilder.new(
59
59
  resource,
60
60
  @configuration.merge(
61
61
  includes_for_single:
62
- @configuration.fetch(:includes_for_single, []) + includes
62
+ @configuration.fetch(:includes_for_single, []) + includes,
63
+ _entries_cache: entries
63
64
  ),
64
65
  localized,
65
66
  @depth + 1,
@@ -6,12 +6,12 @@ module Contentful
6
6
  class FieldsResource < BaseResource
7
7
  attr_reader :localized
8
8
 
9
- def initialize(item, _configuration, localized = false, includes = [], *)
9
+ # rubocop:disable Metrics/ParameterLists
10
+ def initialize(item, _configuration, localized = false, includes = [], entries = {}, *)
10
11
  super
11
12
 
12
13
  @localized = localized
13
- @fields = hydrate_fields(includes)
14
-
14
+ @fields = hydrate_fields(includes, entries)
15
15
  define_fields_methods!
16
16
  end
17
17
 
@@ -56,7 +56,7 @@ module Contentful
56
56
  def marshal_load(raw_object)
57
57
  super(raw_object)
58
58
  @localized = raw_object[:localized]
59
- @fields = hydrate_fields(raw_object[:configuration].fetch(:includes_for_single, []))
59
+ @fields = hydrate_fields(raw_object[:configuration].fetch(:includes_for_single, []), {})
60
60
  define_fields_methods!
61
61
  end
62
62
 
@@ -82,31 +82,18 @@ module Contentful
82
82
  end
83
83
  end
84
84
 
85
- def hydrate_fields(includes)
86
- return {} unless raw.key?('fields')
87
-
85
+ def hydrate_localized_fields(includes, entries)
88
86
  locale = internal_resource_locale
89
87
  result = { locale => {} }
90
-
91
- if localized
92
- raw['fields'].each do |name, locales|
93
- locales.each do |loc, value|
94
- result[loc] ||= {}
95
- name = Support.snakify(name, @configuration[:use_camel_case])
96
- result[loc][name.to_sym] = coerce(
97
- name,
98
- value,
99
- includes
100
- )
101
- end
102
- end
103
- else
104
- raw['fields'].each do |name, value|
88
+ raw['fields'].each do |name, locales|
89
+ locales.each do |loc, value|
90
+ result[loc] ||= {}
105
91
  name = Support.snakify(name, @configuration[:use_camel_case])
106
- result[locale][name.to_sym] = coerce(
92
+ result[loc][name.to_sym] = coerce(
107
93
  name,
108
94
  value,
109
- includes
95
+ includes,
96
+ entries
110
97
  )
111
98
  end
112
99
  end
@@ -114,9 +101,35 @@ module Contentful
114
101
  result
115
102
  end
116
103
 
104
+ def hydrate_nonlocalized_fields(includes, entries)
105
+ result = { locale => {} }
106
+ locale = internal_resource_locale
107
+ raw['fields'].each do |name, value|
108
+ name = Support.snakify(name, @configuration[:use_camel_case])
109
+ result[locale][name.to_sym] = coerce(
110
+ name,
111
+ value,
112
+ includes,
113
+ entries
114
+ )
115
+ end
116
+
117
+ result
118
+ end
119
+
120
+ def hydrate_fields(includes, entries)
121
+ return {} unless raw.key?('fields')
122
+
123
+ if localized
124
+ hydrate_localized_fields(includes, entries)
125
+ else
126
+ hydrate_nonlocalized_fields(includes, entries)
127
+ end
128
+ end
129
+
117
130
  protected
118
131
 
119
- def coerce(_field_id, value, _includes)
132
+ def coerce(_field_id, value, _includes, _entries)
120
133
  value
121
134
  end
122
135
  end
@@ -40,6 +40,7 @@ module Contentful
40
40
  @depth = depth
41
41
  @endpoint = endpoint
42
42
  @configuration = configuration
43
+ @entries = configuration[:_entries_cache] || {}
43
44
  end
44
45
 
45
46
  # Starts the parsing process.
@@ -74,7 +75,17 @@ module Contentful
74
75
  fail UnparsableResource, 'Item type is not known, could not parse' if item_type.nil?
75
76
  item_class = resource_class(item)
76
77
 
77
- item_class.new(item, @configuration, localized?, includes, depth)
78
+ reuse_entries = @configuration.fetch(:reuse_entries, false)
79
+ entries = @entries ? @entries : {}
80
+
81
+ id = "#{item['sys']['type']}:#{item['sys']['id']}"
82
+ entry = if reuse_entries && entries.key?(id)
83
+ entries[id]
84
+ else
85
+ item_class.new(item, @configuration, localized?, includes, entries, depth)
86
+ end
87
+
88
+ entry
78
89
  end
79
90
 
80
91
  def fetch_includes
@@ -1,5 +1,5 @@
1
1
  # Contentful Namespace
2
2
  module Contentful
3
3
  # Gem Version
4
- VERSION = '2.3.0'
4
+ VERSION = '2.4.0'
5
5
  end
data/spec/entry_spec.rb CHANGED
@@ -283,7 +283,73 @@ describe Contentful::Entry do
283
283
  end
284
284
  end
285
285
 
286
+ describe 'reuse objects' do
287
+ it 'should handle recursion as well as not reusing' do
288
+ vcr('entry/include_resolution') {
289
+ entry = create_client(reuse_objects: true).entry('nyancat', include: 2)
290
+
291
+ expect(entry.best_friend.name).to eq 'Happy Cat'
292
+ expect(entry
293
+ .best_friend.best_friend
294
+ .best_friend.best_friend
295
+ .best_friend.best_friend
296
+ .best_friend.best_friend
297
+ .best_friend.best_friend
298
+ .best_friend.best_friend
299
+ .best_friend.best_friend
300
+ .best_friend.best_friend
301
+ .best_friend.best_friend
302
+ .best_friend.best_friend.name).to eq 'Nyan Cat'
303
+ }
304
+ end
305
+ it 'should use the same object for the same entry' do
306
+ vcr('entry/include_resolution') {
307
+ entry = create_client(reuse_entries: true).entry('nyancat', include: 2)
308
+
309
+ expect(entry.best_friend.name).to eq 'Happy Cat'
310
+ expect(entry.best_friend.best_friend).to be(entry)
311
+ }
312
+ end
313
+ it 'works on nested structures with unique objects' do
314
+ vcr('entry/include_resolution_uniques') {
315
+ entry = create_client(
316
+ space: 'v7cxgyxt0w5x',
317
+ access_token: '96e5d256e9a5349ce30e84356597e409f8f1bb485cb4719285b555e0f78aa27e',
318
+ reuse_entries: true
319
+ ).entry('1nLXjjWvk4MEeWeQCWmymc', include: 10)
320
+
321
+ expect(entry.title).to eq '1'
322
+ expect(entry
323
+ .child.child
324
+ .child.child
325
+ .child.child
326
+ .child.child
327
+ .child.title).to eq '10'
328
+ expect(entry
329
+ .child.child
330
+ .child.child
331
+ .child.child
332
+ .child.child
333
+ .child.child.title).to eq '1'
334
+ expect(entry
335
+ .child.child.child.child
336
+ .child.child.child.child
337
+ .child.child.child.child
338
+ .child.child.child.child
339
+ .child.child.child.child.title).to eq '1'
340
+ }
341
+ end
342
+ end
343
+
286
344
  describe 'include resolution' do
345
+ it 'should not reuse objects by default' do
346
+ vcr('entry/include_resolution') {
347
+ entry = create_client.entry('nyancat', include: 2)
348
+
349
+ expect(entry.best_friend.name).to eq 'Happy Cat'
350
+ expect(entry.best_friend.best_friend).not_to be(entry)
351
+ }
352
+ end
287
353
  it 'defaults to 20 depth' do
288
354
  vcr('entry/include_resolution') {
289
355
  entry = create_client.entry('nyancat', include: 2)
@@ -329,6 +395,34 @@ describe Contentful::Entry do
329
395
  .best_friend.best_friend).to be_a ::Contentful::Link
330
396
  }
331
397
  end
398
+ it 'works on nested structures with unique objects' do
399
+ vcr('entry/include_resolution_uniques') {
400
+ entry = create_client(
401
+ space: 'v7cxgyxt0w5x',
402
+ access_token: '96e5d256e9a5349ce30e84356597e409f8f1bb485cb4719285b555e0f78aa27e',
403
+ ).entry('1nLXjjWvk4MEeWeQCWmymc', include: 10)
404
+
405
+ expect(entry.title).to eq '1'
406
+ expect(entry
407
+ .child.child
408
+ .child.child
409
+ .child.child
410
+ .child.child
411
+ .child.title).to eq '10'
412
+ expect(entry
413
+ .child.child
414
+ .child.child
415
+ .child.child
416
+ .child.child
417
+ .child.child.title).to eq '1'
418
+ expect(entry
419
+ .child.child.child.child
420
+ .child.child.child.child
421
+ .child.child.child.child
422
+ .child.child.child.child
423
+ .child.child.child.child.title).to eq '1'
424
+ }
425
+ end
332
426
  end
333
427
 
334
428
  describe 'issues' do
@@ -0,0 +1,81 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: get
5
+ uri: https://cdn.contentful.com/spaces/v7cxgyxt0w5x/entries?include=10&sys.id=1nLXjjWvk4MEeWeQCWmymc
6
+ body:
7
+ encoding: US-ASCII
8
+ string: ''
9
+ headers:
10
+ X-Contentful-User-Agent:
11
+ - sdk contentful.rb/2.3.0; platform ruby/2.4.1; os macOS/16;
12
+ Authorization:
13
+ - Bearer 96e5d256e9a5349ce30e84356597e409f8f1bb485cb4719285b555e0f78aa27e
14
+ Content-Type:
15
+ - application/vnd.contentful.delivery.v1+json
16
+ Accept-Encoding:
17
+ - gzip
18
+ Connection:
19
+ - close
20
+ Host:
21
+ - cdn.contentful.com
22
+ User-Agent:
23
+ - http.rb/2.2.2
24
+ response:
25
+ status:
26
+ code: 200
27
+ message: OK
28
+ headers:
29
+ Access-Control-Allow-Headers:
30
+ - Accept,Accept-Language,Authorization,Cache-Control,Content-Length,Content-Range,Content-Type,DNT,Destination,Expires,If-Match,If-Modified-Since,If-None-Match,Keep-Alive,Last-Modified,Origin,Pragma,Range,User-Agent,X-Http-Method-Override,X-Mx-ReqToken,X-Requested-With,X-Contentful-Version,X-Contentful-Content-Type,X-Contentful-Organization,X-Contentful-Skip-Transformation,X-Contentful-User-Agent,X-Contentful-Enable-Alpha-Feature
31
+ Access-Control-Allow-Methods:
32
+ - GET,HEAD,OPTIONS
33
+ Access-Control-Allow-Origin:
34
+ - "*"
35
+ Access-Control-Expose-Headers:
36
+ - Etag
37
+ Access-Control-Max-Age:
38
+ - '86400'
39
+ Cache-Control:
40
+ - max-age=0
41
+ Content-Encoding:
42
+ - gzip
43
+ Content-Type:
44
+ - application/vnd.contentful.delivery.v1+json
45
+ Etag:
46
+ - W/"61dce3f065024b11b5dce76f1e0b6c4d"
47
+ Server:
48
+ - Contentful
49
+ X-Content-Type-Options:
50
+ - nosniff
51
+ X-Contentful-Request-Id:
52
+ - d4bd8c6410d7abd46c1d4669c38f6357
53
+ Content-Length:
54
+ - '878'
55
+ Accept-Ranges:
56
+ - bytes
57
+ Date:
58
+ - Wed, 14 Feb 2018 10:17:53 GMT
59
+ Via:
60
+ - 1.1 varnish
61
+ Age:
62
+ - '0'
63
+ Connection:
64
+ - close
65
+ X-Served-By:
66
+ - cache-hhn1548-HHN
67
+ X-Cache:
68
+ - MISS
69
+ X-Cache-Hits:
70
+ - '0'
71
+ X-Timer:
72
+ - S1518603473.158289,VS0,VE398
73
+ Vary:
74
+ - Accept-Encoding
75
+ body:
76
+ encoding: ASCII-8BIT
77
+ string: !binary |-
78
+ H4sIAAAAAAAAA+2ZS4+bMBCA7/srVpy7yLwhN5SlyWo3jSI2omy1B0po6hBICORBV/nvNQ8TkpqsqDggBQ4oNh6PZ+xvxnY+7u7vqTAOqd79B/qJClG8dlCJUjcbK6ZQ3fFL0iZaRdYS1TNpKXThGhVAWlhCD0bJJ5CVYeR4SYc/0g6zbi+0pJrCtWUnqnCLrLI0lqQircRjeoG+SyU6Tw/S7ruv+Zj1tMeLBnCWmLOT7MM8PkRgLxwSq/BzLH6ndmYPlckw/sv3xcLYufxIcwxn0je82LNL/RcD0/xoE5c/2BvHipyZmviFYgEjPwD2geFfGdBjhB6QaEaR3soC2/WsnsDG2cEQrnw8J/nI7ZUfOX6Ue6Q51/ZL/RIdHDlh9LljlyvbWqbry/EfpjoWKHxP/YLOcnZajolVVASjTIY5c/FvuEymtjkbL2cxVZ4tBS4+DIP9ePHmae5ozsoAgq2rVdibr6NsaSXv95QT6NvL7cwpsZYpxKScWCHQUsELsWXmtJwJAjNI6DNqCsOruEFxoUxReUlgelbKgJMfuT99bS+6q+0YPO9Z9YzOSn6Q+msE8T1BoRlBKBOERK4xVCFCpihRX8lREy6vpqlw/CVP1xxeQVUeu/PIQCALKSvY4s9nxibQ1YTlJMYKm324CL2nTTxUtbk3Z0dePx6JZcgunHCK3Rg5vA5PQeEsu+WuIGWeJowj5Z/CuP8niR0eBo7R/8oZtjsyNTZkbSDrjZCEchFDS4CtQVKFSEcSzlJcC0hiyMG3FLJvE6WKPH6WvvBu899QdTUpIS54WuSUeiiRRDqUMEpsC1CqiL43j5IgqlAJdGfwZNpxIKsGUCc8bCQr8T1epnlBroFShUiHEkZJbgFKQfAszR7h7pvKW9vpNLQCGXb7O0pQTOHnUDCkLeuG7ACo4/lorDVEksDRMsfUIoko0pGESRJbQJL+bMbuU8zMBxaiaDJyJroudyclwexvZ/owXE9FMQByoJnQXJkNkcQpNKi1veN7RJGOpOI+D7QApYpb3pvf35FDTBMnJXQXx9BArLe9I4p0KGGUpBaQVHEiuHmSyNeaDZEk0rwk1tvekUQ6kjBJQhtIIp8Ibp4k8gGyGZJ4npZBvb+UiCIdSZgkpQ0kkU8EdUhK276j9/HuePcXnaKW8jEhAAA=
79
+ http_version:
80
+ recorded_at: Wed, 14 Feb 2018 10:17:52 GMT
81
+ recorded_with: VCR 3.0.3
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: contentful
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.0
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Contentful GmbH (Jan Lelis)
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2018-02-06 00:00:00.000000000 Z
13
+ date: 2018-02-14 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: http
@@ -455,6 +455,7 @@ files:
455
455
  - spec/fixtures/vcr_cassettes/entry.yml
456
456
  - spec/fixtures/vcr_cassettes/entry/custom_resource.yml
457
457
  - spec/fixtures/vcr_cassettes/entry/include_resolution.yml
458
+ - spec/fixtures/vcr_cassettes/entry/include_resolution_uniques.yml
458
459
  - spec/fixtures/vcr_cassettes/entry/json_objects.yml
459
460
  - spec/fixtures/vcr_cassettes/entry/json_objects_client.yml
460
461
  - spec/fixtures/vcr_cassettes/entry/marshal_138.yml
@@ -591,6 +592,7 @@ test_files:
591
592
  - spec/fixtures/vcr_cassettes/entry.yml
592
593
  - spec/fixtures/vcr_cassettes/entry/custom_resource.yml
593
594
  - spec/fixtures/vcr_cassettes/entry/include_resolution.yml
595
+ - spec/fixtures/vcr_cassettes/entry/include_resolution_uniques.yml
594
596
  - spec/fixtures/vcr_cassettes/entry/json_objects.yml
595
597
  - spec/fixtures/vcr_cassettes/entry/json_objects_client.yml
596
598
  - spec/fixtures/vcr_cassettes/entry/marshal_138.yml