contentful 1.2.2 → 2.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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/LICENSE.txt +1 -0
  4. data/README.md +8 -0
  5. data/contentful.gemspec +2 -1
  6. data/examples/custom_classes.rb +23 -26
  7. data/examples/raise_errors.rb +2 -2
  8. data/lib/contentful.rb +0 -1
  9. data/lib/contentful/array.rb +27 -19
  10. data/lib/contentful/array_like.rb +51 -0
  11. data/lib/contentful/asset.rb +43 -11
  12. data/lib/contentful/base_resource.rb +87 -0
  13. data/lib/contentful/client.rb +43 -34
  14. data/lib/contentful/coercions.rb +116 -0
  15. data/lib/contentful/content_type.rb +23 -8
  16. data/lib/contentful/content_type_cache.rb +26 -0
  17. data/lib/contentful/deleted_asset.rb +2 -5
  18. data/lib/contentful/deleted_entry.rb +2 -5
  19. data/lib/contentful/entry.rb +55 -33
  20. data/lib/contentful/error.rb +1 -1
  21. data/lib/contentful/field.rb +37 -9
  22. data/lib/contentful/fields_resource.rb +115 -0
  23. data/lib/contentful/file.rb +7 -8
  24. data/lib/contentful/link.rb +3 -6
  25. data/lib/contentful/locale.rb +6 -6
  26. data/lib/contentful/location.rb +7 -5
  27. data/lib/contentful/resource_builder.rb +72 -226
  28. data/lib/contentful/space.rb +16 -6
  29. data/lib/contentful/support.rb +41 -3
  30. data/lib/contentful/sync_page.rb +17 -10
  31. data/lib/contentful/version.rb +1 -1
  32. data/spec/array_spec.rb +4 -8
  33. data/spec/client_class_spec.rb +12 -23
  34. data/spec/client_configuration_spec.rb +13 -23
  35. data/spec/content_type_spec.rb +0 -5
  36. data/spec/entry_spec.rb +130 -125
  37. data/spec/error_requests_spec.rb +1 -1
  38. data/spec/field_spec.rb +0 -5
  39. data/spec/file_spec.rb +0 -5
  40. data/spec/fixtures/vcr_cassettes/entry.yml +54 -64
  41. data/spec/fixtures/vcr_cassettes/entry/include_resolution.yml +101 -0
  42. data/spec/fixtures/vcr_cassettes/entry/marshall.yml +227 -251
  43. data/spec/fixtures/vcr_cassettes/entry/raw.yml +88 -124
  44. data/spec/fixtures/vcr_cassettes/entry_locales.yml +56 -74
  45. data/spec/fixtures/vcr_cassettes/human.yml +63 -40
  46. data/spec/fixtures/vcr_cassettes/location.yml +99 -211
  47. data/spec/fixtures/vcr_cassettes/multi_locale_array_reference.yml +12 -16
  48. data/spec/fixtures/vcr_cassettes/not_found.yml +26 -21
  49. data/spec/fixtures/vcr_cassettes/nyancat.yml +53 -63
  50. data/spec/fixtures/vcr_cassettes/ratelimit.yml +1 -1
  51. data/spec/fixtures/vcr_cassettes/reloaded_entry.yml +54 -64
  52. data/spec/fixtures/vcr_cassettes/unauthorized.yml +1 -1
  53. data/spec/fixtures/vcr_cassettes/unavailable.yml +27 -15
  54. data/spec/link_spec.rb +3 -2
  55. data/spec/locale_spec.rb +0 -5
  56. data/spec/location_spec.rb +1 -6
  57. data/spec/request_spec.rb +3 -2
  58. data/spec/resource_building_spec.rb +10 -7
  59. data/spec/response_spec.rb +1 -1
  60. data/spec/space_spec.rb +0 -5
  61. data/spec/spec_helper.rb +3 -0
  62. data/spec/support/json_responses.rb +3 -3
  63. data/spec/sync_page_spec.rb +1 -6
  64. data/spec/sync_spec.rb +11 -7
  65. metadata +69 -20
  66. data/examples/dynamic_entries.rb +0 -124
  67. data/examples/resource_mapping.rb +0 -32
  68. data/lib/contentful/constants.rb +0 -504
  69. data/lib/contentful/dynamic_entry.rb +0 -57
  70. data/lib/contentful/resource.rb +0 -239
  71. data/lib/contentful/resource/array_like.rb +0 -39
  72. data/lib/contentful/resource/asset_fields.rb +0 -58
  73. data/lib/contentful/resource/custom_resource.rb +0 -29
  74. data/lib/contentful/resource/fields.rb +0 -73
  75. data/lib/contentful/resource/system_properties.rb +0 -55
  76. data/spec/coercions_spec.rb +0 -23
  77. data/spec/dynamic_entry_spec.rb +0 -75
  78. data/spec/resource_spec.rb +0 -79
