contentful 0.8.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +3 -1
- data/.rubocop.yml +25 -0
- data/.rubocop_todo.yml +35 -0
- data/.travis.yml +5 -0
- data/.yardopts +4 -0
- data/CHANGELOG.md +12 -0
- data/Guardfile +18 -36
- data/README.md +34 -0
- data/Rakefile +5 -0
- data/contentful.gemspec +3 -0
- data/lib/contentful.rb +1 -0
- data/lib/contentful/array.rb +5 -2
- data/lib/contentful/asset.rb +15 -12
- data/lib/contentful/client.rb +90 -48
- data/lib/contentful/constants.rb +499 -497
- data/lib/contentful/dynamic_entry.rb +3 -0
- data/lib/contentful/entry.rb +40 -1
- data/lib/contentful/request.rb +3 -6
- data/lib/contentful/resource.rb +92 -69
- data/lib/contentful/resource/array_like.rb +10 -2
- data/lib/contentful/resource/asset_fields.rb +7 -0
- data/lib/contentful/resource/custom_resource.rb +29 -0
- data/lib/contentful/resource/fields.rb +20 -12
- data/lib/contentful/resource/system_properties.rb +7 -0
- data/lib/contentful/resource_builder.rb +24 -17
- data/lib/contentful/support.rb +5 -1
- data/lib/contentful/sync.rb +26 -15
- data/lib/contentful/sync_page.rb +12 -0
- data/lib/contentful/version.rb +3 -1
- data/spec/entry_spec.rb +116 -0
- data/spec/fixtures/vcr_cassettes/entry/custom_resource.yml +168 -0
- data/spec/fixtures/vcr_cassettes/entry/marshall.yml +317 -0
- data/spec/fixtures/vcr_cassettes/entry/raw.yml +237 -0
- metadata +113 -3
@@ -3,7 +3,9 @@ require_relative 'resource/fields'
|
|
3
3
|
require_relative 'location'
|
4
4
|
|
5
5
|
module Contentful
|
6
|
+
# Wrapper for Entries with Cached Content Types
|
6
7
|
class DynamicEntry < Entry
|
8
|
+
# Coercions from Contentful Types to Ruby native types
|
7
9
|
KNOWN_TYPES = {
|
8
10
|
'String' => :string,
|
9
11
|
'Text' => :string,
|
@@ -15,6 +17,7 @@ module Contentful
|
|
15
17
|
'Location' => Location
|
16
18
|
}
|
17
19
|
|
20
|
+
# @private
|
18
21
|
def self.create(content_type)
|
19
22
|
unless content_type.is_a? ContentType
|
20
23
|
content_type = ContentType.new(content_type)
|
data/lib/contentful/entry.rb
CHANGED
@@ -3,10 +3,49 @@ require_relative 'resource/fields'
|
|
3
3
|
|
4
4
|
module Contentful
|
5
5
|
# Resource class for Entry.
|
6
|
-
# https://www.contentful.com/developers/documentation/content-delivery-api/#entries
|
6
|
+
# @see _ https://www.contentful.com/developers/documentation/content-delivery-api/#entries
|
7
7
|
class Entry
|
8
8
|
include Contentful::Resource
|
9
9
|
include Contentful::Resource::SystemProperties
|
10
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
|
+
end
|
23
|
+
|
24
|
+
# @private
|
25
|
+
def raw_with_links
|
26
|
+
links = properties.keys.select { |property| known_link?(property) }
|
27
|
+
processed_raw = Marshal.load(Marshal.dump(raw)) # Deep Copy
|
28
|
+
raw['fields'].each do |k, v|
|
29
|
+
processed_raw['fields'][k] = links.include?(k.to_sym) ? send(snakify(k)) : v
|
30
|
+
end
|
31
|
+
|
32
|
+
processed_raw
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def known_link?(name)
|
38
|
+
field_name = name.to_sym
|
39
|
+
return true if known_contentful_object?(fields[field_name])
|
40
|
+
fields[field_name].is_a?(Enumerable) && known_contentful_object?(fields[field_name].first)
|
41
|
+
end
|
42
|
+
|
43
|
+
def known_contentful_object?(object)
|
44
|
+
(object.is_a?(Contentful::Entry) || object.is_a?(Contentful::Asset))
|
45
|
+
end
|
46
|
+
|
47
|
+
def snakify(name)
|
48
|
+
Contentful::Support.snakify(name).to_sym
|
49
|
+
end
|
11
50
|
end
|
12
51
|
end
|
data/lib/contentful/request.rb
CHANGED
@@ -8,11 +8,8 @@ module Contentful
|
|
8
8
|
def initialize(client, endpoint, query = {}, id = nil)
|
9
9
|
@client = client
|
10
10
|
@endpoint = endpoint
|
11
|
-
@absolute = true if @endpoint.start_with?('http')
|
12
11
|
|
13
|
-
@query = if query && !query.empty?
|
14
|
-
normalize_query(query)
|
15
|
-
end
|
12
|
+
@query = (normalize_query(query) if query && !query.empty?)
|
16
13
|
|
17
14
|
if id
|
18
15
|
@type = :single
|
@@ -25,7 +22,7 @@ module Contentful
|
|
25
22
|
|
26
23
|
# Returns the final URL, relative to a contentful space
|
27
24
|
def url
|
28
|
-
"#{@endpoint}#{
|
25
|
+
"#{@endpoint}#{@type == :single ? "/#{id}" : ''}"
|
29
26
|
end
|
30
27
|
|
31
28
|
# Delegates the actual HTTP work to the client
|
@@ -35,7 +32,7 @@ module Contentful
|
|
35
32
|
|
36
33
|
# Returns true if endpoint is an absolute url
|
37
34
|
def absolute?
|
38
|
-
|
35
|
+
@endpoint.start_with?('http')
|
39
36
|
end
|
40
37
|
|
41
38
|
# Returns a new Request object with the same data
|
data/lib/contentful/resource.rb
CHANGED
@@ -9,9 +9,14 @@ module Contentful
|
|
9
9
|
# You can define your own classes that behave like contentful resources:
|
10
10
|
# See examples/custom_classes.rb to see how.
|
11
11
|
#
|
12
|
-
# Take a look at examples/resource_mapping.rb on how to register them
|
13
|
-
#
|
12
|
+
# Take a look at examples/resource_mapping.rb on how to register them to be returned
|
13
|
+
# by the client by default
|
14
|
+
#
|
15
|
+
# @see _ examples/custom_classes.rb Custom Class as Resource
|
16
|
+
# @see _ examples/resource_mapping.rb Mapping a Custom Class
|
14
17
|
module Resource
|
18
|
+
# @private
|
19
|
+
# rubocop:disable Style/DoubleNegation
|
15
20
|
COERCIONS = {
|
16
21
|
string: ->(v) { v.to_s },
|
17
22
|
integer: ->(v) { v.to_i },
|
@@ -19,20 +24,33 @@ module Contentful
|
|
19
24
|
boolean: ->(v) { !!v },
|
20
25
|
date: ->(v) { DateTime.parse(v) }
|
21
26
|
}
|
27
|
+
# rubocop:enable Style/DoubleNegation
|
22
28
|
|
23
|
-
attr_reader :properties, :request, :client, :default_locale
|
29
|
+
attr_reader :properties, :request, :client, :default_locale, :raw
|
24
30
|
|
25
|
-
|
31
|
+
# @private
|
32
|
+
def initialize(object = nil,
|
33
|
+
request = nil,
|
34
|
+
client = nil,
|
35
|
+
default_locale = Contentful::Client::DEFAULT_CONFIGURATION[:default_locale])
|
26
36
|
self.class.update_coercions!
|
27
37
|
@default_locale = default_locale
|
28
38
|
|
29
|
-
@properties =
|
30
|
-
|
39
|
+
@properties = {}
|
40
|
+
self.class.property_coercions.keys.each do |property_name|
|
41
|
+
@properties[property_name] = nil
|
42
|
+
end
|
43
|
+
|
44
|
+
@properties = @properties.merge(
|
45
|
+
extract_from_object(object, :property,
|
46
|
+
self.class.property_coercions.keys)
|
47
|
+
)
|
31
48
|
@request = request
|
32
49
|
@client = client
|
33
|
-
@
|
50
|
+
@raw = object
|
34
51
|
end
|
35
52
|
|
53
|
+
# @private
|
36
54
|
def inspect(info = nil)
|
37
55
|
properties_info = properties.empty? ? '' : " @properties=#{properties.inspect}"
|
38
56
|
"#<#{self.class}:#{properties_info}#{info}>"
|
@@ -43,6 +61,9 @@ module Contentful
|
|
43
61
|
false
|
44
62
|
end
|
45
63
|
|
64
|
+
# Returns true if resource is localized
|
65
|
+
#
|
66
|
+
# @return [Boolean]
|
46
67
|
def localized?(value)
|
47
68
|
return false unless value.is_a? ::Hash
|
48
69
|
value.keys.any? { |possible_locale| Contentful::Constants::KNOWN_LOCALES.include?(possible_locale) }
|
@@ -68,6 +89,57 @@ module Contentful
|
|
68
89
|
end
|
69
90
|
end
|
70
91
|
|
92
|
+
# Register the resources properties on class level by using the #property method
|
93
|
+
# @private
|
94
|
+
module ClassMethods
|
95
|
+
# By default, fields come flattened in the current locale. This is different for sync
|
96
|
+
def property_coercions
|
97
|
+
@property_coercions ||= {}
|
98
|
+
end
|
99
|
+
|
100
|
+
# Defines which properties of a resource your class expects
|
101
|
+
# Define them in :camelCase, they will be available as #snake_cased methods
|
102
|
+
#
|
103
|
+
# You can pass in a second "type" argument:
|
104
|
+
# - If it is a class, it will be initialized for the property
|
105
|
+
# - Symbols are looked up in the COERCION constant for a lambda that
|
106
|
+
# defines a type conversion to apply
|
107
|
+
#
|
108
|
+
# Note: This second argument is not meant for contentful sub-resources,
|
109
|
+
# but for structured objects (like locales in a space)
|
110
|
+
# Sub-resources are handled by the resource builder
|
111
|
+
def property(name, property_class = nil)
|
112
|
+
property_coercions[name.to_sym] = property_class
|
113
|
+
define_method Contentful::Support.snakify(name) do
|
114
|
+
properties[name.to_sym]
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Ensure inherited classes pick up coercions
|
119
|
+
def update_coercions!
|
120
|
+
return if @coercions_updated
|
121
|
+
|
122
|
+
if superclass.respond_to? :property_coercions
|
123
|
+
@property_coercions = superclass.property_coercions.dup.merge(@property_coercions || {})
|
124
|
+
end
|
125
|
+
|
126
|
+
if superclass.respond_to? :sys_coercions
|
127
|
+
@sys_coercions = superclass.sys_coercions.dup.merge(@sys_coercions || {})
|
128
|
+
end
|
129
|
+
|
130
|
+
if superclass.respond_to? :fields_coercions
|
131
|
+
@fields_coercions = superclass.fields_coercions.dup.merge(@fields_coercions || {})
|
132
|
+
end
|
133
|
+
|
134
|
+
@coercions_updated = true
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# @private
|
139
|
+
def self.included(base)
|
140
|
+
base.extend(ClassMethods)
|
141
|
+
end
|
142
|
+
|
71
143
|
private
|
72
144
|
|
73
145
|
def initialize_fields_for_localized_resource(object)
|
@@ -107,11 +179,11 @@ module Contentful
|
|
107
179
|
elsif value.is_a? ::Array
|
108
180
|
value.map { |v| coerce_or_create_class(v, what) }
|
109
181
|
elsif should_coerce_hash?(value)
|
110
|
-
::Hash[value.map
|
182
|
+
::Hash[value.map do |k, v|
|
111
183
|
to_coerce = v.is_a?(Hash) ? v : v.to_s
|
112
184
|
coercion = v.is_a?(Numeric) ? v : coerce_or_create_class(to_coerce, what)
|
113
185
|
[k.to_sym, coercion]
|
114
|
-
|
186
|
+
end]
|
115
187
|
else
|
116
188
|
coerce_or_create_class(value, what)
|
117
189
|
end
|
@@ -119,23 +191,23 @@ module Contentful
|
|
119
191
|
|
120
192
|
def should_coerce_hash?(value)
|
121
193
|
value.is_a?(::Hash) &&
|
122
|
-
!
|
123
|
-
!
|
124
|
-
!
|
125
|
-
!
|
126
|
-
!
|
194
|
+
!is_a?(Asset) &&
|
195
|
+
!is_a?(Field) &&
|
196
|
+
!location?(value) &&
|
197
|
+
!link?(value) &&
|
198
|
+
!image?(value)
|
127
199
|
end
|
128
200
|
|
129
|
-
def
|
130
|
-
value.
|
201
|
+
def location?(value)
|
202
|
+
value.key?('lat') || value.key?('lon')
|
131
203
|
end
|
132
204
|
|
133
|
-
def
|
134
|
-
value.
|
205
|
+
def link?(value)
|
206
|
+
value.key?('sys') && value['sys']['type'] == 'Link'
|
135
207
|
end
|
136
208
|
|
137
|
-
def
|
138
|
-
value.
|
209
|
+
def image?(value)
|
210
|
+
value.key?('image')
|
139
211
|
end
|
140
212
|
|
141
213
|
def coerce_or_create_class(value, what)
|
@@ -148,54 +220,5 @@ module Contentful
|
|
148
220
|
value
|
149
221
|
end
|
150
222
|
end
|
151
|
-
|
152
|
-
# Register the resources properties on class level by using the #property method
|
153
|
-
module ClassMethods
|
154
|
-
# By default, fields come flattened in the current locale. This is different for sync
|
155
|
-
def property_coercions
|
156
|
-
@property_coercions ||= {}
|
157
|
-
end
|
158
|
-
|
159
|
-
# Defines which properties of a resource your class expects
|
160
|
-
# Define them in :camelCase, they will be available as #snake_cased methods
|
161
|
-
#
|
162
|
-
# You can pass in a second "type" argument:
|
163
|
-
# - If it is a class, it will be initialized for the property
|
164
|
-
# - Symbols are looked up in the COERCION constant for a lambda that
|
165
|
-
# defines a type conversion to apply
|
166
|
-
#
|
167
|
-
# Note: This second argument is not meant for contentful sub-resources,
|
168
|
-
# but for structured objects (like locales in a space)
|
169
|
-
# Sub-resources are handled by the resource builder
|
170
|
-
def property(name, property_class = nil)
|
171
|
-
property_coercions[name.to_sym] = property_class
|
172
|
-
define_method Contentful::Support.snakify(name) do
|
173
|
-
properties[name.to_sym]
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
# Ensure inherited classes pick up coercions
|
178
|
-
def update_coercions!
|
179
|
-
return if @coercions_updated
|
180
|
-
|
181
|
-
if superclass.respond_to? :property_coercions
|
182
|
-
@property_coercions = superclass.property_coercions.dup.merge(@property_coercions || {})
|
183
|
-
end
|
184
|
-
|
185
|
-
if superclass.respond_to? :sys_coercions
|
186
|
-
@sys_coercions = superclass.sys_coercions.dup.merge(@sys_coercions || {})
|
187
|
-
end
|
188
|
-
|
189
|
-
if superclass.respond_to? :fields_coercions
|
190
|
-
@fields_coercions = superclass.fields_coercions.dup.merge(@fields_coercions || {})
|
191
|
-
end
|
192
|
-
|
193
|
-
@coercions_updated = true
|
194
|
-
end
|
195
|
-
end
|
196
|
-
|
197
|
-
def self.included(base)
|
198
|
-
base.extend(ClassMethods)
|
199
|
-
end
|
200
223
|
end
|
201
224
|
end
|
@@ -6,26 +6,34 @@ module Contentful
|
|
6
6
|
include Enumerable
|
7
7
|
|
8
8
|
# Returns true for array-like resources
|
9
|
+
#
|
10
|
+
# @return [true]
|
9
11
|
def array?
|
10
12
|
true
|
11
13
|
end
|
12
14
|
|
13
15
|
# Delegates to items#each
|
16
|
+
#
|
17
|
+
# @yield [Contentful::Entry, Contentful::Asset]
|
14
18
|
def each_item(&block)
|
15
19
|
items.each(&block)
|
16
20
|
end
|
17
|
-
|
21
|
+
alias each each_item
|
18
22
|
|
19
23
|
# Delegates to items#empty?
|
24
|
+
#
|
25
|
+
# @return [Boolean]
|
20
26
|
def empty?
|
21
27
|
items.empty?
|
22
28
|
end
|
23
29
|
|
24
30
|
# Delegetes to items#size
|
31
|
+
#
|
32
|
+
# @return [Number]
|
25
33
|
def size
|
26
34
|
items.size
|
27
35
|
end
|
28
|
-
|
36
|
+
alias length size
|
29
37
|
end
|
30
38
|
end
|
31
39
|
end
|
@@ -6,6 +6,7 @@ module Contentful
|
|
6
6
|
#
|
7
7
|
# It depends on system properties being available
|
8
8
|
module AssetFields
|
9
|
+
# Special field coercions for Asset.
|
9
10
|
FIELDS_COERCIONS = {
|
10
11
|
title: :string,
|
11
12
|
description: :string,
|
@@ -13,16 +14,20 @@ module Contentful
|
|
13
14
|
}
|
14
15
|
|
15
16
|
# Returns all fields of the asset
|
17
|
+
#
|
18
|
+
# @return [Hash] localized fields
|
16
19
|
def fields(wanted_locale = default_locale)
|
17
20
|
@fields[locale || wanted_locale]
|
18
21
|
end
|
19
22
|
|
23
|
+
# @private
|
20
24
|
def initialize(object, *)
|
21
25
|
super
|
22
26
|
|
23
27
|
initialize_fields_for_localized_resource(object)
|
24
28
|
end
|
25
29
|
|
30
|
+
# @private
|
26
31
|
def inspect(info = nil)
|
27
32
|
if fields.empty?
|
28
33
|
super(info)
|
@@ -31,12 +36,14 @@ module Contentful
|
|
31
36
|
end
|
32
37
|
end
|
33
38
|
|
39
|
+
# @private
|
34
40
|
module ClassMethods
|
35
41
|
def fields_coercions
|
36
42
|
FIELDS_COERCIONS
|
37
43
|
end
|
38
44
|
end
|
39
45
|
|
46
|
+
# @private
|
40
47
|
def self.included(base)
|
41
48
|
base.extend(ClassMethods)
|
42
49
|
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Contentful
|
2
|
+
module Resource
|
3
|
+
# Module for simplifying Custom Resource Creation
|
4
|
+
# Allows auto-mapping of fields to properties and properties to fields
|
5
|
+
module CustomResource
|
6
|
+
# @private
|
7
|
+
def initialize(*)
|
8
|
+
super
|
9
|
+
|
10
|
+
update_mappings!
|
11
|
+
end
|
12
|
+
|
13
|
+
# @private
|
14
|
+
def update_mappings!
|
15
|
+
properties.keys.each do |name|
|
16
|
+
define_singleton_method Contentful::Support.snakify(name).to_sym do |wanted_locale = default_locale|
|
17
|
+
properties[name] ||= fields(wanted_locale)[name]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# @private
|
23
|
+
def marshal_load(raw_object)
|
24
|
+
super raw_object
|
25
|
+
update_mappings!
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -8,12 +8,16 @@ module Contentful
|
|
8
8
|
# It depends on system properties being available
|
9
9
|
module Fields
|
10
10
|
# Returns all fields of the asset
|
11
|
+
#
|
12
|
+
# @return [Hash] fields for Resource on selected locale
|
11
13
|
def fields(wanted_locale = default_locale)
|
12
14
|
wanted_locale = wanted_locale.to_s
|
13
|
-
@fields.
|
15
|
+
@fields.key?(wanted_locale) ? @fields[wanted_locale] : @fields[locale]
|
14
16
|
end
|
15
17
|
|
16
18
|
# Returns all fields of the asset with locales nested by field
|
19
|
+
#
|
20
|
+
# @return [Hash] fields for Resource grouped by field name
|
17
21
|
def fields_with_locales
|
18
22
|
remapped_fields = {}
|
19
23
|
locales.each do |locale|
|
@@ -26,11 +30,26 @@ module Contentful
|
|
26
30
|
remapped_fields
|
27
31
|
end
|
28
32
|
|
33
|
+
# @private
|
34
|
+
module ClassMethods
|
35
|
+
# No coercions, since no content type available
|
36
|
+
def fields_coercions
|
37
|
+
{}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# @private
|
42
|
+
def self.included(base)
|
43
|
+
base.extend(ClassMethods)
|
44
|
+
end
|
45
|
+
|
46
|
+
# @private
|
29
47
|
def initialize(object = nil, *)
|
30
48
|
super
|
31
49
|
extract_fields_from_object! object if object
|
32
50
|
end
|
33
51
|
|
52
|
+
# @private
|
34
53
|
def inspect(info = nil)
|
35
54
|
if fields.empty?
|
36
55
|
super(info)
|
@@ -48,17 +67,6 @@ module Contentful
|
|
48
67
|
def extract_fields_from_object!(object)
|
49
68
|
initialize_fields_for_localized_resource(object)
|
50
69
|
end
|
51
|
-
|
52
|
-
module ClassMethods
|
53
|
-
# No coercions, since no content type available
|
54
|
-
def fields_coercions
|
55
|
-
{}
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
def self.included(base)
|
60
|
-
base.extend(ClassMethods)
|
61
|
-
end
|
62
70
|
end
|
63
71
|
end
|
64
72
|
end
|