contentful 1.2.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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