@@ -2,6 +2,7 @@ require_relative 'request'
2
2
  require_relative 'response'
3
3
  require_relative 'resource_builder'
4
4
  require_relative 'sync'
5
+ require_relative 'content_type_cache'
5
6
 
6
7
  require 'http'
7
8
  require 'logger'
@@ -32,12 +33,13 @@ module Contentful
32
33
  proxy_password: nil,
33
34
  proxy_port: nil,
34
35
  max_rate_limit_retries: 1,
35
- max_rate_limit_wait: 60
36
+ max_rate_limit_wait: 60,
37
+ max_include_resolution_depth: 20
36
38
  }
37
39
  # Rate Limit Reset Header Key
38
40
  RATE_LIMIT_RESET_HEADER_KEY = 'x-contentful-ratelimit-reset'
39
41
 
40
- attr_reader :configuration, :dynamic_entry_cache, :logger, :proxy
42
+ attr_reader :configuration, :logger, :proxy
41
43
 
42
44
  # Wraps the actual HTTP request via proxy
43
45
  # @private
@@ -62,6 +64,7 @@ module Contentful
62
64
  # @option given_configuration [Number] :proxy_port
63
65
  # @option given_configuration [Number] :max_rate_limit_retries
64
66
  # @option given_configuration [Number] :max_rate_limit_wait
67
+ # @option given_configuration [Number] :max_include_resolution_depth
65
68
  # @option given_configuration [Boolean] :gzip_encoded
66
69
  # @option given_configuration [Boolean] :raw_mode
67
70
  # @option given_configuration [false, ::Logger] :logger
@@ -76,11 +79,7 @@ module Contentful
76
79
  validate_configuration!
77
80
  setup_logger
78
81
 
79
- if configuration[:dynamic_entries] == :auto
80
- update_dynamic_entry_cache!
81
- else
82
- @dynamic_entry_cache = {}
83
- end
82
+ update_dynamic_entry_cache! if configuration[:dynamic_entries] == :auto
84
83
  end
85
84
 
86
85
  # @private
@@ -140,7 +139,13 @@ module Contentful
140
139
  #
141
140
  # @return [Contentful::Entry]
142
141
  def entry(id, query = {})
143
- Request.new(self, '/entries', query, id).get
142
+ normalize_select!(query)
143
+ query['sys.id'] = id
144
+ entries = Request.new(self, '/entries', query).get
145
+
146
+ return entries if configuration[:raw_mode]
147
+
148
+ entries.first
144
149
  end
145
150
 
146
151
  # Gets a collection of entries
@@ -205,22 +210,23 @@ module Contentful
205
210
  # @private
206
211
  def get(request, build_resource = true)
207
212
  retries_left = configuration[:max_rate_limit_retries]
213
+ result = nil
208
214
  begin
209
215
  response = run_request(request)
210
216
 
211
217
  return response if !build_resource || configuration[:raw_mode]
212
218
 
213
- result = do_build_resource(response)
219
+ return fail_response(response) if response.status != :ok
214
220
 
215
- fail result if result.is_a?(Error) && configuration[:raise_errors]
221
+ result = do_build_resource(response)
222
+ rescue UnparsableResource => error
223
+ raise error if configuration[:raise_errors]
224
+ return error
216
225
  rescue Contentful::RateLimitExceeded => rate_limit_error
217
226
  reset_time = rate_limit_error.response.raw[RATE_LIMIT_RESET_HEADER_KEY].to_i
218
227
  if should_retry(retries_left, reset_time, configuration[:max_rate_limit_wait])
219
228
  retries_left -= 1
220
- retry_message = 'Contentful API Rate Limit Hit! '
221
- retry_message += "Retrying - Retries left: #{retries_left}"
222
- retry_message += "- Time until reset (seconds): #{reset_time}"
223
- logger.info(retry_message) if logger
229
+ logger.info(retry_message(retries_left, reset_time)) if logger
224
230
  sleep(reset_time * Random.new.rand(1.0..1.2))
225
231
  retry
226
232
  end
@@ -231,6 +237,20 @@ module Contentful
231
237
  result
232
238
  end
233
239
 
