wcc-contentful 0.2.2 → 0.3.0.pre.rc
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 +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
|