wcc-contentful 0.4.0.pre.alpha → 1.0.0.pre.rc3
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 +5 -5
- data/Guardfile +43 -0
- data/README.md +246 -11
- data/Rakefile +5 -0
- data/app/controllers/wcc/contentful/webhook_controller.rb +25 -24
- data/app/jobs/wcc/contentful/webhook_enable_job.rb +36 -2
- data/config/routes.rb +1 -1
- data/doc +1 -0
- data/lib/tasks/download_schema.rake +12 -0
- data/lib/wcc/contentful.rb +70 -16
- data/lib/wcc/contentful/active_record_shim.rb +72 -0
- data/lib/wcc/contentful/configuration.rb +177 -46
- data/lib/wcc/contentful/content_type_indexer.rb +14 -0
- data/lib/wcc/contentful/downloads_schema.rb +112 -0
- data/lib/wcc/contentful/engine.rb +33 -14
- data/lib/wcc/contentful/event.rb +171 -0
- data/lib/wcc/contentful/events.rb +41 -0
- data/lib/wcc/contentful/exceptions.rb +3 -0
- data/lib/wcc/contentful/indexed_representation.rb +2 -2
- data/lib/wcc/contentful/instrumentation.rb +31 -0
- data/lib/wcc/contentful/link.rb +28 -0
- data/lib/wcc/contentful/link_visitor.rb +122 -0
- data/lib/wcc/contentful/middleware.rb +7 -0
- data/lib/wcc/contentful/middleware/store.rb +158 -0
- data/lib/wcc/contentful/middleware/store/caching_middleware.rb +114 -0
- data/lib/wcc/contentful/model.rb +37 -3
- data/lib/wcc/contentful/model_builder.rb +1 -0
- data/lib/wcc/contentful/model_methods.rb +40 -15
- data/lib/wcc/contentful/model_singleton_methods.rb +47 -30
- data/lib/wcc/contentful/rake.rb +4 -0
- data/lib/wcc/contentful/rspec.rb +46 -0
- data/lib/wcc/contentful/services.rb +61 -27
- data/lib/wcc/contentful/simple_client.rb +81 -25
- data/lib/wcc/contentful/simple_client/management.rb +43 -10
- data/lib/wcc/contentful/simple_client/response.rb +61 -22
- data/lib/wcc/contentful/simple_client/typhoeus_adapter.rb +17 -17
- data/lib/wcc/contentful/store.rb +7 -66
- data/lib/wcc/contentful/store/README.md +85 -0
- data/lib/wcc/contentful/store/base.rb +34 -119
- data/lib/wcc/contentful/store/cdn_adapter.rb +71 -12
- data/lib/wcc/contentful/store/factory.rb +186 -0
- data/lib/wcc/contentful/store/instrumentation.rb +55 -0
- data/lib/wcc/contentful/store/interface.rb +82 -0
- data/lib/wcc/contentful/store/memory_store.rb +27 -24
- data/lib/wcc/contentful/store/postgres_store.rb +268 -101
- data/lib/wcc/contentful/store/postgres_store/schema_1.sql +73 -0
- data/lib/wcc/contentful/store/postgres_store/schema_2.sql +21 -0
- data/lib/wcc/contentful/store/query.rb +246 -0
- data/lib/wcc/contentful/store/query/interface.rb +63 -0
- data/lib/wcc/contentful/store/rspec_examples.rb +48 -0
- data/lib/wcc/contentful/store/rspec_examples/basic_store.rb +629 -0
- data/lib/wcc/contentful/store/rspec_examples/include_param.rb +283 -0
- data/lib/wcc/contentful/store/rspec_examples/nested_queries.rb +342 -0
- data/lib/wcc/contentful/sync_engine.rb +181 -0
- data/lib/wcc/contentful/test.rb +7 -0
- data/lib/wcc/contentful/test/attributes.rb +56 -0
- data/lib/wcc/contentful/test/double.rb +76 -0
- data/lib/wcc/contentful/test/factory.rb +101 -0
- data/lib/wcc/contentful/version.rb +1 -1
- data/wcc-contentful.gemspec +23 -11
- metadata +299 -116
- data/Gemfile +0 -6
- data/app/jobs/wcc/contentful/delayed_sync_job.rb +0 -63
- data/lib/wcc/contentful/client_ext.rb +0 -28
- data/lib/wcc/contentful/graphql.rb +0 -14
- data/lib/wcc/contentful/graphql/builder.rb +0 -177
- data/lib/wcc/contentful/graphql/types.rb +0 -54
- data/lib/wcc/contentful/simple_client/http_adapter.rb +0 -24
- data/lib/wcc/contentful/store/lazy_cache_store.rb +0 -161
@@ -5,6 +5,15 @@
|
|
5
5
|
#
|
6
6
|
# @api Model
|
7
7
|
module WCC::Contentful::ModelMethods
|
8
|
+
include WCC::Contentful::Instrumentation
|
9
|
+
|
10
|
+
# The set of options keys that are specific to the Model layer and shouldn't
|
11
|
+
# be passed down to the Store layer.
|
12
|
+
MODEL_LAYER_CONTEXT_KEYS = %i[
|
13
|
+
preview
|
14
|
+
backlinks
|
15
|
+
].freeze
|
16
|
+
|
8
17
|
# Resolves all links in an entry to the specified depth.
|
9
18
|
#
|
10
19
|
# Each link in the entry is recursively retrieved from the store until the given
|
@@ -22,7 +31,7 @@ module WCC::Contentful::ModelMethods
|
|
22
31
|
# handled. `:raise` causes a {WCC::Contentful::CircularReferenceError} to be raised,
|
23
32
|
# `:ignore` will cause the field to remain unresolved, and any other value (or nil)
|
24
33
|
# will cause the field to point to the previously resolved ruby object for that ID.
|
25
|
-
def resolve(depth: 1, fields: nil, context:
|
34
|
+
def resolve(depth: 1, fields: nil, context: sys.context.to_h, **options)
|
26
35
|
raise ArgumentError, "Depth must be > 0 (was #{depth})" unless depth && depth > 0
|
27
36
|
return self if resolved?(depth: depth, fields: fields)
|
28
37
|
|
@@ -32,20 +41,26 @@ module WCC::Contentful::ModelMethods
|
|
32
41
|
typedef = self.class.content_type_definition
|
33
42
|
links = fields.select { |f| %i[Asset Link].include?(typedef.fields[f].type) }
|
34
43
|
|
35
|
-
|
36
|
-
links.
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
44
|
+
raw_link_ids =
|
45
|
+
links.map { |field_name| raw.dig('fields', field_name, sys.locale) }
|
46
|
+
.flat_map do |raw_value|
|
47
|
+
_try_map(raw_value) { |v| v.dig('sys', 'id') if v.dig('sys', 'type') == 'Link' }
|
48
|
+
end
|
49
|
+
raw_link_ids = raw_link_ids.compact
|
50
|
+
backlinked_ids = (context[:backlinks]&.map { |m| m.id } || [])
|
51
|
+
|
52
|
+
has_unresolved_raw_links = (raw_link_ids - backlinked_ids).any?
|
53
|
+
if has_unresolved_raw_links
|
54
|
+
raw =
|
55
|
+
_instrument 'resolve', id: id, depth: depth, backlinks: backlinked_ids do
|
56
|
+
# use include param to do resolution
|
57
|
+
self.class.store(context[:preview])
|
58
|
+
.find_by(content_type: self.class.content_type,
|
59
|
+
filter: { 'sys.id' => id },
|
60
|
+
options: context.except(*MODEL_LAYER_CONTEXT_KEYS).merge!({
|
61
|
+
include: [depth, 10].min
|
62
|
+
}))
|
42
63
|
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
64
|
unless raw
|
50
65
|
raise WCC::Contentful::ResolveError, "Cannot find #{self.class.content_type} with ID #{id}"
|
51
66
|
end
|
@@ -118,6 +133,12 @@ module WCC::Contentful::ModelMethods
|
|
118
133
|
|
119
134
|
delegate :to_json, to: :to_h
|
120
135
|
|
136
|
+
protected
|
137
|
+
|
138
|
+
def _instrumentation_event_prefix
|
139
|
+
'.model.contentful.wcc'
|
140
|
+
end
|
141
|
+
|
121
142
|
private
|
122
143
|
|
123
144
|
def _resolve_field(field_name, depth = 1, context = {}, options = {})
|
@@ -147,7 +168,10 @@ module WCC::Contentful::ModelMethods
|
|
147
168
|
# instantiate from already resolved raw entry data.
|
148
169
|
m = already_resolved ||
|
149
170
|
if raw.dig('sys', 'type') == 'Link'
|
150
|
-
|
171
|
+
_instrument 'resolve',
|
172
|
+
id: self.id, depth: depth, backlinks: context[:backlinks]&.map(&:id) do
|
173
|
+
WCC::Contentful::Model.find(id, options: new_context)
|
174
|
+
end
|
151
175
|
else
|
152
176
|
WCC::Contentful::Model.new_from_raw(raw, new_context)
|
153
177
|
end
|
@@ -158,6 +182,7 @@ module WCC::Contentful::ModelMethods
|
|
158
182
|
|
159
183
|
begin
|
160
184
|
val = _try_map(val) { |v| load.call(v) }
|
185
|
+
val = val.compact if val.is_a? Array
|
161
186
|
|
162
187
|
instance_variable_set(var_name + '_resolved', val)
|
163
188
|
rescue WCC::Contentful::CircularReferenceError
|
@@ -4,18 +4,6 @@
|
|
4
4
|
# methods that are not dynamically generated.
|
5
5
|
# @api Model
|
6
6
|
module WCC::Contentful::ModelSingletonMethods
|
7
|
-
def store(preview = false)
|
8
|
-
if preview
|
9
|
-
if WCC::Contentful::Model.preview_store.nil?
|
10
|
-
raise ArgumentError,
|
11
|
-
'You must include a contentful preview token in your WCC::Contentful.configure block'
|
12
|
-
end
|
13
|
-
WCC::Contentful::Model.preview_store
|
14
|
-
else
|
15
|
-
WCC::Contentful::Model.store
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
7
|
# Finds an instance of this content type.
|
20
8
|
#
|
21
9
|
# @return [nil, WCC::Contentful::Model] An instance of the appropriate model class
|
@@ -24,8 +12,12 @@ module WCC::Contentful::ModelSingletonMethods
|
|
24
12
|
# WCC::Contentful::Model::Page.find(id)
|
25
13
|
def find(id, options: nil)
|
26
14
|
options ||= {}
|
27
|
-
|
28
|
-
|
15
|
+
store = store(options[:preview])
|
16
|
+
raw =
|
17
|
+
WCC::Contentful::Instrumentation.instrument 'find.model.contentful.wcc',
|
18
|
+
content_type: content_type, id: id, options: options do
|
19
|
+
store.find(id, { hint: type }.merge!(options.except(:preview)))
|
20
|
+
end
|
29
21
|
new(raw, options) if raw.present?
|
30
22
|
end
|
31
23
|
|
@@ -40,16 +32,16 @@ module WCC::Contentful::ModelSingletonMethods
|
|
40
32
|
filter = filter&.dup
|
41
33
|
options = filter&.delete(:options) || {}
|
42
34
|
|
43
|
-
if filter.present?
|
44
|
-
filter.transform_keys! { |k| k.to_s.camelize(:lower) }
|
45
|
-
bad_fields = filter.keys.reject { |k| self::FIELDS.include?(k) }
|
46
|
-
raise ArgumentError, "These fields do not exist: #{bad_fields}" unless bad_fields.empty?
|
47
|
-
end
|
35
|
+
filter.transform_keys! { |k| k.to_s.camelize(:lower) } if filter.present?
|
48
36
|
|
49
|
-
|
50
|
-
|
37
|
+
store = store(options[:preview])
|
38
|
+
query =
|
39
|
+
WCC::Contentful::Instrumentation.instrument 'find_all.model.contentful.wcc',
|
40
|
+
content_type: content_type, filter: filter, options: options do
|
41
|
+
store.find_all(content_type: content_type, options: options.except(:preview))
|
42
|
+
end
|
51
43
|
query = query.apply(filter) if filter.present?
|
52
|
-
|
44
|
+
ModelQuery.new(query, options, self)
|
53
45
|
end
|
54
46
|
|
55
47
|
# Finds the first instance of this content type matching the given query.
|
@@ -62,22 +54,47 @@ module WCC::Contentful::ModelSingletonMethods
|
|
62
54
|
filter = filter&.dup
|
63
55
|
options = filter&.delete(:options) || {}
|
64
56
|
|
65
|
-
if filter.present?
|
66
|
-
filter.transform_keys! { |k| k.to_s.camelize(:lower) }
|
67
|
-
bad_fields = filter.keys.reject { |k| self::FIELDS.include?(k) }
|
68
|
-
raise ArgumentError, "These fields do not exist: #{bad_fields}" unless bad_fields.empty?
|
69
|
-
end
|
57
|
+
filter.transform_keys! { |k| k.to_s.camelize(:lower) } if filter.present?
|
70
58
|
|
71
|
-
|
72
|
-
|
59
|
+
store = store(options[:preview])
|
60
|
+
result =
|
61
|
+
WCC::Contentful::Instrumentation.instrument 'find_by.model.contentful.wcc',
|
62
|
+
content_type: content_type, filter: filter, options: options do
|
63
|
+
store.find_by(content_type: content_type, filter: filter, options: options.except(:preview))
|
64
|
+
end
|
73
65
|
|
74
66
|
new(result, options) if result
|
75
67
|
end
|
76
68
|
|
77
69
|
def inherited(subclass)
|
78
|
-
#
|
70
|
+
# If another different class is already registered for this content type,
|
71
|
+
# don't auto-register this one.
|
79
72
|
return if WCC::Contentful::Model.registered?(content_type)
|
80
73
|
|
81
74
|
WCC::Contentful::Model.register_for_content_type(content_type, klass: subclass)
|
82
75
|
end
|
76
|
+
|
77
|
+
class ModelQuery
|
78
|
+
include Enumerable
|
79
|
+
|
80
|
+
# by default all enumerable methods delegated to the to_enum method
|
81
|
+
delegate(*(Enumerable.instance_methods - Module.instance_methods), to: :to_enum)
|
82
|
+
delegate :each, to: :to_enum
|
83
|
+
|
84
|
+
# except count - because that needs to pull data off the final query obj
|
85
|
+
delegate :count, to: :wrapped_query
|
86
|
+
|
87
|
+
attr_reader :wrapped_query, :options, :klass
|
88
|
+
|
89
|
+
def initialize(wrapped_query, options, klass)
|
90
|
+
@wrapped_query = wrapped_query
|
91
|
+
@options = options
|
92
|
+
@klass = klass
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_enum
|
96
|
+
wrapped_query.to_enum
|
97
|
+
.map { |r| klass.new(r, options) }
|
98
|
+
end
|
99
|
+
end
|
83
100
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'wcc/contentful'
|
4
|
+
|
5
|
+
require_relative './test'
|
6
|
+
|
7
|
+
module WCC::Contentful::RSpec
|
8
|
+
include WCC::Contentful::Test::Double
|
9
|
+
include WCC::Contentful::Test::Factory
|
10
|
+
|
11
|
+
##
|
12
|
+
# Builds out a fake Contentful entry for the given content type, and then
|
13
|
+
# stubs the Model API to return that content type for `.find` and `.find_by`
|
14
|
+
# query methods.
|
15
|
+
def contentful_stub(content_type, **attrs)
|
16
|
+
const = WCC::Contentful::Model.resolve_constant(content_type.to_s)
|
17
|
+
instance = contentful_create(content_type, **attrs)
|
18
|
+
|
19
|
+
# mimic what's going on inside model_singleton_methods.rb
|
20
|
+
# find, find_by, etc always return a new instance from the same raw
|
21
|
+
allow(WCC::Contentful::Model).to receive(:find)
|
22
|
+
.with(instance.id, any_args) do |_id, keyword_params|
|
23
|
+
options = keyword_params && keyword_params[:options]
|
24
|
+
contentful_create(content_type, options, raw: instance.raw, **attrs)
|
25
|
+
end
|
26
|
+
allow(const).to receive(:find) { |id, options| WCC::Contentful::Model.find(id, **(options || {})) }
|
27
|
+
|
28
|
+
attrs.each do |k, v|
|
29
|
+
allow(const).to receive(:find_by)
|
30
|
+
.with(hash_including(k => v)) do |filter|
|
31
|
+
filter = filter&.dup
|
32
|
+
options = filter&.delete(:options) || {}
|
33
|
+
|
34
|
+
contentful_create(content_type, options, raw: instance.raw, **attrs)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
instance
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
if defined?(RSpec)
|
43
|
+
RSpec.configure do |config|
|
44
|
+
config.include WCC::Contentful::RSpec
|
45
|
+
end
|
46
|
+
end
|
@@ -1,21 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'singleton'
|
4
|
-
|
5
3
|
module WCC::Contentful
|
6
4
|
class Services
|
7
|
-
|
5
|
+
class << self
|
6
|
+
def instance
|
7
|
+
@singleton__instance__ ||= new # rubocop:disable Naming/MemoizedInstanceVariableName
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def configuration
|
12
|
+
@configuration ||= WCC::Contentful.configuration
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(configuration = nil)
|
16
|
+
@configuration = configuration
|
17
|
+
end
|
8
18
|
|
9
19
|
# Gets the data-store which executes the queries run against the dynamic
|
10
20
|
# models in the WCC::Contentful::Model namespace.
|
11
|
-
# This is one of the following based on the configured
|
21
|
+
# This is one of the following based on the configured store method:
|
12
22
|
#
|
13
23
|
# [:direct] an instance of {WCC::Contentful::Store::CDNAdapter} with a
|
14
24
|
# {WCC::Contentful::SimpleClient::Cdn CDN Client} to access the CDN.
|
15
25
|
#
|
16
|
-
# [:lazy_sync] an instance of {WCC::Contentful::Store::
|
17
|
-
# with the configured ActiveSupport::Cache implementation
|
18
|
-
# {WCC::Contentful::
|
26
|
+
# [:lazy_sync] an instance of {WCC::Contentful::Middleware::Store::CachingMiddleware}
|
27
|
+
# with the configured ActiveSupport::Cache implementation around a
|
28
|
+
# {WCC::Contentful::Store::CDNAdapter} for when data
|
19
29
|
# cannot be found in the cache.
|
20
30
|
#
|
21
31
|
# [:eager_sync] an instance of the configured Store type, defined by
|
@@ -25,12 +35,7 @@ module WCC::Contentful
|
|
25
35
|
def store
|
26
36
|
@store ||=
|
27
37
|
ensure_configured do |config|
|
28
|
-
|
29
|
-
config,
|
30
|
-
self,
|
31
|
-
config.content_delivery,
|
32
|
-
config.content_delivery_params
|
33
|
-
).build_sync_store
|
38
|
+
config.store.build(self)
|
34
39
|
end
|
35
40
|
end
|
36
41
|
|
@@ -43,10 +48,9 @@ module WCC::Contentful
|
|
43
48
|
ensure_configured do |config|
|
44
49
|
WCC::Contentful::Store::Factory.new(
|
45
50
|
config,
|
46
|
-
self,
|
47
51
|
:direct,
|
48
|
-
|
49
|
-
).
|
52
|
+
:preview
|
53
|
+
).build(self)
|
50
54
|
end
|
51
55
|
end
|
52
56
|
|
@@ -58,10 +62,11 @@ module WCC::Contentful
|
|
58
62
|
@client ||=
|
59
63
|
ensure_configured do |config|
|
60
64
|
WCC::Contentful::SimpleClient::Cdn.new(
|
65
|
+
**config.connection_options,
|
61
66
|
access_token: config.access_token,
|
62
67
|
space: config.space,
|
63
68
|
default_locale: config.default_locale,
|
64
|
-
|
69
|
+
connection: config.connection,
|
65
70
|
environment: config.environment
|
66
71
|
)
|
67
72
|
end
|
@@ -76,10 +81,11 @@ module WCC::Contentful
|
|
76
81
|
ensure_configured do |config|
|
77
82
|
if config.preview_token.present?
|
78
83
|
WCC::Contentful::SimpleClient::Preview.new(
|
84
|
+
**config.connection_options,
|
79
85
|
preview_token: config.preview_token,
|
80
86
|
space: config.space,
|
81
87
|
default_locale: config.default_locale,
|
82
|
-
|
88
|
+
connection: config.connection,
|
83
89
|
environment: config.environment
|
84
90
|
)
|
85
91
|
end
|
@@ -95,27 +101,57 @@ module WCC::Contentful
|
|
95
101
|
ensure_configured do |config|
|
96
102
|
if config.management_token.present?
|
97
103
|
WCC::Contentful::SimpleClient::Management.new(
|
104
|
+
**config.connection_options,
|
98
105
|
management_token: config.management_token,
|
99
106
|
space: config.space,
|
100
107
|
default_locale: config.default_locale,
|
101
|
-
|
108
|
+
connection: config.connection,
|
102
109
|
environment: config.environment
|
103
110
|
)
|
104
111
|
end
|
105
112
|
end
|
106
113
|
end
|
107
114
|
|
115
|
+
# Gets the configured WCC::Contentful::SyncEngine which is responsible for
|
116
|
+
# updating the currently configured store. The application must periodically
|
117
|
+
# call #next on this instance. Alternately, the application can mount the
|
118
|
+
# WCC::Contentful::Engine, which will call #next anytime a webhook is received.
|
119
|
+
#
|
120
|
+
# This returns `nil` if the currently configured store does not respond to sync
|
121
|
+
# events.
|
122
|
+
def sync_engine
|
123
|
+
@sync_engine ||=
|
124
|
+
if store.index?
|
125
|
+
SyncEngine.new(
|
126
|
+
store: store,
|
127
|
+
client: client,
|
128
|
+
key: 'sync:token'
|
129
|
+
)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Gets the configured instrumentation adapter, defaulting to ActiveSupport::Notifications
|
134
|
+
def instrumentation
|
135
|
+
return @instrumentation if @instrumentation
|
136
|
+
return ActiveSupport::Notifications if WCC::Contentful.configuration.nil?
|
137
|
+
|
138
|
+
@instrumentation ||=
|
139
|
+
WCC::Contentful.configuration.instrumentation_adapter ||
|
140
|
+
ActiveSupport::Notifications
|
141
|
+
end
|
142
|
+
|
108
143
|
private
|
109
144
|
|
110
145
|
def ensure_configured
|
111
|
-
|
112
|
-
raise StandardError, 'WCC::Contentful has not yet been configured!'
|
113
|
-
end
|
146
|
+
raise StandardError, 'WCC::Contentful has not yet been configured!' if configuration.nil?
|
114
147
|
|
115
|
-
yield
|
148
|
+
yield configuration
|
116
149
|
end
|
117
150
|
end
|
118
151
|
|
152
|
+
SERVICES = (WCC::Contentful::Services.instance_methods -
|
153
|
+
Object.instance_methods)
|
154
|
+
|
119
155
|
# Include this module to define accessors for every method defined on the
|
120
156
|
# {Services} singleton.
|
121
157
|
#
|
@@ -129,14 +165,12 @@ module WCC::Contentful
|
|
129
165
|
# store.find(...)
|
130
166
|
#
|
131
167
|
# client.entries(...)
|
168
|
+
#
|
169
|
+
# sync_engine.next
|
132
170
|
# end
|
133
171
|
# end
|
134
172
|
# @see Services
|
135
173
|
module ServiceAccessors
|
136
|
-
SERVICES = (WCC::Contentful::Services.instance_methods -
|
137
|
-
Object.instance_methods -
|
138
|
-
Singleton.instance_methods)
|
139
|
-
|
140
174
|
SERVICES.each do |m|
|
141
175
|
define_method m do
|
142
176
|
Services.instance.public_send(m)
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require_relative 'simple_client/response'
|
4
4
|
require_relative 'simple_client/management'
|
5
|
+
require_relative 'instrumentation'
|
5
6
|
|
6
7
|
module WCC::Contentful
|
7
8
|
# The SimpleClient accesses the Contentful CDN to get JSON responses,
|
@@ -16,11 +17,13 @@ module WCC::Contentful
|
|
16
17
|
# `get`. This method returns a WCC::Contentful::SimpleClient::Response
|
17
18
|
# that handles paging automatically.
|
18
19
|
#
|
19
|
-
# The SimpleClient by default uses '
|
20
|
-
# client
|
20
|
+
# The SimpleClient by default uses 'faraday' to perform the gets, but any HTTP
|
21
|
+
# client adapter be injected by passing the `connection:` option.
|
21
22
|
#
|
22
23
|
# @api Client
|
23
24
|
class SimpleClient
|
25
|
+
include WCC::Contentful::Instrumentation
|
26
|
+
|
24
27
|
attr_reader :api_url
|
25
28
|
attr_reader :space
|
26
29
|
|
@@ -30,21 +33,27 @@ module WCC::Contentful
|
|
30
33
|
# @param [String] space The Space ID to access
|
31
34
|
# @param [String] access_token A Contentful Access Token to be sent in the Authorization header
|
32
35
|
# @param [Hash] options The remaining optional parameters, defined below
|
33
|
-
# @option options [Symbol, Object]
|
36
|
+
# @option options [Symbol, Object] connection The Faraday connection to use to make requests.
|
34
37
|
# Auto-discovered based on what gems are installed if this is not provided.
|
35
38
|
# @option options [String] default_locale The locale query param to set by default.
|
36
39
|
# @option options [String] environment The contentful environment to access. Defaults to 'master'.
|
37
40
|
# @option options [Boolean] no_follow_redirects If true, do not follow 300 level redirects.
|
41
|
+
# @option options [Number] rate_limit_wait_timeout The maximum time to block the thread waiting
|
42
|
+
# on a rate limit response. By default will wait for one 429 and then fail on the second 429.
|
38
43
|
def initialize(api_url:, space:, access_token:, **options)
|
39
44
|
@api_url = URI.join(api_url, '/spaces/', space + '/')
|
40
45
|
@space = space
|
41
46
|
@access_token = access_token
|
42
47
|
|
43
|
-
@adapter = SimpleClient.load_adapter(options[:
|
48
|
+
@adapter = SimpleClient.load_adapter(options[:connection])
|
44
49
|
|
45
50
|
@options = options
|
51
|
+
@_instrumentation = @options[:instrumentation]
|
46
52
|
@query_defaults = {}
|
47
53
|
@query_defaults[:locale] = @options[:default_locale] if @options[:default_locale]
|
54
|
+
# default 1.5 so that we retry one time then fail if still rate limited
|
55
|
+
# https://www.contentful.com/developers/docs/references/content-preview-api/#/introduction/api-rate-limits
|
56
|
+
@rate_limit_wait_timeout = @options[:rate_limit_wait_timeout] || 1.5
|
48
57
|
|
49
58
|
return unless options[:environment].present?
|
50
59
|
|
@@ -57,13 +66,17 @@ module WCC::Contentful
|
|
57
66
|
def get(path, query = {})
|
58
67
|
url = URI.join(@api_url, path)
|
59
68
|
|
69
|
+
resp =
|
70
|
+
_instrument 'get_http', url: url, query: query do
|
71
|
+
get_http(url, query)
|
72
|
+
end
|
60
73
|
Response.new(self,
|
61
74
|
{ url: url, query: query },
|
62
|
-
|
75
|
+
resp)
|
63
76
|
end
|
64
77
|
|
65
78
|
ADAPTERS = {
|
66
|
-
|
79
|
+
faraday: ['faraday', '>= 0.9'],
|
67
80
|
typhoeus: ['typhoeus', '~> 1.0']
|
68
81
|
}.freeze
|
69
82
|
|
@@ -80,16 +93,19 @@ module WCC::Contentful
|
|
80
93
|
end
|
81
94
|
raise ArgumentError, 'Unable to load adapter! Please install one of '\
|
82
95
|
"#{ADAPTERS.values.map(&:join).join(',')}"
|
83
|
-
when :
|
84
|
-
|
85
|
-
|
96
|
+
when :faraday
|
97
|
+
require 'faraday'
|
98
|
+
::Faraday.new do |faraday|
|
99
|
+
faraday.response :logger, (Rails.logger if defined?(Rails)), { headers: false, bodies: false }
|
100
|
+
faraday.adapter :net_http
|
101
|
+
end
|
86
102
|
when :typhoeus
|
87
103
|
require_relative 'simple_client/typhoeus_adapter'
|
88
104
|
TyphoeusAdapter.new
|
89
105
|
else
|
90
|
-
unless adapter.respond_to?(:
|
106
|
+
unless adapter.respond_to?(:get)
|
91
107
|
raise ArgumentError, "Adapter #{adapter} is not invokeable! Please "\
|
92
|
-
"pass
|
108
|
+
"pass use one of #{ADAPTERS.keys} or create a Faraday-compatible adapter"
|
93
109
|
end
|
94
110
|
adapter
|
95
111
|
end
|
@@ -97,7 +113,12 @@ module WCC::Contentful
|
|
97
113
|
|
98
114
|
private
|
99
115
|
|
100
|
-
def
|
116
|
+
def _instrumentation_event_prefix
|
117
|
+
# Unify all CDN, Management, Preview notifications under same namespace
|
118
|
+
'.simpleclient.contentful.wcc'
|
119
|
+
end
|
120
|
+
|
121
|
+
def get_http(url, query, headers = {})
|
101
122
|
headers = {
|
102
123
|
Authorization: "Bearer #{@access_token}"
|
103
124
|
}.merge(headers || {})
|
@@ -105,12 +126,28 @@ module WCC::Contentful
|
|
105
126
|
q = @query_defaults.dup
|
106
127
|
q = q.merge(query) if query
|
107
128
|
|
108
|
-
|
129
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
130
|
+
loop do
|
131
|
+
resp = @adapter.get(url, q, headers)
|
132
|
+
|
133
|
+
if [301, 302, 307].include?(resp.status) && !@options[:no_follow_redirects]
|
134
|
+
url = resp.headers['Location']
|
135
|
+
next
|
136
|
+
end
|
137
|
+
|
138
|
+
if resp.status == 429 &&
|
139
|
+
reset = resp.headers['X-Contentful-RateLimit-Reset'].presence
|
140
|
+
reset = reset.to_f
|
141
|
+
_instrument 'rate_limit', start: start, reset: reset, timeout: @rate_limit_wait_timeout
|
142
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
143
|
+
if (now - start) + reset < @rate_limit_wait_timeout
|
144
|
+
sleep(reset)
|
145
|
+
next
|
146
|
+
end
|
147
|
+
end
|
109
148
|
|
110
|
-
|
111
|
-
resp = get_http(resp.headers['location'], nil, headers, proxy)
|
149
|
+
return resp
|
112
150
|
end
|
113
|
-
resp
|
114
151
|
end
|
115
152
|
|
116
153
|
# The CDN SimpleClient accesses 'https://cdn.contentful.com' to get raw
|
@@ -135,31 +172,46 @@ module WCC::Contentful
|
|
135
172
|
|
136
173
|
# Gets an entry by ID
|
137
174
|
def entry(key, query = {})
|
138
|
-
resp =
|
175
|
+
resp =
|
176
|
+
_instrument 'entries', id: key, type: 'Entry', query: query do
|
177
|
+
get("entries/#{key}", query)
|
178
|
+
end
|
139
179
|
resp.assert_ok!
|
140
180
|
end
|
141
181
|
|
142
182
|
# Queries entries with optional query parameters
|
143
183
|
def entries(query = {})
|
144
|
-
resp =
|
184
|
+
resp =
|
185
|
+
_instrument 'entries', type: 'Entry', query: query do
|
186
|
+
get('entries', query)
|
187
|
+
end
|
145
188
|
resp.assert_ok!
|
146
189
|
end
|
147
190
|
|
148
191
|
# Gets an asset by ID
|
149
192
|
def asset(key, query = {})
|
150
|
-
resp =
|
193
|
+
resp =
|
194
|
+
_instrument 'entries', type: 'Asset', id: key, query: query do
|
195
|
+
get("assets/#{key}", query)
|
196
|
+
end
|
151
197
|
resp.assert_ok!
|
152
198
|
end
|
153
199
|
|
154
200
|
# Queries assets with optional query parameters
|
155
201
|
def assets(query = {})
|
156
|
-
resp =
|
202
|
+
resp =
|
203
|
+
_instrument 'entries', type: 'Asset', query: query do
|
204
|
+
get('assets', query)
|
205
|
+
end
|
157
206
|
resp.assert_ok!
|
158
207
|
end
|
159
208
|
|
160
209
|
# Queries content types with optional query parameters
|
161
210
|
def content_types(query = {})
|
162
|
-
resp =
|
211
|
+
resp =
|
212
|
+
_instrument 'content_types', query: query do
|
213
|
+
get('content_types', query)
|
214
|
+
end
|
163
215
|
resp.assert_ok!
|
164
216
|
end
|
165
217
|
|
@@ -177,7 +229,11 @@ module WCC::Contentful
|
|
177
229
|
{ initial: true }
|
178
230
|
end
|
179
231
|
query = query.merge(sync_token)
|
180
|
-
resp =
|
232
|
+
resp =
|
233
|
+
_instrument 'sync', sync_token: sync_token, query: query do
|
234
|
+
get('sync', query)
|
235
|
+
end
|
236
|
+
resp = SyncResponse.new(resp)
|
181
237
|
resp.assert_ok!
|
182
238
|
end
|
183
239
|
end
|
@@ -186,10 +242,10 @@ module WCC::Contentful
|
|
186
242
|
class Preview < Cdn
|
187
243
|
def initialize(space:, preview_token:, **options)
|
188
244
|
super(
|
189
|
-
|
245
|
+
**options,
|
246
|
+
api_url: options[:preview_api_url] || 'https://preview.contentful.com/',
|
190
247
|
space: space,
|
191
|
-
access_token: preview_token
|
192
|
-
**options
|
248
|
+
access_token: preview_token
|
193
249
|
)
|
194
250
|
end
|
195
251
|
|