240
+ # @private
241
+ def retry_message(retries_left, reset_time)
242
+ message = 'Contentful API Rate Limit Hit! '
243
+ message += "Retrying - Retries left: #{retries_left}"
244
+ message += "- Time until reset (seconds): #{reset_time}"
245
+ message
246
+ end
247
+
248
+ # @private
249
+ def fail_response(response)
250
+ fail response.object if configuration[:raise_errors]
251
+ response.object
252
+ end
253
+
234
254
  # @private
235
255
  def should_retry(retries_left, reset_time, max_wait)
236
256
  retries_left > 0 && max_wait > reset_time
@@ -256,11 +276,11 @@ module Contentful
256
276
  def do_build_resource(response)
257
277
  logger.debug(response: response) if logger
258
278
  configuration[:resource_builder].new(
259
- self,
260
- response,
261
- configuration[:resource_mapping],
262
- configuration[:entry_mapping],
263
- configuration[:default_locale]
279
+ response.object,
280
+ configuration,
281
+ (response.request.query || {}).fetch(:locale, nil) == '*',
282
+ 0,
283
+ response.request.endpoint
264
284
  ).run
265
285
  end
266
286
 
@@ -268,21 +288,16 @@ module Contentful
268
288
  # See README for details.
269
289
  # @private
270
290
  def update_dynamic_entry_cache!
271
- @dynamic_entry_cache = Hash[
272
- content_types(limit: 1000).map do |ct|
273
- [
274
- ct.id.to_sym,
275
- DynamicEntry.create(ct)
276
- ]
277
- end
278
- ]
291
+ content_types(limit: 1000).map do |ct|
292
+ ContentTypeCache.cache_set(configuration[:space], ct.id, ct)
293
+ end
279
294
  end
280
295
 
281
296
  # Use this method to manually register a dynamic entry
282
297
  # See examples/dynamic_entries.rb
283
298
  # @private
284
299
  def register_dynamic_entry(key, klass)
285
- @dynamic_entry_cache[key.to_sym] = klass
300
+ ContentTypeCache.cache_set(configuration[:space], key, klass)
286
301
  end
287
302
 
288
303
  # Create a new synchronisation object
@@ -317,19 +332,13 @@ module Contentful
317
332
 
318
333
  def validate_configuration!
319
334
  fail ArgumentError, 'You will need to initialize a client with a :space' if configuration[:space].empty?
320
-
321
335
  fail ArgumentError, 'You will need to initialize a client with an :access_token' if configuration[:access_token].empty?
322
-
323
336
  fail ArgumentError, 'The client configuration needs to contain an :api_url' if configuration[:api_url].empty?
324
-
325
337
  fail ArgumentError, 'The client configuration needs to contain a :default_locale' if configuration[:default_locale].empty?
326
-
327
338
  fail ArgumentError, 'The :api_version must be a positive number or nil' unless configuration[:api_version].to_i >= 0
328
-
329
339
  fail ArgumentError, 'The authentication mechanism must be :header or :query_string' unless [:header, :query_string].include?(
330
340
  configuration[:authentication_mechanism]
331
341
  )
332
-
333
342
  fail ArgumentError, 'The :dynamic_entries mode must be :auto or :manual' unless [:auto, :manual].include?(
334
343
  configuration[:dynamic_entries]
335
344
  )
