wcc-contentful 0.2.2 → 0.3.0.pre.rc
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +0 -1
- data/README.md +181 -8
- data/app/controllers/wcc/contentful/webhook_controller.rb +42 -2
- data/app/jobs/wcc/contentful/delayed_sync_job.rb +52 -3
- data/app/jobs/wcc/contentful/webhook_enable_job.rb +43 -0
- data/bin/console +4 -3
- data/bin/rails +2 -0
- data/config/initializers/mime_types.rb +10 -1
- data/lib/wcc/contentful.rb +14 -142
- data/lib/wcc/contentful/client_ext.rb +17 -4
- data/lib/wcc/contentful/configuration.rb +25 -84
- data/lib/wcc/contentful/engine.rb +19 -0
- data/lib/wcc/contentful/exceptions.rb +25 -28
- data/lib/wcc/contentful/graphql.rb +0 -1
- data/lib/wcc/contentful/graphql/types.rb +1 -1
- data/lib/wcc/contentful/helpers.rb +3 -2
- data/lib/wcc/contentful/indexed_representation.rb +6 -0
- data/lib/wcc/contentful/model.rb +68 -34
- data/lib/wcc/contentful/model_builder.rb +65 -67
- data/lib/wcc/contentful/model_methods.rb +189 -0
- data/lib/wcc/contentful/model_singleton_methods.rb +83 -0
- data/lib/wcc/contentful/services.rb +146 -0
- data/lib/wcc/contentful/simple_client.rb +35 -33
- data/lib/wcc/contentful/simple_client/http_adapter.rb +9 -0
- data/lib/wcc/contentful/simple_client/management.rb +81 -0
- data/lib/wcc/contentful/simple_client/response.rb +61 -37
- data/lib/wcc/contentful/simple_client/typhoeus_adapter.rb +12 -0
- data/lib/wcc/contentful/store.rb +45 -18
- data/lib/wcc/contentful/store/base.rb +128 -8
- data/lib/wcc/contentful/store/cdn_adapter.rb +92 -22
- data/lib/wcc/contentful/store/lazy_cache_store.rb +94 -9
- data/lib/wcc/contentful/store/memory_store.rb +13 -8
- data/lib/wcc/contentful/store/postgres_store.rb +44 -11
- data/lib/wcc/contentful/sys.rb +28 -0
- data/lib/wcc/contentful/version.rb +1 -1
- data/wcc-contentful.gemspec +3 -9
- metadata +87 -107
- data/.circleci/config.yml +0 -51
- data/.gitignore +0 -26
- data/.rubocop.yml +0 -243
- data/.rubocop_todo.yml +0 -13
- data/.travis.yml +0 -5
- data/CHANGELOG.md +0 -45
- data/CODE_OF_CONDUCT.md +0 -74
- data/Guardfile +0 -58
- data/LICENSE.txt +0 -21
- data/Rakefile +0 -8
- data/lib/generators/wcc/USAGE +0 -24
- data/lib/generators/wcc/model_generator.rb +0 -90
- data/lib/generators/wcc/templates/.keep +0 -0
- data/lib/generators/wcc/templates/Procfile +0 -3
- data/lib/generators/wcc/templates/contentful_shell_wrapper +0 -385
- data/lib/generators/wcc/templates/menu/generated_add_menus.ts +0 -90
- data/lib/generators/wcc/templates/menu/models/menu.rb +0 -23
- data/lib/generators/wcc/templates/menu/models/menu_button.rb +0 -23
- data/lib/generators/wcc/templates/page/generated_add_pages.ts +0 -50
- data/lib/generators/wcc/templates/page/models/page.rb +0 -23
- data/lib/generators/wcc/templates/release +0 -9
- data/lib/generators/wcc/templates/wcc_contentful.rb +0 -17
- data/lib/wcc/contentful/model/menu.rb +0 -7
- data/lib/wcc/contentful/model/menu_button.rb +0 -15
- data/lib/wcc/contentful/model/page.rb +0 -8
- data/lib/wcc/contentful/model/redirect.rb +0 -19
- data/lib/wcc/contentful/model_validators.rb +0 -115
- data/lib/wcc/contentful/model_validators/dsl.rb +0 -165
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
4
3
|
module WCC::Contentful::Graphql::Types
|
@@ -17,6 +16,7 @@ module WCC::Contentful::Graphql::Types
|
|
17
16
|
return value if value.is_a? Array
|
18
17
|
return value.to_h if value.respond_to?(:to_h)
|
19
18
|
return JSON.parse(value) if value.is_a? String
|
19
|
+
|
20
20
|
raise ArgumentError, "Cannot coerce value '#{value}' to a hash"
|
21
21
|
}
|
22
22
|
end
|
@@ -5,9 +5,9 @@ module WCC::Contentful::Helpers
|
|
5
5
|
|
6
6
|
def content_type_from_raw(value)
|
7
7
|
case value.dig('sys', 'type')
|
8
|
-
when 'Entry'
|
8
|
+
when 'Entry', 'DeletedEntry'
|
9
9
|
value.dig('sys', 'contentType', 'sys', 'id')
|
10
|
-
when 'Asset'
|
10
|
+
when 'Asset', 'DeletedAsset'
|
11
11
|
'Asset'
|
12
12
|
else
|
13
13
|
raise ArgumentError, "Unknown content type '#{value.dig('sys', 'type') || 'null'}'"
|
@@ -27,6 +27,7 @@ module WCC::Contentful::Helpers
|
|
27
27
|
|
28
28
|
def content_type_from_constant(const)
|
29
29
|
return const.content_type if const.respond_to?(:content_type)
|
30
|
+
|
30
31
|
name = const.try(:name) || const.to_s
|
31
32
|
name.demodulize.camelize(:lower)
|
32
33
|
end
|
@@ -15,6 +15,7 @@ module WCC::Contentful
|
|
15
15
|
|
16
16
|
def []=(id, value)
|
17
17
|
raise ArgumentError unless value.is_a?(ContentType)
|
18
|
+
|
18
19
|
@types[id] = value
|
19
20
|
end
|
20
21
|
|
@@ -39,6 +40,7 @@ module WCC::Contentful
|
|
39
40
|
def ==(other)
|
40
41
|
my_keys = keys
|
41
42
|
return false unless my_keys == other.keys
|
43
|
+
|
42
44
|
my_keys.all? { |k| self[k] == other[k] }
|
43
45
|
end
|
44
46
|
|
@@ -54,6 +56,7 @@ module WCC::Contentful
|
|
54
56
|
def initialize(hash_or_id = nil)
|
55
57
|
@fields = {}
|
56
58
|
return unless hash_or_id
|
59
|
+
|
57
60
|
if hash_or_id.is_a?(String)
|
58
61
|
@name = hash_or_id
|
59
62
|
return
|
@@ -108,11 +111,13 @@ module WCC::Contentful
|
|
108
111
|
unless TYPES.include?(raw_type)
|
109
112
|
raise ArgumentError, "Unknown type #{raw_type}, expected one of: #{TYPES}"
|
110
113
|
end
|
114
|
+
|
111
115
|
@type = raw_type
|
112
116
|
end
|
113
117
|
|
114
118
|
def initialize(hash_or_id = nil)
|
115
119
|
return unless hash_or_id
|
120
|
+
|
116
121
|
if hash_or_id.is_a?(String)
|
117
122
|
@name = hash_or_id
|
118
123
|
return
|
@@ -128,6 +133,7 @@ module WCC::Contentful
|
|
128
133
|
unless TYPES.include?(raw_type)
|
129
134
|
raise ArgumentError, "Unknown type #{raw_type}, expected one of: #{TYPES}"
|
130
135
|
end
|
136
|
+
|
131
137
|
@type = raw_type
|
132
138
|
end
|
133
139
|
|
data/lib/wcc/contentful/model.rb
CHANGED
@@ -1,23 +1,50 @@
|
|
1
|
-
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
3
|
+
# This is the top layer of the WCC::Contentful gem. It exposes an API by which
|
4
|
+
# you can query for data from Contentful. The API is only accessible after calling
|
5
|
+
# WCC::Contentful.init!
|
6
|
+
#
|
7
|
+
# The WCC::Contentful::Model class is the base class for all auto-generated model
|
8
|
+
# classes. A model class represents a content type inside Contentful. For example,
|
9
|
+
# the "page" content type is represented by a class named WCC::Contentful::Model::Page
|
10
|
+
#
|
11
|
+
# This WCC::Contentful::Model::Page class exposes the following API methods:
|
12
|
+
# * {WCC::Contentful::ModelSingletonMethods#find Page.find(id)}
|
13
|
+
# finds a single Page by it's ID
|
14
|
+
# * {WCC::Contentful::ModelSingletonMethods#find_by Page.find_by(field: <value>)}
|
15
|
+
# finds a single Page with the matching value for the specified field
|
16
|
+
# * {WCC::Contentful::ModelSingletonMethods#find_all Page.find_all(field: <value>)}
|
17
|
+
# finds all instances of Page with the matching value for the specified field.
|
18
|
+
# It returns a lazy iterator of Page objects.
|
19
|
+
#
|
20
|
+
# The returned objects are instances of WCC::Contentful::Model::Page, or whatever
|
21
|
+
# constant exists in the registry for the page content type. You can register
|
22
|
+
# custom types to be instantiated for each content type. If a Model is subclassed,
|
23
|
+
# the subclass is automatically registered. This allows you to put models in your
|
24
|
+
# app's `app/models` directory:
|
25
|
+
#
|
26
|
+
# class Page < WCC::Contentful::Model::Page; end
|
27
|
+
#
|
28
|
+
# and then use the API via those models:
|
29
|
+
#
|
30
|
+
# # this returns a ::Page, not a WCC::Contentful::Model::Page
|
31
|
+
# Page.find_by(slug: 'foo')
|
32
|
+
#
|
33
|
+
# Furthermore, anytime links are automatically resolved, the registered classes will
|
34
|
+
# be used:
|
35
|
+
#
|
36
|
+
# Menu.find_by(name: 'home').buttons.first.linked_page # is a ::Page
|
37
|
+
#
|
38
|
+
# @api Model
|
4
39
|
class WCC::Contentful::Model
|
5
40
|
extend WCC::Contentful::Helpers
|
6
|
-
extend WCC::Contentful::ModelValidators
|
7
41
|
|
8
42
|
# The Model base class maintains a registry which is best expressed as a
|
9
43
|
# class var.
|
10
44
|
# rubocop:disable Style/ClassVars
|
11
45
|
|
12
46
|
class << self
|
13
|
-
|
14
|
-
# The configured store which executes all model queries against either the
|
15
|
-
# Contentful CDN or a locally-downloaded copy.
|
16
|
-
#
|
17
|
-
# See the {sync_store}[rdoc-ref:WCC::Contentful::Configuration.sync_store] parameter
|
18
|
-
# on the WCC::Contentful::Configuration class.
|
19
|
-
attr_accessor :store
|
20
|
-
attr_accessor :preview_store
|
47
|
+
include WCC::Contentful::ServiceAccessors
|
21
48
|
|
22
49
|
def const_missing(name)
|
23
50
|
raise WCC::Contentful::ContentTypeNotFoundError,
|
@@ -27,41 +54,48 @@ class WCC::Contentful::Model
|
|
27
54
|
|
28
55
|
@@registry = {}
|
29
56
|
|
30
|
-
def self.all_models
|
31
|
-
# TODO: this needs to use the registry but it's OK for now cause we only
|
32
|
-
# use it in specs
|
33
|
-
WCC::Contentful::Model.constants(false).map { |k| WCC::Contentful::Model.const_get(k) }
|
34
|
-
end
|
35
|
-
|
36
|
-
##
|
37
57
|
# Finds an Entry or Asset by ID in the configured contentful space
|
38
58
|
# and returns an initialized instance of the appropriate model type.
|
39
59
|
#
|
40
|
-
# Makes use of the
|
60
|
+
# Makes use of the {WCC::Contentful::Services#store configured store}
|
41
61
|
# to access the Contentful CDN.
|
42
62
|
def self.find(id, context = nil)
|
43
63
|
return unless raw = store.find(id)
|
44
64
|
|
65
|
+
new_from_raw(raw, context)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Creates a new initialized instance of the appropriate model type for the
|
69
|
+
# given raw value. The raw value must be the same format as returned from one
|
70
|
+
# of the stores for a given object.
|
71
|
+
def self.new_from_raw(raw, context = nil)
|
45
72
|
content_type = content_type_from_raw(raw)
|
73
|
+
const = resolve_constant(content_type)
|
74
|
+
const.new(raw, context)
|
75
|
+
end
|
46
76
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
const
|
58
|
-
|
77
|
+
# Accepts a content type ID as a string and returns the Ruby constant
|
78
|
+
# stored in the registry that represents this content type.
|
79
|
+
def self.resolve_constant(content_type)
|
80
|
+
const = @@registry[content_type]
|
81
|
+
return const if const
|
82
|
+
|
83
|
+
const_name = constant_from_content_type(content_type).to_s
|
84
|
+
begin
|
85
|
+
# The app may have defined a model and we haven't loaded it yet
|
86
|
+
const = Object.const_missing(const_name)
|
87
|
+
return const if const && const < WCC::Contentful::Model
|
88
|
+
rescue NameError => e
|
89
|
+
raise e unless e.message =~ /uninitialized constant #{const_name}/
|
90
|
+
|
91
|
+
nil
|
59
92
|
end
|
60
93
|
|
61
|
-
|
94
|
+
# Autoloading couldn't find their model - we'll register our own.
|
95
|
+
const = WCC::Contentful::Model.const_get(constant_from_content_type(content_type))
|
96
|
+
register_for_content_type(content_type, klass: const)
|
62
97
|
end
|
63
98
|
|
64
|
-
##
|
65
99
|
# Registers a class constant to be instantiated when resolving an instance
|
66
100
|
# of the given content type. This automatically happens for the first subclass
|
67
101
|
# of a generated model type, example:
|
@@ -82,19 +116,19 @@ class WCC::Contentful::Model
|
|
82
116
|
def self.register_for_content_type(content_type = nil, klass: nil)
|
83
117
|
klass ||= self
|
84
118
|
raise ArgumentError, "#{klass} must be a class constant!" unless klass.respond_to?(:new)
|
119
|
+
|
85
120
|
content_type ||= content_type_from_constant(klass)
|
86
121
|
|
87
122
|
@@registry[content_type] = klass
|
88
123
|
end
|
89
124
|
|
90
|
-
##
|
91
125
|
# Returns the current registry of content type names to constants.
|
92
126
|
def self.registry
|
93
127
|
return {} unless @@registry
|
128
|
+
|
94
129
|
@@registry.dup.freeze
|
95
130
|
end
|
96
131
|
|
97
|
-
##
|
98
132
|
# Checks if a content type has already been registered to a class and returns
|
99
133
|
# that class. If nil, the generated WCC::Contentful::Model::{content_type} class
|
100
134
|
# will be resolved for this content type.
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative './sys'
|
4
|
+
|
3
5
|
module WCC::Contentful
|
4
6
|
class ModelBuilder
|
5
7
|
include Helpers
|
@@ -22,11 +24,23 @@ module WCC::Contentful
|
|
22
24
|
|
23
25
|
# TODO: https://github.com/dkubb/ice_nine ?
|
24
26
|
typedef = typedef.deep_dup.freeze
|
25
|
-
fields = typedef.fields.keys
|
26
27
|
WCC::Contentful::Model.const_set(const,
|
27
28
|
Class.new(WCC::Contentful::Model) do
|
29
|
+
extend ModelSingletonMethods
|
30
|
+
include ModelMethods
|
28
31
|
include Helpers
|
29
32
|
|
33
|
+
const_set('ATTRIBUTES', typedef.fields.keys.map(&:to_sym).freeze)
|
34
|
+
const_set('FIELDS', typedef.fields.keys.freeze)
|
35
|
+
|
36
|
+
# Magic type in their system which has a separate endpoint
|
37
|
+
# but we represent in the same model space
|
38
|
+
if const == 'Asset'
|
39
|
+
define_singleton_method(:type) { 'Asset' }
|
40
|
+
else
|
41
|
+
define_singleton_method(:type) { 'Entry' }
|
42
|
+
end
|
43
|
+
|
30
44
|
define_singleton_method(:content_type) do
|
31
45
|
typedef.content_type
|
32
46
|
end
|
@@ -35,67 +49,45 @@ module WCC::Contentful
|
|
35
49
|
typedef
|
36
50
|
end
|
37
51
|
|
38
|
-
define_singleton_method(:find) do |id, context = nil|
|
39
|
-
raw = WCC::Contentful::Model.store.find(id)
|
40
|
-
new(raw, context) if raw.present?
|
41
|
-
end
|
42
|
-
|
43
|
-
define_singleton_method(:find_all) do |filter = nil, context = nil|
|
44
|
-
if filter
|
45
|
-
filter.transform_keys! { |k| k.to_s.camelize(:lower) }
|
46
|
-
bad_fields = filter.keys.reject { |k| fields.include?(k) }
|
47
|
-
raise ArgumentError, "These fields do not exist: #{bad_fields}" unless bad_fields.empty?
|
48
|
-
end
|
49
|
-
|
50
|
-
query = WCC::Contentful::Model.store.find_all(content_type: content_type)
|
51
|
-
query = query.apply(filter) if filter
|
52
|
-
query.map { |r| new(r, context) }
|
53
|
-
end
|
54
|
-
|
55
|
-
define_singleton_method(:find_by) do |filter, context = nil|
|
56
|
-
filter.transform_keys! { |k| k.to_s.camelize(:lower) }
|
57
|
-
bad_fields = filter.keys.reject { |k| fields.include?(k) }
|
58
|
-
raise ArgumentError, "These fields do not exist: #{bad_fields}" unless bad_fields.empty?
|
59
|
-
|
60
|
-
result =
|
61
|
-
if defined?(context[:preview]) && context[:preview] == true
|
62
|
-
WCC::Contentful::Model.preview_store.find_by(content_type: content_type, filter: filter)
|
63
|
-
else
|
64
|
-
WCC::Contentful::Model.store.find_by(content_type: content_type, filter: filter)
|
65
|
-
end
|
66
|
-
|
67
|
-
new(result, context) if result
|
68
|
-
end
|
69
|
-
|
70
|
-
define_singleton_method(:inherited) do |subclass|
|
71
|
-
# only register if it's not already registered
|
72
|
-
return if WCC::Contentful::Model.registered?(typedef.content_type)
|
73
|
-
WCC::Contentful::Model.register_for_content_type(typedef.content_type, klass: subclass)
|
74
|
-
end
|
75
|
-
|
76
52
|
define_method(:initialize) do |raw, context = nil|
|
77
53
|
ct = content_type_from_raw(raw)
|
78
54
|
if ct != typedef.content_type
|
79
55
|
raise ArgumentError, 'Wrong Content Type - ' \
|
80
56
|
"'#{raw.dig('sys', 'id')}' is a #{ct}, expected #{typedef.content_type}"
|
81
57
|
end
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
@
|
89
|
-
|
90
|
-
|
91
|
-
|
58
|
+
@raw = raw.freeze
|
59
|
+
|
60
|
+
created_at = raw.dig('sys', 'createdAt')
|
61
|
+
created_at = Time.parse(created_at) if created_at.present?
|
62
|
+
updated_at = raw.dig('sys', 'updatedAt')
|
63
|
+
updated_at = Time.parse(updated_at) if updated_at.present?
|
64
|
+
@sys = WCC::Contentful::Sys.new(
|
65
|
+
raw.dig('sys', 'id'),
|
66
|
+
raw.dig('sys', 'type'),
|
67
|
+
raw.dig('sys', 'locale') || context.try(:[], :locale) || 'en-US',
|
68
|
+
raw.dig('sys', 'space', 'sys', 'id'),
|
69
|
+
created_at,
|
70
|
+
updated_at,
|
71
|
+
raw.dig('sys', 'revision'),
|
72
|
+
OpenStruct.new(context).freeze
|
73
|
+
)
|
92
74
|
|
93
75
|
typedef.fields.each_value do |f|
|
94
|
-
raw_value = raw.dig('fields', f.name, @locale)
|
76
|
+
raw_value = raw.dig('fields', f.name, @sys.locale)
|
95
77
|
if raw_value.present?
|
96
78
|
case f.type
|
97
|
-
|
98
|
-
|
79
|
+
# DateTime is intentionally not parsed!
|
80
|
+
# a DateTime can be '2018-09-28', '2018-09-28T17:00:00', or '2018-09-28T17:00:00Z'
|
81
|
+
# depending entirely on the editor interface in Contentful. Trying to parse this
|
82
|
+
# requires an assumption of the correct time zone to place them in. At this point
|
83
|
+
# in the code we don't have that knowledge, so we're punting to app-defined models.
|
84
|
+
#
|
85
|
+
# As an example, a user enters '2018-09-28' into Contentful. That date is parsed as
|
86
|
+
# '2018-09-28T00:00:00Z' when system time is UTC (ex. on Heroku), but translating that
|
87
|
+
# date to US Central results in '2018-09-27' which is not what the user intentded.
|
88
|
+
#
|
89
|
+
# when :DateTime
|
90
|
+
# raw_value = Time.parse(raw_value).localtime
|
99
91
|
when :Int
|
100
92
|
raw_value = Integer(raw_value)
|
101
93
|
when :Float
|
@@ -109,11 +101,13 @@ module WCC::Contentful
|
|
109
101
|
end
|
110
102
|
end
|
111
103
|
|
112
|
-
attr_reader :
|
113
|
-
attr_reader :
|
114
|
-
|
115
|
-
|
116
|
-
|
104
|
+
attr_reader :sys
|
105
|
+
attr_reader :raw
|
106
|
+
delegate :id, to: :sys
|
107
|
+
delegate :created_at, to: :sys
|
108
|
+
delegate :updated_at, to: :sys
|
109
|
+
delegate :revision, to: :sys
|
110
|
+
delegate :space, to: :sys
|
117
111
|
|
118
112
|
# Make a field for each column:
|
119
113
|
typedef.fields.each_value do |f|
|
@@ -125,18 +119,21 @@ module WCC::Contentful
|
|
125
119
|
val = instance_variable_get(var_name + '_resolved')
|
126
120
|
return val if val.present?
|
127
121
|
|
128
|
-
|
129
|
-
|
130
|
-
val =
|
131
|
-
if val.is_a? Array
|
132
|
-
val.map { |v| WCC::Contentful::Model.find(v.dig('sys', 'id')) }
|
133
|
-
else
|
134
|
-
WCC::Contentful::Model.find(val.dig('sys', 'id'))
|
135
|
-
end
|
122
|
+
_resolve_field(name)
|
123
|
+
end
|
136
124
|
|
137
|
-
|
138
|
-
|
125
|
+
id_method_name = "#{name}_id"
|
126
|
+
if f.array
|
127
|
+
id_method_name = "#{name}_ids"
|
128
|
+
define_method(id_method_name) do
|
129
|
+
instance_variable_get(var_name)&.map { |link| link.dig('sys', 'id') }
|
130
|
+
end
|
131
|
+
else
|
132
|
+
define_method(id_method_name) do
|
133
|
+
instance_variable_get(var_name)&.dig('sys', 'id')
|
134
|
+
end
|
139
135
|
end
|
136
|
+
alias_method id_method_name.underscore, id_method_name
|
140
137
|
when :Coordinates
|
141
138
|
define_method(name) do
|
142
139
|
val = instance_variable_get(var_name)
|
@@ -162,6 +159,7 @@ module WCC::Contentful
|
|
162
159
|
instance_variable_get(var_name)
|
163
160
|
end
|
164
161
|
end
|
162
|
+
|
165
163
|
alias_method name.underscore, name
|
166
164
|
end
|
167
165
|
end)
|
@@ -0,0 +1,189 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This module is included by all {WCC::Contentful::Model models} and defines instance
|
4
|
+
# methods that are not dynamically generated.
|
5
|
+
#
|
6
|
+
# @api Model
|
7
|
+
module WCC::Contentful::ModelMethods
|
8
|
+
# Resolves all links in an entry to the specified depth.
|
9
|
+
#
|
10
|
+
# Each link in the entry is recursively retrieved from the store until the given
|
11
|
+
# depth is satisfied. Depth resolution is unlimited, circular references will
|
12
|
+
# be resolved to the same object.
|
13
|
+
#
|
14
|
+
# @param [Fixnum] depth how far to recursively resolve. Must be >= 1
|
15
|
+
# @param [Array<String, Symbol>] fields (optional) A subset of fields whose
|
16
|
+
# links should be resolved. Defaults to all fields.
|
17
|
+
# @param [Hash] context passed to the resolved model's `new` function to provide
|
18
|
+
# contextual information ex. current locale.
|
19
|
+
# See {WCC::Contentful::ModelSingletonMethods#find Model#find}, {WCC::Contentful::Sys#context}
|
20
|
+
# @param [Hash] options The remaining optional parameters, defined below
|
21
|
+
# @option options [Symbol] circular_reference Determines how circular references are
|
22
|
+
# handled. `:raise` causes a {WCC::Contentful::CircularReferenceError} to be raised,
|
23
|
+
# `:ignore` will cause the field to remain unresolved, and any other value (or nil)
|
24
|
+
# will cause the field to point to the previously resolved ruby object for that ID.
|
25
|
+
def resolve(depth: 1, fields: nil, context: {}, **options)
|
26
|
+
raise ArgumentError, "Depth must be > 0 (was #{depth})" unless depth && depth > 0
|
27
|
+
return self if resolved?(depth: depth, fields: fields)
|
28
|
+
|
29
|
+
fields = fields.map { |f| f.to_s.camelize(:lower) } if fields.present?
|
30
|
+
fields ||= self.class::FIELDS
|
31
|
+
|
32
|
+
typedef = self.class.content_type_definition
|
33
|
+
links = fields.select { |f| %i[Asset Link].include?(typedef.fields[f].type) }
|
34
|
+
|
35
|
+
raw_links =
|
36
|
+
links.any? do |field_name|
|
37
|
+
raw_value = raw.dig('fields', field_name, sys.locale)
|
38
|
+
if raw_value&.is_a? Array
|
39
|
+
raw_value.any? { |v| v&.dig('sys', 'type') == 'Link' }
|
40
|
+
elsif raw_value
|
41
|
+
raw_value.dig('sys', 'type') == 'Link'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
if raw_links
|
45
|
+
# use include param to do resolution
|
46
|
+
raw = self.class.store.find_by(content_type: self.class.content_type,
|
47
|
+
filter: { 'sys.id' => id },
|
48
|
+
options: { include: [depth, 10].min })
|
49
|
+
unless raw
|
50
|
+
raise WCC::Contentful::ResolveError, "Cannot find #{self.class.content_type} with ID #{id}"
|
51
|
+
end
|
52
|
+
|
53
|
+
@raw = raw.freeze
|
54
|
+
links.each { |f| instance_variable_set('@' + f, raw.dig('fields', f, sys.locale)) }
|
55
|
+
end
|
56
|
+
|
57
|
+
links.each { |f| _resolve_field(f, depth, context, options) }
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
# Determines whether the object has been resolved up to the prescribed depth.
|
62
|
+
def resolved?(depth: 1, fields: nil)
|
63
|
+
raise ArgumentError, "Depth must be > 0 (was #{depth})" unless depth && depth > 0
|
64
|
+
|
65
|
+
fields = fields.map { |f| f.to_s.camelize(:lower) } if fields.present?
|
66
|
+
fields ||= self.class::FIELDS
|
67
|
+
|
68
|
+
typedef = self.class.content_type_definition
|
69
|
+
links = fields.select { |f| %i[Asset Link].include?(typedef.fields[f].type) }
|
70
|
+
links.all? { |f| _resolved_field?(f, depth) }
|
71
|
+
end
|
72
|
+
|
73
|
+
# Turns the current model into a hash representation as though it had been retrieved from
|
74
|
+
# the Contentful API.
|
75
|
+
#
|
76
|
+
# This differs from `#raw` in that it recursively includes the `#to_h`
|
77
|
+
# of resolved links. It also sets the fields to the value for the entry's `#sys.locale`,
|
78
|
+
# as though the entry had been retrieved from the API with `locale={#sys.locale}` rather
|
79
|
+
# than `locale=*`.
|
80
|
+
def to_h(stack = nil)
|
81
|
+
raise WCC::Contentful::CircularReferenceError.new(stack, id) if stack&.include?(id)
|
82
|
+
|
83
|
+
stack = [*stack, id]
|
84
|
+
typedef = self.class.content_type_definition
|
85
|
+
fields =
|
86
|
+
typedef.fields.each_with_object({}) do |(name, field_def), h|
|
87
|
+
if field_def.type == :Link || field_def.type == :Asset
|
88
|
+
if _resolved_field?(name, 0)
|
89
|
+
val = public_send(name)
|
90
|
+
val =
|
91
|
+
_try_map(val) { |v| v.to_h(stack) }
|
92
|
+
else
|
93
|
+
ids = field_def.array ? public_send("#{name}_ids") : public_send("#{name}_id")
|
94
|
+
val =
|
95
|
+
_try_map(ids) do |id|
|
96
|
+
{
|
97
|
+
'sys' => {
|
98
|
+
'type' => 'Link',
|
99
|
+
'linkType' => field_def.type == :Asset ? 'Asset' : 'Entry',
|
100
|
+
'id' => id
|
101
|
+
}
|
102
|
+
}
|
103
|
+
end
|
104
|
+
end
|
105
|
+
else
|
106
|
+
val = public_send(name)
|
107
|
+
val = _try_map(val) { |v| v.respond_to?(:to_h) ? v.to_h.stringify_keys! : v }
|
108
|
+
end
|
109
|
+
|
110
|
+
h[name] = val
|
111
|
+
end
|
112
|
+
|
113
|
+
{
|
114
|
+
'sys' => { 'locale' => @sys.locale }.merge!(@raw['sys']),
|
115
|
+
'fields' => fields
|
116
|
+
}
|
117
|
+
end
|
118
|
+
|
119
|
+
delegate :to_json, to: :to_h
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def _resolve_field(field_name, depth = 1, context = {}, options = {})
|
124
|
+
return if depth <= 0
|
125
|
+
|
126
|
+
var_name = '@' + field_name
|
127
|
+
return unless val = instance_variable_get(var_name)
|
128
|
+
|
129
|
+
context = sys.context.to_h.merge(context)
|
130
|
+
# load a single link from a raw link or entry, by either finding it via the API
|
131
|
+
# or instantiating it directly from a raw entry
|
132
|
+
load =
|
133
|
+
->(raw) {
|
134
|
+
id = raw.dig('sys', 'id')
|
135
|
+
already_resolved = context[:backlinks]&.find { |m| m.id == id }
|
136
|
+
|
137
|
+
new_context = context.merge({ backlinks: [self, *context[:backlinks]].freeze })
|
138
|
+
|
139
|
+
if already_resolved && %i[ignore raise].include?(options[:circular_reference])
|
140
|
+
raise WCC::Contentful::CircularReferenceError.new(
|
141
|
+
new_context[:backlinks].map(&:id).reverse,
|
142
|
+
id
|
143
|
+
)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Use the already resolved circular reference, or resolve a link, or
|
147
|
+
# instantiate from already resolved raw entry data.
|
148
|
+
m = already_resolved ||
|
149
|
+
if raw.dig('sys', 'type') == 'Link'
|
150
|
+
WCC::Contentful::Model.find(id, new_context)
|
151
|
+
else
|
152
|
+
WCC::Contentful::Model.new_from_raw(raw, new_context)
|
153
|
+
end
|
154
|
+
|
155
|
+
m.resolve(depth: depth - 1, context: new_context, **options) if m && depth > 1
|
156
|
+
m
|
157
|
+
}
|
158
|
+
|
159
|
+
begin
|
160
|
+
val = _try_map(val) { |v| load.call(v) }
|
161
|
+
|
162
|
+
instance_variable_set(var_name + '_resolved', val)
|
163
|
+
rescue WCC::Contentful::CircularReferenceError
|
164
|
+
raise unless options[:circular_reference] == :ignore
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def _resolved_field?(field_name, depth = 1)
|
169
|
+
var_name = '@' + field_name
|
170
|
+
raw = instance_variable_get(var_name)
|
171
|
+
return true if raw.nil? || (raw.is_a?(Array) && raw.all?(&:nil?))
|
172
|
+
return false unless val = instance_variable_get(var_name + '_resolved')
|
173
|
+
return true if depth <= 1
|
174
|
+
|
175
|
+
return val.resolved?(depth: depth - 1) unless val.is_a? Array
|
176
|
+
|
177
|
+
val.all? { |i| i.nil? || i.resolved?(depth: depth - 1) }
|
178
|
+
end
|
179
|
+
|
180
|
+
def _try_map(val)
|
181
|
+
if val.is_a? Array
|
182
|
+
return val&.map do |item|
|
183
|
+
yield item unless item.nil?
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
yield val unless val.nil?
|
188
|
+
end
|
189
|
+
end
|