@@ -0,0 +1,116 @@
1
+ require_relative 'location'
2
+
3
+ module Contentful
4
+ # Basic Coercion
5
+ class BaseCoercion
6
+ attr_reader :value, :options
7
+ def initialize(value, options = {})
8
+ @value = value
9
+ @options = options
10
+ end
11
+
12
+ # Coerces value
13
+ def coerce
14
+ value
15
+ end
16
+ end
17
+
18
+ # Coercion for String Types
19
+ class StringCoercion < BaseCoercion
20
+ # Coerces value to String
21
+ def coerce
22
+ value.to_s
23
+ end
24
+ end
25
+
26
+ # Coercion for Text Types
27
+ class TextCoercion < StringCoercion; end
28
+
29
+ # Coercion for Symbol Types
30
+ class SymbolCoercion < StringCoercion; end
31
+
32
+ # Coercion for Integer Types
33
+ class IntegerCoercion < BaseCoercion
34
+ # Coerces value to Integer
35
+ def coerce
36
+ value.to_i
37
+ end
38
+ end
39
+
40
+ # Coercion for Float Types
41
+ class FloatCoercion < BaseCoercion
42
+ # Coerces value to Float
43
+ def coerce
44
+ value.to_f
45
+ end
46
+ end
47
+
48
+ # Coercion for Boolean Types
49
+ class BooleanCoercion < BaseCoercion
50
+ # Coerces value to Boolean
51
+ def coerce
52
+ # rubocop:disable Style/DoubleNegation
53
+ !!value
54
+ # rubocop:enable Style/DoubleNegation
55
+ end
56
+ end
57
+
58
+ # Coercion for Date Types
59
+ class DateCoercion < BaseCoercion
60
+ # Coerces value to DateTime
61
+ def coerce
62
+ DateTime.parse(value)
63
+ end
64
+ end
65
+
66
+ # Coercion for Location Types
67
+ class LocationCoercion < BaseCoercion
68
+ # Coerces value to Location
69
+ def coerce
70
+ Location.new(value)
71
+ end
72
+ end
73
+
74
+ # Coercion for Object Types
75
+ class ObjectCoercion < BaseCoercion
76
+ # Coerces value to hash, symbolizing each key
77
+ def coerce
78
+ symbolize_recursive(value)
79
+ end
80
+
81
+ private
82
+
83
+ def symbolize_recursive(hash)
84
+ {}.tap do |h|
85
+ hash.each { |key, value| h[key.to_sym] = map_value(value) }
86
+ end
87
+ end
88
+
89
+ def map_value(thing)
90
+ case thing
91
+ when Hash
92
+ symbolize_recursive(thing)
93
+ when Array
94
+ thing.map { |v| map_value(v) }
95
+ else
96
+ thing
97
+ end
98
+ end
99
+ end
100
+
101
+ # Coercion for Link Types
102
+ # Nothing should be done here as include resolution is handled within
103
+ # entries due to depth handling (explained within Entry).
104
+ # Only present as a placeholder for proper resolution within ContentType.
105
+ class LinkCoercion < BaseCoercion; end
106
+
107
+ # Coercion for Array Types
108
+ class ArrayCoercion < BaseCoercion
109
+ # Coerces value for each element
110
+ def coerce
111
+ value.map do |e|
112
+ options[:coercion_class].new(e).coerce
113
+ end
114
+ end
115
+ end
116
+ end
@@ -1,16 +1,31 @@
1
- require_relative 'resource'
1
+ require_relative 'base_resource'
2
2
  require_relative 'field'
3
+ require_relative 'support'
3
4
 
4
5
  module Contentful
5
6
  # Resource Class for Content Types
6
7
  # https://www.contentful.com/developers/documentation/content-delivery-api/#content-types
7
- class ContentType
8
- include Contentful::Resource
9
- include Contentful::Resource::SystemProperties
8
+ class ContentType < BaseResource
9
+ attr_reader :name, :description, :fields, :display_field
10
10
 
11
- property :name, :string
12
- property :description, :string
13
- property :fields, Field
14
- property :displayField, :string
11
+ def initialize(item, *)
12
+ super
13
+
14
+ @name = item.fetch('name', nil)
15
+ @description = item.fetch('description', nil)
16
+ @fields = item.fetch('fields', []).map { |field| Field.new(field) }
17
+ @display_field = item.fetch('displayField', nil)
18
+ end
19
+
20
+ # Field definition for field
21
+ def field_for(field_id)
22
+ fields.detect { |f| Support.snakify(f.id) == Support.snakify(field_id) }
23
+ end
24
+
25
+ protected
26
+
27
+ def repr_name
28
+ "#{super}[#{name}]"
29
+ end
15
30
  end
16
31
  end
@@ -0,0 +1,26 @@
1
+ module Contentful
2
+ # Cache for Content Types
3
+ class ContentTypeCache
4
+ @cache = {}
5
+
6
+ class << self
7
+ attr_reader :cache
8
+ end
9
+
10
+ # Clears the Content Type Cache
11
+ def self.clear!
12
+ @cache = {}
13
+ end
14
+
15
+ # Gets a Content Type from the Cache
16
+ def self.cache_get(space_id, content_type_id)
17
+ @cache.fetch(space_id, {}).fetch(content_type_id.to_sym, nil)
18
+ end
19
+
20
+ # Sets a Content Type in the Cache
21
+ def self.cache_set(space_id, content_type_id, klass)
22
+ @cache[space_id] ||= {}
23
+ @cache[space_id][content_type_id.to_sym] = klass
24
+ end
25
+ end
26
+ end
@@ -1,10 +1,7 @@
1
- require_relative 'resource'
1
+ require_relative 'base_resource'
2
2
 
3
3
  module Contentful
4
4
  # Resource class for deleted entries
5
5
  # https://www.contentful.com/developers/documentation/content-delivery-api/http/#sync-item-types
6
- class DeletedAsset
7
- include Contentful::Resource
8
- include Contentful::Resource::SystemProperties
9
- end
6
+ class DeletedAsset < BaseResource; end
10
7
  end
@@ -1,10 +1,7 @@
1
- require_relative 'resource'
1
+ require_relative 'base_resource'
2
2
 
3
3
  module Contentful
4
4
  # Resource class for deleted entries
5
5
  # https://www.contentful.com/developers/documentation/content-delivery-api/http/#sync-item-types
6
- class DeletedEntry
7
- include Contentful::Resource
8
- include Contentful::Resource::SystemProperties
9
- end
6
+ class DeletedEntry < BaseResource; end
10
7
  end
@@ -1,38 +1,10 @@
1
- require_relative 'resource'
2
- require_relative 'resource/fields'
1
+ require_relative 'fields_resource'
2
+ require_relative 'content_type_cache'
3
3
 
4
4
  module Contentful
5
5
  # Resource class for Entry.
6
6
  # @see _ https://www.contentful.com/developers/documentation/content-delivery-api/#entries
7
- class Entry
8
- include Contentful::Resource
9
- include Contentful::Resource::SystemProperties
10
- include Contentful::Resource::Fields
11
-
12
- # @private
13
- def marshal_dump
14
- raw_with_links
15
- end
16
-
17
- # @private
18
- def marshal_load(raw_object)
19
- @properties = extract_from_object(raw_object, :property, self.class.property_coercions.keys)
20
- @sys = raw_object.key?('sys') ? extract_from_object(raw_object['sys'], :sys) : {}
21
- extract_fields_from_object!(raw_object)
22
- @raw = raw_object
23
- end
24
-
25
- # @private
26
- def raw_with_links
27
- links = properties.keys.select { |property| known_link?(property) }
28
- processed_raw = raw.clone
29
- raw['fields'].each do |k, v|
30
- processed_raw['fields'][k] = links.include?(k.to_sym) ? send(snakify(k)) : v
31
- end
32
-
33
- processed_raw
34
- end
35
-
7
+ class Entry < FieldsResource
36
8
  # Returns true for resources that are entries
37
9
  def entry?
38
10
  true
@@ -40,6 +12,54 @@ module Contentful
40
12
 
41
13
  private
42
14
 
15
+ def coerce(field_id, value, localized, includes)
16
+ return build_nested_resource(value, localized, includes) if Support.link?(value)
17
+ return coerce_link_array(value, localized, includes) if Support.link_array?(value)
18
+
19
+ content_type = ContentTypeCache.cache_get(sys[:space].id, sys[:content_type].id)
20
+
21
+ unless content_type.nil?
22
+ content_type_field = content_type.field_for(field_id)
23
+ return content_type_field.coerce(value) unless content_type_field.nil?
24
+ end
25
+
26
+ super(field_id, value, localized, includes)
27
+ end
28
+
29
+ def coerce_link_array(value, localized, includes)
30
+ items = []
31
+ value.each do |link|
32
+ items << build_nested_resource(link, localized, includes)
33
+ end
34
+
35
+ items
36
+ end
37
+
38
+ # Maximum include depth is 10 in the API, but we raise it to 20 (by default),
39
+ # in case one of the included items has a reference in an upper level,
40
+ # so we can keep the include chain for that object as well
41
+ # Any included object after the maximum include resolution depth will be just a Link
42
+ def build_nested_resource(value, localized, includes)
43
+ if @depth < @configuration.fetch(:max_include_resolution_depth, 20)
44
+ resource = Support.resource_for_link(value, includes)
45
+ return resolve_include(resource, localized, includes) unless resource.nil?
46
+ end
47
+
48
+ build_link(value)
49
+ end
50
+
51
+ def resolve_include(resource, localized, includes)
52
+ require_relative 'resource_builder'
53
+
54
+ ResourceBuilder.new(
55
+ resource,
56
+ @configuration.merge(includes_for_single: includes),
57
+ localized,
58
+ @depth + 1,
59
+ includes
60
+ ).run
61
+ end
62
+
43
63
  def known_link?(name)
44
64
  field_name = name.to_sym
45
65
  return true if known_contentful_object?(fields[field_name])
@@ -50,8 +70,10 @@ module Contentful
50
70
  (object.is_a?(Contentful::Entry) || object.is_a?(Contentful::Asset))
51
71
  end
52
72
 
53
- def snakify(name)
54
- Contentful::Support.snakify(name).to_sym
73
+ protected
74
+
75
+ def repr_name
76
+ "#{super}[#{sys[:content_type].id}]"
55
77
  end
56
78
  end
57
79
  end