-
1
require 'contentful/version'
-
1
require 'contentful/support'
-
1
require 'contentful/client'
-
1
require_relative 'base_resource'
-
1
require_relative 'array_like'
-
-
1
module Contentful
-
# Resource Class for Arrays (e.g. search results)
-
# @see _ https://www.contentful.com/developers/documentation/content-delivery-api/#arrays
-
# @note It also provides an #each method and includes Ruby's Enumerable module (gives you methods like #min, #first, etc)
-
1
class Array < BaseResource
-
# @private
-
1
DEFAULT_LIMIT = 100
-
-
1
include Contentful::ArrayLike
-
-
1
attr_reader :total, :limit, :skip, :items, :endpoint
-
-
1
def initialize(item = nil,
-
configuration = {
-
default_locale: Contentful::Client::DEFAULT_CONFIGURATION[:default_locale]
-
},
-
endpoint = '', *)
-
70
super(item, configuration)
-
-
70
@endpoint = endpoint
-
70
@total = item.fetch('total', nil)
-
70
@limit = item.fetch('limit', nil)
-
70
@skip = item.fetch('skip', nil)
-
70
@items = item.fetch('items', [])
-
end
-
-
# @private
-
1
def marshal_dump
-
super.merge(endpoint: endpoint)
-
end
-
-
# @private
-
1
def marshal_load(raw_object)
-
super
-
@endpoint = raw_object[:endpoint]
-
@total = raw.fetch('total', nil)
-
@limit = raw.fetch('limit', nil)
-
@skip = raw.fetch('skip', nil)
-
@items = raw.fetch('items', []).map do |item|
-
require_relative 'resource_builder'
-
ResourceBuilder.new(
-
item.raw,
-
raw_object[:configuration].merge(includes_for_single: Support.includes_from_response(raw, false)),
-
item.respond_to?(:localized) ? item.localized : false
-
).run
-
end
-
end
-
-
# @private
-
1
def inspect
-
"<#{repr_name} total=#{total} skip=#{skip} limit=#{limit}>"
-
end
-
-
# Simplifies pagination
-
#
-
# @return [Contentful::Array, false]
-
1
def next_page(client = nil)
-
return false if client.nil?
-
return false if items.first.nil?
-
-
new_skip = (skip || 0) + (limit || DEFAULT_LIMIT)
-
-
plurals = {
-
'Space' => 'spaces',
-
'ContentType' => 'content_types',
-
'Entry' => 'entries',
-
'Asset' => 'assets',
-
'Locale' => 'locales'
-
}
-
client.public_send(plurals[items.first.type], limit: limit, skip: new_skip)
-
end
-
end
-
end
-
1
module Contentful
-
# Useful methods for array-like resources that can be included if an
-
# :items property exists
-
1
module ArrayLike
-
1
include Enumerable
-
-
# Returns true for array-like resources
-
#
-
# @return [true]
-
1
def array?
-
true
-
end
-
-
# Delegates to items#each
-
#
-
# @yield [Contentful::Entry, Contentful::Asset]
-
1
def each_item(&block)
-
72
items.each(&block)
-
end
-
1
alias each each_item
-
-
# Delegates to items#empty?
-
#
-
# @return [Boolean]
-
1
def empty?
-
3
items.empty?
-
end
-
-
# Delegetes to items#size
-
#
-
# @return [Number]
-
1
def size
-
items.size
-
end
-
1
alias length size
-
-
# Delegates to items#[]
-
#
-
# @return [Contentful::Entry, Contentful::Asset]
-
1
def [](index)
-
items[index]
-
end
-
-
# Delegates to items#last
-
#
-
# @return [Contentful::Entry, Contentful::Asset]
-
1
def last
-
items.last
-
end
-
end
-
end
-
1
require_relative 'fields_resource'
-
1
require_relative 'file'
-
1
require_relative 'resource_references'
-
-
1
module Contentful
-
# Resource class for Asset.
-
# https://www.contentful.com/developers/documentation/content-delivery-api/#assets
-
1
class Asset < FieldsResource
-
1
include Contentful::ResourceReferences
-
-
# @private
-
1
def marshal_dump
-
10
{
-
configuration: @configuration,
-
raw: raw
-
}
-
end
-
-
# @private
-
1
def marshal_load(raw_object)
-
10
super(raw_object)
-
10
create_files!
-
10
define_asset_methods!
-
end
-
-
# @private
-
1
def inspect
-
"<#{repr_name} id='#{sys[:id]}' url='#{url}'>"
-
end
-
-
1
def initialize(*)
-
485
super
-
485
create_files!
-
485
define_asset_methods!
-
end
-
-
# Generates a URL for the Contentful Image API
-
#
-
# @param [Hash] options
-
# @option options [Integer] :width
-
# @option options [Integer] :height
-
# @option options [String] :format
-
# @option options [String] :quality
-
# @option options [String] :focus
-
# @option options [String] :fit
-
# @option options [String] :fl File Layering - 'progressive'
-
# @option options [String] :background
-
# @see _ https://www.contentful.com/developers/documentation/content-delivery-api/#image-asset-resizing
-
#
-
# @return [String] Image API URL
-
1
def image_url(options = {})
-
4
query = build_query(options)
-
-
4
if query.empty?
-
4
file.url
-
else
-
"#{file.url}?#{URI.encode_www_form(query)}"
-
end
-
end
-
-
1
alias url image_url
-
-
1
private
-
-
1
def build_query(options)
-
{
-
w: options[:w] || options[:width],
-
h: options[:h] || options[:height],
-
fm: options[:fm] || options[:format],
-
q: options[:q] || options[:quality],
-
f: options[:f] || options[:focus],
-
bg: options[:bg] || options[:background],
-
r: options[:r] || options[:radius],
-
fit: options[:fit],
-
fl: options[:fl]
-
40
}.reject { |_k, v| v.nil? }
-
end
-
-
1
def create_files!
-
495
file_json = raw.fetch('fields', {}).fetch('file', nil)
-
495
return if file_json.nil?
-
-
991
is_localized = file_json.keys.none? { |f| %w[fileName contentType details url].include? f }
-
495
if is_localized
-
46
locales.each do |locale|
-
47
@fields[locale][:file] = ::Contentful::File.new(file_json[locale.to_s] || {}, @configuration)
-
end
-
else
-
449
@fields[internal_resource_locale][:file] = ::Contentful::File.new(file_json, @configuration)
-
end
-
end
-
-
1
def define_asset_methods!
-
495
define_singleton_method :description do
-
fields.fetch(:description, nil)
-
end
-
-
495
define_singleton_method :file do |wanted_locale = nil|
-
6
fields(wanted_locale)[:file]
-
end
-
end
-
end
-
end
-
1
require_relative 'support'
-
-
1
module Contentful
-
# Base definition of a Contentful Resource containing Sys properties
-
1
class BaseResource
-
1
attr_reader :raw, :default_locale, :sys
-
-
# rubocop:disable Metrics/ParameterLists
-
1
def initialize(item, configuration = {}, _localized = false, _includes = [], entries = {}, depth = 0, _errors = [])
-
3023
entries["#{item['sys']['type']}:#{item['sys']['id']}"] = self if entries && item.key?('sys')
-
3023
@raw = item
-
3023
@default_locale = configuration[:default_locale]
-
3023
@depth = depth
-
3023
@configuration = configuration
-
3023
@sys = hydrate_sys
-
-
3023
define_sys_methods!
-
end
-
-
# @private
-
1
def inspect
-
"<#{repr_name} id='#{sys[:id]}'>"
-
end
-
-
# Definition of equality
-
1
def ==(other)
-
self.class == other.class && sys[:id] == other.sys[:id]
-
end
-
-
# @private
-
1
def marshal_dump
-
{
-
configuration: @configuration,
-
raw: raw
-
}
-
end
-
-
# @private
-
1
def marshal_load(raw_object)
-
34
@raw = raw_object[:raw]
-
34
@configuration = raw_object[:configuration]
-
34
@default_locale = @configuration[:default_locale]
-
34
@sys = hydrate_sys
-
34
@depth = 0
-
34
define_sys_methods!
-
end
-
-
# Issues the request that was made to fetch this response again.
-
# Only works for Entry, Asset, ContentType and Space
-
1
def reload(client = nil)
-
return client.send(Support.snakify(self.class.name.split('::').last), id) unless client.nil?
-
-
false
-
end
-
-
1
private
-
-
1
def define_sys_methods!
-
3057
@sys.each do |k, v|
-
14082
define_singleton_method k do
-
6481
v
-
end
-
end
-
end
-
-
1
def hydrate_sys
-
3057
result = {}
-
3057
raw.fetch('sys', {}).each do |k, v|
-
14082
if %w[space contentType environment].include?(k)
-
1783
v = build_link(v)
-
12299
elsif %w[createdAt updatedAt deletedAt].include?(k)
-
2268
v = DateTime.parse(v)
-
end
-
14082
result[Support.snakify(k, @configuration[:use_camel_case]).to_sym] = v
-
end
-
3057
result
-
end
-
-
1
protected
-
-
1
def repr_name
-
self.class
-
end
-
-
1
def internal_resource_locale
-
2903
sys.fetch(:locale, nil) || default_locale
-
end
-
-
1
def build_link(item)
-
1848
require_relative 'link'
-
1848
::Contentful::Link.new(item, @configuration)
-
end
-
end
-
end
-
1
require_relative 'request'
-
1
require_relative 'response'
-
1
require_relative 'resource_builder'
-
1
require_relative 'sync'
-
1
require_relative 'content_type_cache'
-
-
1
require 'http'
-
1
require 'logger'
-
1
require 'rbconfig'
-
-
1
module Contentful
-
# The client object is initialized with a space and a key and then used
-
# for querying resources from this space.
-
# See README for details
-
1
class Client
-
# Default configuration for Contentful::Client
-
1
DEFAULT_CONFIGURATION = {
-
secure: true,
-
raise_errors: true,
-
raise_for_empty_fields: true,
-
dynamic_entries: :manual,
-
api_url: 'cdn.contentful.com',
-
api_version: 1,
-
environment: 'master',
-
authentication_mechanism: :header,
-
resource_builder: ResourceBuilder,
-
resource_mapping: {},
-
entry_mapping: {},
-
default_locale: 'en-US',
-
raw_mode: false,
-
gzip_encoded: true,
-
logger: false,
-
log_level: Logger::INFO,
-
proxy_host: nil,
-
proxy_username: nil,
-
proxy_password: nil,
-
proxy_port: nil,
-
max_rate_limit_retries: 1,
-
max_rate_limit_wait: 60,
-
max_include_resolution_depth: 20,
-
use_camel_case: false,
-
application_name: nil,
-
application_version: nil,
-
integration_name: nil,
-
integration_version: nil
-
}
-
-
1
attr_reader :configuration, :logger, :proxy
-
-
# Wraps the actual HTTP request via proxy
-
# @private
-
1
def self.get_http(url, query, headers = {}, proxy = {})
-
72
if proxy[:host]
-
HTTP[headers].via(proxy[:host], proxy[:port], proxy[:username], proxy[:password]).get(url, params: query)
-
else
-
72
HTTP[headers].get(url, params: query)
-
end
-
end
-
-
# @see _ https://github.com/contentful/contentful.rb#client-configuration-options
-
# @param [Hash] given_configuration
-
# @option given_configuration [String] :space Required
-
# @option given_configuration [String] :access_token Required
-
# @option given_configuration [String] :api_url Modifying this to 'preview.contentful.com' gives you access to our Preview API
-
# @option given_configuration [String] :api_version
-
# @option given_configuration [String] :default_locale
-
# @option given_configuration [String] :proxy_host
-
# @option given_configuration [String] :proxy_username
-
# @option given_configuration [String] :proxy_password
-
# @option given_configuration [Number] :proxy_port
-
# @option given_configuration [Number] :max_rate_limit_retries
-
# @option given_configuration [Number] :max_rate_limit_wait
-
# @option given_configuration [Number] :max_include_resolution_depth
-
# @option given_configuration [Boolean] :use_camel_case
-
# @option given_configuration [Boolean] :gzip_encoded
-
# @option given_configuration [Boolean] :raw_mode
-
# @option given_configuration [false, ::Logger] :logger
-
# @option given_configuration [::Logger::DEBUG, ::Logger::INFO, ::Logger::WARN, ::Logger::ERROR] :log_level
-
# @option given_configuration [Boolean] :raise_errors
-
# @option given_configuration [Boolean] :raise_for_empty_fields
-
# @option given_configuration [::Array<String>] :dynamic_entries
-
# @option given_configuration [::Hash<String, Contentful::Resource>] :resource_mapping
-
# @option given_configuration [::Hash<String, Contentful::Resource>] :entry_mapping
-
# @option given_configuration [String] :application_name
-
# @option given_configuration [String] :application_version
-
# @option given_configuration [String] :integration_name
-
# @option given_configuration [String] :integration_version
-
1
def initialize(given_configuration = {})
-
54
@configuration = default_configuration.merge(given_configuration)
-
54
normalize_configuration!
-
54
validate_configuration!
-
54
setup_logger
-
-
54
update_dynamic_entry_cache! if configuration[:dynamic_entries] == :auto
-
end
-
-
# @private
-
1
def setup_logger
-
54
@logger = configuration[:logger]
-
54
logger.level = configuration[:log_level] if logger
-
end
-
-
# @private
-
1
def proxy_params
-
72
{
-
host: configuration[:proxy_host],
-
port: configuration[:proxy_port],
-
username: configuration[:proxy_username],
-
password: configuration[:proxy_password]
-
}
-
end
-
-
# Returns the default configuration
-
# @private
-
1
def default_configuration
-
54
DEFAULT_CONFIGURATION.dup
-
end
-
-
# Gets the client's space
-
#
-
# @param [Hash] query
-
#
-
# @return [Contentful::Space]
-
1
def space(query = {})
-
Request.new(self, '', query).get
-
end
-
-
# Gets a specific content type
-
#
-
# @param [String] id
-
# @param [Hash] query
-
#
-
# @return [Contentful::ContentType]
-
1
def content_type(id, query = {})
-
Request.new(self, environment_url('/content_types'), query, id).get
-
end
-
-
# Gets a collection of content types
-
#
-
# @param [Hash] query
-
#
-
# @return [Contentful::Array<Contentful::ContentType>]
-
1
def content_types(query = {})
-
18
Request.new(self, environment_url('/content_types'), query).get
-
end
-
-
# Gets a specific entry
-
#
-
# @param [String] id
-
# @param [Hash] query
-
#
-
# @return [Contentful::Entry]
-
1
def entry(id, query = {})
-
36
normalize_select!(query)
-
36
query['sys.id'] = id
-
36
entries = Request.new(self, environment_url('/entries'), query).get
-
-
36
return entries if configuration[:raw_mode]
-
-
35
entries.first
-
end
-
-
# Gets a collection of entries
-
#
-
# @param [Hash] query
-
#
-
# @return [Contentful::Array<Contentful::Entry>]
-
1
def entries(query = {})
-
18
normalize_select!(query)
-
18
Request.new(self, environment_url('/entries'), query).get
-
end
-
-
# Gets a specific asset
-
#
-
# @param [String] id
-
# @param [Hash] query
-
#
-
# @return [Contentful::Asset]
-
1
def asset(id, query = {})
-
Request.new(self, environment_url('/assets'), query, id).get
-
end
-
-
# Gets a collection of assets
-
#
-
# @param [Hash] query
-
#
-
# @return [Contentful::Array<Contentful::Asset>]
-
1
def assets(query = {})
-
normalize_select!(query)
-
Request.new(self, environment_url('/assets'), query).get
-
end
-
-
# Gets a collection of locales for the current environment
-
#
-
# @param [Hash] query
-
#
-
# @return [Contentful::Array<Contentful::Locale>]
-
1
def locales(query = {})
-
Request.new(self, environment_url('/locales'), query).get
-
end
-
-
# Returns the base url for all of the client's requests
-
# @private
-
1
def base_url
-
72
"http#{configuration[:secure] ? 's' : ''}://#{configuration[:api_url]}/spaces/#{configuration[:space]}"
-
end
-
-
# Returns the url aware of the currently selected environment
-
# @private
-
1
def environment_url(path)
-
72
"/environments/#{configuration[:environment]}#{path}"
-
end
-
-
# Returns the formatted part of the X-Contentful-User-Agent header
-
# @private
-
1
def format_user_agent_header(key, values)
-
216
header = "#{key} #{values[:name]}"
-
216
header = "#{header}/#{values[:version]}" if values[:version]
-
216
"#{header};"
-
end
-
-
# Returns the X-Contentful-User-Agent sdk data
-
# @private
-
1
def sdk_info
-
72
{ name: 'contentful.rb', version: ::Contentful::VERSION }
-
end
-
-
# Returns the X-Contentful-User-Agent app data
-
# @private
-
1
def app_info
-
72
{ name: configuration[:application_name], version: configuration[:application_version] }
-
end
-
-
# Returns the X-Contentful-User-Agent integration data
-
# @private
-
1
def integration_info
-
72
{ name: configuration[:integration_name], version: configuration[:integration_version] }
-
end
-
-
# Returns the X-Contentful-User-Agent platform data
-
# @private
-
1
def platform_info
-
72
{ name: 'ruby', version: RUBY_VERSION }
-
end
-
-
# Returns the X-Contentful-User-Agent os data
-
# @private
-
1
def os_info
-
72
os_name = case ::RbConfig::CONFIG['host_os']
-
when /(cygwin|mingw|mswin|windows)/i then 'Windows'
-
72
when /(darwin|macruby|mac os)/i then 'macOS'
-
when /(linux|bsd|aix|solarix)/i then 'Linux'
-
end
-
72
{ name: os_name, version: Gem::Platform.local.version }
-
end
-
-
# Returns the X-Contentful-User-Agent
-
# @private
-
1
def contentful_user_agent
-
72
header = {
-
'sdk' => sdk_info,
-
'app' => app_info,
-
'integration' => integration_info,
-
'platform' => platform_info,
-
'os' => os_info
-
}
-
-
72
result = []
-
72
header.each do |key, values|
-
360
next unless values[:name]
-
216
result << format_user_agent_header(key, values)
-
end
-
72
result.join(' ')
-
end
-
-
# Returns the headers used for the HTTP requests
-
# @private
-
1
def request_headers
-
72
headers = { 'X-Contentful-User-Agent' => contentful_user_agent }
-
72
headers['Authorization'] = "Bearer #{configuration[:access_token]}" if configuration[:authentication_mechanism] == :header
-
72
headers['Content-Type'] = "application/vnd.contentful.delivery.v#{configuration[:api_version].to_i}+json" if configuration[:api_version]
-
72
headers['Accept-Encoding'] = 'gzip' if configuration[:gzip_encoded]
-
72
headers
-
end
-
-
# Patches a query hash with the client configurations for queries
-
# @private
-
1
def request_query(query)
-
72
if configuration[:authentication_mechanism] == :query_string
-
query['access_token'] = configuration[:access_token]
-
end
-
72
query
-
end
-
-
# Get a Contentful::Request object
-
# Set second parameter to false to deactivate Resource building and
-
# return Response objects instead
-
#
-
# @private
-
1
def get(request, build_resource = true)
-
72
retries_left = configuration[:max_rate_limit_retries]
-
72
result = nil
-
72
begin
-
72
response = run_request(request)
-
-
72
return response if !build_resource || configuration[:raw_mode]
-
-
71
return fail_response(response) if response.status != :ok
-
-
70
result = do_build_resource(response)
-
rescue UnparsableResource => error
-
raise error if configuration[:raise_errors]
-
return error
-
rescue Contentful::RateLimitExceeded => rate_limit_error
-
reset_time = rate_limit_error.reset_time.to_i
-
if should_retry(retries_left, reset_time, configuration[:max_rate_limit_wait])
-
retries_left -= 1
-
logger.info(retry_message(retries_left, reset_time)) if logger
-
sleep(reset_time * Random.new.rand(1.0..1.2))
-
retry
-
end
-
-
raise
-
end
-
-
70
result
-
end
-
-
# @private
-
1
def retry_message(retries_left, reset_time)
-
message = 'Contentful API Rate Limit Hit! '
-
message += "Retrying - Retries left: #{retries_left}"
-
message += "- Time until reset (seconds): #{reset_time}"
-
message
-
end
-
-
# @private
-
1
def fail_response(response)
-
1
fail response.object if configuration[:raise_errors]
-
response.object
-
end
-
-
# @private
-
1
def should_retry(retries_left, reset_time, max_wait)
-
retries_left > 0 && max_wait > reset_time
-
end
-
-
# Runs request and parses Response
-
# @private
-
1
def run_request(request)
-
72
url = request.absolute? ? request.url : base_url + request.url
-
72
logger.info(request: { url: url, query: request.query, header: request_headers }) if logger
-
72
Response.new(
-
self.class.get_http(
-
url,
-
request_query(request.query),
-
request_headers,
-
proxy_params
-
), request
-
)
-
end
-
-
# Runs Resource Builder
-
# @private
-
1
def do_build_resource(response)
-
70
logger.debug(response: response) if logger
-
configuration[:resource_builder].new(
-
response.object,
-
configuration.merge(endpoint: response.request.endpoint),
-
70
(response.request.query || {}).fetch(:locale, nil) == '*',
-
0
-
70
).run
-
end
-
-
# Use this method together with the client's :dynamic_entries configuration.
-
# See README for details.
-
# @private
-
1
def update_dynamic_entry_cache!
-
18
return if configuration[:raw_mode]
-
18
content_types(limit: 1000).map do |ct|
-
40
ContentTypeCache.cache_set(configuration[:space], ct.id, ct)
-
end
-
end
-
-
# Use this method to manually register a dynamic entry
-
# See examples/dynamic_entries.rb
-
# @private
-
1
def register_dynamic_entry(key, klass)
-
ContentTypeCache.cache_set(configuration[:space], key, klass)
-
end
-
-
# Create a new synchronisation object
-
#
-
# @param [Hash, String] options Options or Sync URL
-
#
-
# @note You will need to call #each_page or #first_page on it
-
#
-
# @return [Contentful::Sync]
-
1
def sync(options = { initial: true })
-
Sync.new(self, options)
-
end
-
-
1
private
-
-
# If the query contains the :select operator, we enforce :sys properties.
-
# The SDK requires sys.type to function properly, but as other of our SDKs
-
# require more parts of the :sys properties, we decided that every SDK should
-
# include the complete :sys block to provide consistency accross our SDKs.
-
1
def normalize_select!(query)
-
54
return unless query.key?(:select)
-
-
6
query[:select] = query[:select].split(',').map(&:strip) if query[:select].is_a? String
-
13
query[:select] = query[:select].reject { |p| p.start_with?('sys.') }
-
6
query[:select] << 'sys' unless query[:select].include?('sys')
-
end
-
-
1
def normalize_configuration!
-
270
%i[space access_token api_url default_locale].each { |s| configuration[s] = configuration[s].to_s }
-
54
configuration[:authentication_mechanism] = configuration[:authentication_mechanism].to_sym
-
end
-
-
1
def validate_configuration!
-
54
fail ArgumentError, 'You will need to initialize a client with a :space' if configuration[:space].empty?
-
54
fail ArgumentError, 'You will need to initialize a client with an :access_token' if configuration[:access_token].empty?
-
54
fail ArgumentError, 'The client configuration needs to contain an :api_url' if configuration[:api_url].empty?
-
54
fail ArgumentError, 'The client configuration needs to contain a :default_locale' if configuration[:default_locale].empty?
-
54
fail ArgumentError, 'The :api_version must be a positive number or nil' unless configuration[:api_version].to_i >= 0
-
54
fail ArgumentError, 'The authentication mechanism must be :header or :query_string' unless %i[header query_string].include?(
-
configuration[:authentication_mechanism]
-
)
-
54
fail ArgumentError, 'The :dynamic_entries mode must be :auto or :manual' unless %i[auto manual].include?(
-
configuration[:dynamic_entries]
-
)
-
end
-
end
-
end
-
1
require_relative 'location'
-
1
require_relative 'link'
-
-
1
module Contentful
-
# Basic Coercion
-
1
class BaseCoercion
-
1
attr_reader :value, :options
-
1
def initialize(value, options = {})
-
61
@value = value
-
61
@options = options
-
end
-
-
# Coerces value
-
1
def coerce(*)
-
5
value
-
end
-
end
-
-
# Coercion for String Types
-
1
class StringCoercion < BaseCoercion
-
# Coerces value to String
-
1
def coerce(*)
-
43
value.to_s
-
end
-
end
-
-
# Coercion for Text Types
-
1
class TextCoercion < StringCoercion; end
-
-
# Coercion for Symbol Types
-
1
class SymbolCoercion < StringCoercion; end
-
-
# Coercion for Integer Types
-
1
class IntegerCoercion < BaseCoercion
-
# Coerces value to Integer
-
1
def coerce(*)
-
1
value.to_i
-
end
-
end
-
-
# Coercion for Float Types
-
1
class FloatCoercion < BaseCoercion
-
# Coerces value to Float
-
1
def coerce(*)
-
1
value.to_f
-
end
-
end
-
-
# Coercion for Boolean Types
-
1
class BooleanCoercion < BaseCoercion
-
# Coerces value to Boolean
-
1
def coerce(*)
-
# rubocop:disable Style/DoubleNegation
-
!!value
-
# rubocop:enable Style/DoubleNegation
-
end
-
end
-
-
# Coercion for Date Types
-
1
class DateCoercion < BaseCoercion
-
# Coerces value to DateTime
-
1
def coerce(*)
-
return nil if value.nil?
-
return value if value.is_a?(Date)
-
-
DateTime.parse(value)
-
end
-
end
-
-
# Coercion for Location Types
-
1
class LocationCoercion < BaseCoercion
-
# Coerces value to Location
-
1
def coerce(*)
-
Location.new(value)
-
end
-
end
-
-
# Coercion for Object Types
-
1
class ObjectCoercion < BaseCoercion
-
# Coerces value to hash, symbolizing each key
-
1
def coerce(*)
-
2
JSON.parse(JSON.dump(value), symbolize_names: true)
-
end
-
end
-
-
# Coercion for Link Types
-
# Nothing should be done here as include resolution is handled within
-
# entries due to depth handling (explained within Entry).
-
# Only present as a placeholder for proper resolution within ContentType.
-
1
class LinkCoercion < BaseCoercion; end
-
-
# Coercion for Array Types
-
1
class ArrayCoercion < BaseCoercion
-
# Coerces value for each element
-
1
def coerce(*)
-
2
value.map do |e|
-
3
options[:coercion_class].new(e).coerce
-
end
-
end
-
end
-
-
# Coercion for RichText Types
-
1
class RichTextCoercion < BaseCoercion
-
# Resolves includes and removes unresolvable nodes
-
1
def coerce(configuration)
-
7
coerce_block(value, configuration)
-
end
-
-
1
private
-
-
1
def link?(node)
-
226
!node['data'].is_a?(::Contentful::Entry) &&
-
!node.fetch('data', {}).empty? &&
-
node['data']['target']
-
end
-
-
1
def content_block?(node)
-
211
!node.fetch('content', []).empty?
-
end
-
-
1
def coerce_block(block, configuration)
-
125
return block unless block.is_a?(Hash) && block.key?('content')
-
-
125
invalid_nodes = []
-
125
coerced_nodes = {}
-
125
block['content'].each_with_index do |node, index|
-
226
if link?(node)
-
15
link = coerce_link(node, configuration)
-
-
15
if !link.nil?
-
14
node['data']['target'] = link
-
else
-
1
invalid_nodes << index
-
end
-
211
elsif content_block?(node)
-
118
coerced_nodes[index] = coerce_block(node, configuration)
-
end
-
end
-
-
125
coerced_nodes.each do |index, coerced_node|
-
118
block['content'][index] = coerced_node
-
end
-
-
125
invalid_nodes.each do |index|
-
1
block['content'].delete_at(index)
-
end
-
-
125
block
-
end
-
-
1
def coerce_link(node, configuration)
-
15
return node unless node.key?('data') && node['data'].key?('target')
-
15
return node['data']['target'] unless node['data']['target'].is_a?(::Hash)
-
14
return node unless node['data']['target']['sys']['type'] == 'Link'
-
-
14
return nil if Support.unresolvable?(node['data']['target'], configuration[:errors])
-
-
13
resource = Support.resource_for_link(
-
node['data']['target'],
-
configuration[:includes_for_single]
-
)
-
-
# Resource is valid but unreachable
-
13
return Link.new(node['data']['target'], configuration) if resource.nil?
-
-
ResourceBuilder.new(
-
resource,
-
configuration,
-
configuration[:localized],
-
configuration[:depth] + 1,
-
configuration[:errors]
-
8
).run
-
end
-
end
-
end
-
1
require_relative 'base_resource'
-
1
require_relative 'field'
-
1
require_relative 'support'
-
-
1
module Contentful
-
# Resource Class for Content Types
-
# https://www.contentful.com/developers/documentation/content-delivery-api/#content-types
-
1
class ContentType < BaseResource
-
1
attr_reader :name, :description, :fields, :display_field
-
-
1
def initialize(item, *)
-
40
super
-
-
40
@name = item.fetch('name', nil)
-
40
@description = item.fetch('description', nil)
-
130
@fields = item.fetch('fields', []).map { |field| Field.new(field) }
-
40
@display_field = item.fetch('displayField', nil)
-
end
-
-
# Field definition for field
-
1
def field_for(field_id)
-
181
fields.detect { |f| Support.snakify(f.id) == Support.snakify(field_id) }
-
end
-
-
1
alias displayField display_field
-
-
1
protected
-
-
1
def repr_name
-
"#{super}[#{name}]"
-
end
-
end
-
end
-
1
module Contentful
-
# Cache for Content Types
-
1
class ContentTypeCache
-
1
@cache = {}
-
-
1
class << self
-
1
attr_reader :cache
-
end
-
-
# Clears the Content Type Cache
-
1
def self.clear!
-
@cache = {}
-
end
-
-
# Gets a Content Type from the Cache
-
1
def self.cache_get(space_id, content_type_id)
-
2711
@cache.fetch(space_id, {}).fetch(content_type_id.to_sym, nil)
-
end
-
-
# Sets a Content Type in the Cache
-
1
def self.cache_set(space_id, content_type_id, klass)
-
40
@cache[space_id] ||= {}
-
40
@cache[space_id][content_type_id.to_sym] = klass
-
end
-
end
-
end
-
1
require_relative 'base_resource'
-
-
1
module Contentful
-
# Resource class for deleted entries
-
# https://www.contentful.com/developers/documentation/content-delivery-api/http/#sync-item-types
-
1
class DeletedAsset < BaseResource; end
-
end
-
1
require_relative 'base_resource'
-
-
1
module Contentful
-
# Resource class for deleted entries
-
# https://www.contentful.com/developers/documentation/content-delivery-api/http/#sync-item-types
-
1
class DeletedEntry < BaseResource; end
-
end
-
1
require_relative 'error'
-
1
require_relative 'fields_resource'
-
1
require_relative 'content_type_cache'
-
1
require_relative 'resource_references'
-
-
1
module Contentful
-
# Resource class for Entry.
-
# @see _ https://www.contentful.com/developers/documentation/content-delivery-api/#entries
-
1
class Entry < FieldsResource
-
1
include Contentful::ResourceReferences
-
-
# Returns true for resources that are entries
-
1
def entry?
-
4
true
-
end
-
-
1
private
-
-
1
def coerce(field_id, value, includes, errors, entries = {})
-
3765
if Support.link?(value) && !Support.unresolvable?(value, errors)
-
1039
return build_nested_resource(value, includes, entries, errors)
-
end
-
2726
return coerce_link_array(value, includes, errors, entries) if Support.link_array?(value)
-
-
2708
content_type_key = Support.snakify('contentType', @configuration[:use_camel_case])
-
2708
content_type = ContentTypeCache.cache_get(sys[:space].id, sys[content_type_key.to_sym].id)
-
-
2708
unless content_type.nil?
-
58
content_type_field = content_type.field_for(field_id)
-
58
coercion_configuration = @configuration.merge(
-
includes_for_single:
-
@configuration.fetch(:includes_for_single, []) + includes,
-
_entries_cache: entries,
-
localized: localized,
-
depth: @depth,
-
errors: errors
-
)
-
58
return content_type_field.coerce(value, coercion_configuration) unless content_type_field.nil?
-
end
-
-
2650
super(field_id, value, includes, errors, entries)
-
end
-
-
1
def coerce_link_array(value, includes, errors, entries)
-
18
items = []
-
18
value.each do |link|
-
32
nested_resource = build_nested_resource(link, includes, entries, errors) unless Support.unresolvable?(link, errors)
-
32
items << nested_resource unless nested_resource.nil?
-
end
-
-
18
items
-
end
-
-
# Maximum include depth is 10 in the API, but we raise it to 20 (by default),
-
# in case one of the included items has a reference in an upper level,
-
# so we can keep the include chain for that object as well
-
# Any included object after the maximum include resolution depth will be just a Link
-
1
def build_nested_resource(value, includes, entries, errors)
-
1067
if @depth < @configuration.fetch(:max_include_resolution_depth, 20)
-
1002
resource = Support.resource_for_link(value, includes)
-
1002
return resolve_include(resource, includes, entries, errors) unless resource.nil?
-
end
-
-
65
build_link(value)
-
end
-
-
1
def resolve_include(resource, includes, entries, errors)
-
1002
require_relative 'resource_builder'
-
-
ResourceBuilder.new(
-
resource,
-
@configuration.merge(
-
includes_for_single:
-
@configuration.fetch(:includes_for_single, []) + includes,
-
_entries_cache: entries
-
),
-
localized,
-
@depth + 1,
-
errors
-
1002
).run
-
end
-
-
1
def known_link?(name)
-
113
field_name = name.to_sym
-
113
return true if known_contentful_object?(fields[field_name])
-
130
fields[field_name].is_a?(Enumerable) && fields[field_name].any? { |object| known_contentful_object?(object) }
-
end
-
-
1
def known_contentful_object?(object)
-
150
(object.is_a?(Contentful::Entry) || object.is_a?(Contentful::Asset))
-
end
-
-
1
def method_missing(name, *args, &block)
-
3
return empty_field_error(name) if content_type_field?(name)
-
-
1
super
-
end
-
-
1
def respond_to_missing?(name, include_private = false)
-
content_type_field?(name) || super
-
end
-
-
1
protected
-
-
1
def content_type_field?(name)
-
3
content_type = ContentTypeCache.cache_get(
-
sys[:space].id,
-
sys[:content_type].id
-
)
-
-
3
return false if content_type.nil?
-
-
3
!content_type.field_for(name).nil?
-
end
-
-
1
def empty_field_error(name)
-
2
return nil unless @configuration[:raise_for_empty_fields]
-
1
fail EmptyFieldError, name
-
end
-
-
1
def repr_name
-
content_type_key = Support.snakify('contentType', @configuration[:use_camel_case]).to_sym
-
"#{super}[#{sys[content_type_key].id}]"
-
end
-
end
-
end
-
1
module Contentful
-
# All errors raised by the contentful gem are either instances of Contentful::Error
-
# or inherit from Contentful::Error
-
1
class Error < StandardError
-
1
attr_reader :response
-
-
1
def initialize(response)
-
1
@response = response
-
1
super best_available_message
-
end
-
-
# Shortcut for creating specialized error classes
-
# USAGE rescue Contentful::Error[404]
-
1
def self.[](error_status_code)
-
1
errors = {
-
400 => BadRequest,
-
401 => Unauthorized,
-
403 => AccessDenied,
-
404 => NotFound,
-
429 => RateLimitExceeded,
-
500 => ServerError,
-
502 => BadGateway,
-
503 => ServiceUnavailable
-
}
-
-
1
errors.key?(error_status_code) ? errors[error_status_code] : Error
-
end
-
-
1
protected
-
-
1
def default_error_message
-
"The following error was received: #{@response.raw.body}"
-
end
-
-
1
def handle_details(details)
-
details.to_s
-
end
-
-
1
def additional_info?
-
1
false
-
end
-
-
1
def additional_info
-
[]
-
end
-
-
1
def best_available_message
-
1
error_message = [
-
"HTTP status code: #{@response.raw.status}"
-
]
-
-
1
begin
-
1
response_json = @response.load_json
-
1
message = response_json.fetch('message', default_error_message)
-
1
details = response_json.fetch('details', nil)
-
1
request_id = response_json.fetch('requestId', nil)
-
-
1
error_message << "Message: #{message}"
-
1
error_message << "Details: #{handle_details(details)}" if details
-
1
error_message << "Request ID: #{request_id}" if request_id
-
rescue
-
error_message << "Message: #{default_error_message}"
-
end
-
-
1
error_message << additional_info if additional_info?
-
-
1
error_message.join("\n")
-
end
-
end
-
-
# 400
-
1
class BadRequest < Error
-
1
protected
-
-
1
def default_error_message
-
1
'The request was malformed or missing a required parameter.'
-
end
-
-
1
def handle_details(details)
-
return details if details.is_a?(String)
-
-
handle_detail = proc do |detail|
-
return detail if detail.is_a?(String)
-
detail.fetch('details', nil)
-
end
-
-
inner_details = details['errors'].map { |detail| handle_detail[detail] }.reject(&:nil?)
-
inner_details.join("\n\t")
-
end
-
end
-
-
# 401
-
1
class Unauthorized < Error
-
1
protected
-
-
1
def default_error_message
-
'The authorization token was invalid.'
-
end
-
end
-
-
# 403
-
1
class AccessDenied < Error
-
1
protected
-
-
1
def default_error_message
-
'The specified token does not have access to the requested resource.'
-
end
-
-
1
def handle_details(details)
-
"\n\tReasons:\n\t\t#{details['reasons'].join("\n\t\t")}"
-
end
-
end
-
-
# 404
-
1
class NotFound < Error
-
1
protected
-
-
1
def default_error_message
-
'The requested resource or endpoint could not be found.'
-
end
-
-
1
def handle_details(details)
-
return details if details.is_a?(String)
-
-
type = details['type'] || (details['sys'] || {})['type']
-
message = "The requested #{type} could not be found."
-
-
resource_id = details.fetch('id', nil)
-
message += " ID: #{resource_id}." if resource_id
-
-
message
-
end
-
end
-
-
# 429
-
1
class RateLimitExceeded < Error
-
# Rate Limit Reset Header Key
-
1
RATE_LIMIT_RESET_HEADER_KEY = 'x-contentful-ratelimit-reset'
-
-
1
def reset_time?
-
# rubocop:disable Style/DoubleNegation
-
!!reset_time
-
# rubocop:enable Style/DoubleNegation
-
end
-
-
# Time until next available request, in seconds.
-
1
def reset_time
-
@reset_time ||= @response.raw[RATE_LIMIT_RESET_HEADER_KEY]
-
end
-
-
1
protected
-
-
1
def additional_info?
-
reset_time?
-
end
-
-
1
def additional_info
-
["Time until reset (seconds): #{reset_time}"]
-
end
-
-
1
def default_error_message
-
'Rate limit exceeded. Too many requests.'
-
end
-
end
-
-
# 500
-
1
class ServerError < Error
-
1
protected
-
-
1
def default_error_message
-
'Internal server error.'
-
end
-
end
-
-
# 502
-
1
class BadGateway < Error
-
1
protected
-
-
1
def default_error_message
-
'The requested space is hibernated.'
-
end
-
end
-
-
# 503
-
1
class ServiceUnavailable < Error
-
1
protected
-
-
1
def default_error_message
-
'The server is currently unable to handle the request due to a temporary overloading or maintenance of the server.'
-
end
-
end
-
-
# Raised when response is no valid json
-
1
class UnparsableJson < Error
-
1
protected
-
-
1
def default_error_message
-
@response.error_message
-
end
-
end
-
-
# Raised when response is not parsable as a Contentful::Resource
-
1
class UnparsableResource < StandardError; end
-
-
# Raised when an undefined field is requested
-
1
class EmptyFieldError < StandardError
-
1
def initialize(name)
-
1
super("The field '#{name}' is empty and unavailable in the response")
-
end
-
end
-
end
-
1
require_relative 'location'
-
1
require_relative 'coercions'
-
-
1
module Contentful
-
# A ContentType's field schema
-
# See https://www.contentful.com/developers/documentation/content-management-api/#resources-content-types-fields
-
1
class Field
-
# Coercions from Contentful Types to Ruby native types
-
1
KNOWN_TYPES = {
-
'String' => StringCoercion,
-
'Text' => TextCoercion,
-
'Symbol' => SymbolCoercion,
-
'Integer' => IntegerCoercion,
-
'Number' => FloatCoercion,
-
'Boolean' => BooleanCoercion,
-
'Date' => DateCoercion,
-
'Location' => LocationCoercion,
-
'Object' => ObjectCoercion,
-
'Array' => ArrayCoercion,
-
'Link' => LinkCoercion,
-
'RichText' => RichTextCoercion
-
}
-
-
1
attr_reader :raw, :id, :name, :type, :link_type, :items, :required, :localized
-
-
1
def initialize(json)
-
102
@raw = json
-
102
@id = json.fetch('id', nil)
-
102
@name = json.fetch('name', nil)
-
102
@type = json.fetch('type', nil)
-
102
@link_type = json.fetch('linkType', nil)
-
102
@items = json.key?('items') ? Field.new(json.fetch('items', {})) : nil
-
102
@required = json.fetch('required', false)
-
102
@localized = json.fetch('localized', false)
-
end
-
-
# Coerces value to proper type
-
1
def coerce(value, configuration)
-
58
return value if type.nil?
-
58
return value if value.nil?
-
-
58
options = {}
-
58
options[:coercion_class] = KNOWN_TYPES[items.type] unless items.nil?
-
58
KNOWN_TYPES[type].new(value, options).coerce(configuration)
-
end
-
end
-
end
-
1
require_relative 'support'
-
1
require_relative 'base_resource'
-
-
1
module Contentful
-
# Base definition of a Contentful Resource containing Field properties
-
1
class FieldsResource < BaseResource
-
1
attr_reader :localized
-
-
# rubocop:disable Metrics/ParameterLists
-
1
def initialize(item, _configuration, localized = false, includes = [], entries = {}, depth = 0, errors = [])
-
1060
super
-
-
1060
@configuration[:errors] = errors
-
1060
@localized = localized
-
1060
@fields = hydrate_fields(includes, entries, errors)
-
1060
define_fields_methods!
-
end
-
-
# Returns all fields of the asset
-
#
-
# @return [Hash] fields for Resource on selected locale
-
1
def fields(wanted_locale = nil)
-
1382
wanted_locale = internal_resource_locale if wanted_locale.nil?
-
1382
@fields.fetch(wanted_locale.to_s, {})
-
end
-
-
# Returns all fields of the asset with locales nested by field
-
#
-
# @return [Hash] fields for Resource grouped by field name
-
1
def fields_with_locales
-
5
remapped_fields = {}
-
5
locales.each do |locale|
-
8
fields(locale).each do |name, value|
-
38
remapped_fields[name] ||= {}
-
38
remapped_fields[name][locale.to_sym] = value
-
end
-
end
-
-
5
remapped_fields
-
end
-
-
# Provides a list of the available locales for a Resource
-
1
def locales
-
52
@fields.keys
-
end
-
-
# @private
-
1
def marshal_dump
-
24
{
-
configuration: @configuration,
-
raw: raw_with_links,
-
localized: localized
-
}
-
end
-
-
# @private
-
1
def marshal_load(raw_object)
-
34
super(raw_object)
-
34
@localized = raw_object[:localized]
-
34
@fields = hydrate_fields(
-
raw_object[:configuration].fetch(:includes_for_single, []),
-
{},
-
raw_object[:configuration].fetch(:errors, [])
-
)
-
34
define_fields_methods!
-
end
-
-
# @private
-
1
def raw_with_links
-
137
links = fields.keys.select { |property| known_link?(property) }
-
24
processed_raw = raw.clone
-
24
raw['fields'].each do |k, v|
-
127
links_key = Support.snakify(k, @configuration[:use_camel_case])
-
127
processed_raw['fields'][k] = links.include?(links_key.to_sym) ? send(links_key) : v
-
end
-
-
24
processed_raw
-
end
-
-
1
private
-
-
1
def define_fields_methods!
-
1094
fields.each do |k, v|
-
4709
define_singleton_method k do
-
274
v
-
end
-
end
-
end
-
-
1
def hydrate_localized_fields(includes, errors, entries)
-
94
locale = internal_resource_locale
-
94
result = { locale => {} }
-
94
raw['fields'].each do |name, locales|
-
393
locales.each do |loc, value|
-
441
result[loc] ||= {}
-
441
name = Support.snakify(name, @configuration[:use_camel_case])
-
441
result[loc][name.to_sym] = coerce(
-
name,
-
value,
-
includes,
-
errors,
-
entries
-
)
-
end
-
end
-
-
94
result
-
end
-
-
1
def hydrate_nonlocalized_fields(includes, errors, entries)
-
998
result = { locale => {} }
-
998
locale = internal_resource_locale
-
998
raw['fields'].each do |name, value|
-
4316
name = Support.snakify(name, @configuration[:use_camel_case])
-
4316
result[locale][name.to_sym] = coerce(
-
name,
-
value,
-
includes,
-
errors,
-
entries
-
)
-
end
-
-
998
result
-
end
-
-
1
def hydrate_fields(includes, entries, errors)
-
1094
return {} unless raw.key?('fields')
-
-
1092
if localized
-
94
hydrate_localized_fields(includes, errors, entries)
-
else
-
998
hydrate_nonlocalized_fields(includes, errors, entries)
-
end
-
end
-
-
1
protected
-
-
1
def coerce(_field_id, value, _includes, _errors, _entries)
-
3642
value
-
end
-
end
-
end
-
1
module Contentful
-
# An Assets's file info
-
1
class File
-
1
def initialize(json, configuration)
-
496
@configuration = configuration
-
-
496
define_fields!(json)
-
end
-
-
1
private
-
-
1
def define_fields!(json)
-
496
json.each do |k, v|
-
1984
define_singleton_method Support.snakify(k, @configuration[:use_camel_case]) do
-
6
v
-
end
-
end
-
end
-
end
-
end
-
1
require_relative 'base_resource'
-
-
1
module Contentful
-
# Resource Class for Links
-
# https://www.contentful.com/developers/documentation/content-delivery-api/#links
-
1
class Link < BaseResource
-
# Queries contentful for the Resource the Link is refering to
-
# Takes an optional query hash
-
1
def resolve(client, query = {})
-
id_and_query = [(id unless link_type == 'Space')].compact + [query]
-
client.public_send(
-
Contentful::Support.snakify(link_type).to_sym,
-
*id_and_query
-
)
-
end
-
end
-
end
-
1
require_relative 'base_resource'
-
-
1
module Contentful
-
# A Locale definition as included in Space
-
# Read more about Localization at https://www.contentful.com/developers/documentation/content-delivery-api/#i18n
-
1
class Locale < BaseResource
-
1
attr_reader :code, :name, :default, :fallback_code
-
-
1
def initialize(item, *)
-
@code = item.fetch('code', nil)
-
@name = item.fetch('name', nil)
-
@default = item.fetch('default', false)
-
@fallback_code = item.fetch('fallbackCode', nil)
-
end
-
end
-
end
-
1
module Contentful
-
# Location Field Type
-
# You can directly query for them: https://www.contentful.com/developers/documentation/content-delivery-api/#search-filter-geo
-
1
class Location
-
1
attr_reader :lat, :lon
-
1
alias latitude lat
-
1
alias longitude lon
-
-
1
def initialize(json)
-
@lat = json.fetch('lat', nil)
-
@lon = json.fetch('lon', nil)
-
end
-
end
-
end
-
1
module Contentful
-
# This object represents a request that is to be made. It gets initialized by the client
-
# with domain specific logic. The client later uses the Request's #url and #query methods
-
# to execute the HTTP request.
-
1
class Request
-
1
attr_reader :client, :type, :query, :id, :endpoint
-
-
1
def initialize(client, endpoint, query = {}, id = nil)
-
72
@client = client
-
72
@endpoint = endpoint
-
-
72
@query = (normalize_query(query) if query && !query.empty?)
-
-
72
if id
-
@type = :single
-
@id = URI.escape(id)
-
else
-
72
@type = :multi
-
72
@id = nil
-
end
-
end
-
-
# Returns the final URL, relative to a contentful space
-
1
def url
-
72
"#{@endpoint}#{@type == :single ? "/#{id}" : ''}"
-
end
-
-
# Delegates the actual HTTP work to the client
-
1
def get
-
72
client.get(self)
-
end
-
-
# Returns true if endpoint is an absolute url
-
1
def absolute?
-
72
@endpoint.start_with?('http')
-
end
-
-
# Returns a new Request object with the same data
-
1
def copy
-
Marshal.load(Marshal.dump(self))
-
end
-
-
1
private
-
-
1
def normalize_query(query)
-
68
Hash[
-
query.map do |key, value|
-
89
[
-
key.to_sym,
-
89
value.is_a?(::Array) ? value.join(',') : value
-
]
-
end
-
]
-
end
-
end
-
end
-
1
require_relative 'error'
-
1
require_relative 'space'
-
1
require_relative 'content_type'
-
1
require_relative 'entry'
-
1
require_relative 'asset'
-
1
require_relative 'array'
-
1
require_relative 'link'
-
1
require_relative 'deleted_entry'
-
1
require_relative 'deleted_asset'
-
1
require_relative 'locale'
-
-
1
module Contentful
-
# Transforms a Contentful::Response into a Contentful::Resource or a Contentful::Error
-
# See example/resource_mapping.rb for advanced usage
-
1
class ResourceBuilder
-
# Default Resource Mapping
-
# @see _ README for more information on Resource Mapping
-
1
DEFAULT_RESOURCE_MAPPING = {
-
'Space' => Space,
-
'ContentType' => ContentType,
-
'Entry' => Entry,
-
'Asset' => Asset,
-
'Array' => Array,
-
'Link' => Link,
-
'DeletedEntry' => DeletedEntry,
-
'DeletedAsset' => DeletedAsset,
-
'Locale' => Locale
-
}
-
# Default Entry Mapping
-
# @see _ README for more information on Entry Mapping
-
1
DEFAULT_ENTRY_MAPPING = {}
-
-
1
attr_reader :json, :default_locale, :endpoint, :depth, :localized, :resource_mapping, :entry_mapping, :resource
-
-
1
def initialize(json, configuration = {}, localized = false, depth = 0, errors = [])
-
1080
@json = json
-
1080
@default_locale = configuration.fetch(:default_locale, ::Contentful::Client::DEFAULT_CONFIGURATION[:default_locale])
-
1080
@resource_mapping = default_resource_mapping.merge(configuration.fetch(:resource_mapping, {}))
-
1080
@entry_mapping = default_entry_mapping.merge(configuration.fetch(:entry_mapping, {}))
-
1080
@includes_for_single = configuration.fetch(:includes_for_single, [])
-
1080
@localized = localized
-
1080
@depth = depth
-
1080
@endpoint = configuration.fetch(:endpoint, nil)
-
1080
@configuration = configuration
-
1080
@resource_cache = configuration[:_entries_cache] || {}
-
1080
@errors = errors
-
end
-
-
# Starts the parsing process.
-
#
-
# @return [Contentful::Resource, Contentful::Error]
-
1
def run
-
1080
return build_array if array?
-
1010
build_single
-
rescue UnparsableResource => error
-
error
-
end
-
-
1
private
-
-
1
def build_array
-
70
includes = fetch_includes || @includes_for_single
-
70
errors = fetch_errors || @errors
-
-
70
result = json['items'].map do |item|
-
92
next if Support.unresolvable?(item, errors)
-
92
build_item(item, includes, errors)
-
end
-
70
array_class = fetch_array_class
-
70
array_class.new(json.dup.merge('items' => result), @configuration, endpoint)
-
end
-
-
1
def build_single
-
1010
return if Support.unresolvable?(json, @errors)
-
1010
includes = @includes_for_single
-
1010
build_item(json, includes, @errors)
-
end
-
-
1
def build_item(item, includes = [], errors = [])
-
1102
buildables = %w[Entry Asset ContentType Space DeletedEntry DeletedAsset Locale]
-
2769
item_type = buildables.detect { |b| b.to_s == item['sys']['type'] }
-
1102
fail UnparsableResource, 'Item type is not known, could not parse' if item_type.nil?
-
1102
item_class = resource_class(item)
-
-
1102
reuse_entries = @configuration.fetch(:reuse_entries, false)
-
1102
resource_cache = @resource_cache ? @resource_cache : {}
-
-
1102
id = "#{item['sys']['type']}:#{item['sys']['id']}"
-
1102
resource = if reuse_entries && resource_cache.key?(id)
-
2
resource_cache[id]
-
else
-
1100
item_class.new(item, @configuration, localized?, includes, resource_cache, depth, errors)
-
end
-
-
1102
resource
-
end
-
-
1
def fetch_includes
-
70
Support.includes_from_response(json)
-
end
-
-
1
def fetch_errors
-
70
json.fetch('errors', [])
-
end
-
-
1
def resource_class(item)
-
1102
return fetch_custom_resource_class(item) if %w[Entry DeletedEntry Asset DeletedAsset].include?(item['sys']['type'])
-
40
resource_mapping[item['sys']['type']]
-
end
-
-
1
def fetch_custom_resource_class(item)
-
1062
case item['sys']['type']
-
when 'Entry'
-
577
resource_class = entry_mapping[item['sys']['contentType']['sys']['id']]
-
577
return resource_class unless resource_class.nil?
-
-
577
fetch_custom_resource_mapping(item, 'Entry', Entry)
-
when 'Asset'
-
485
fetch_custom_resource_mapping(item, 'Asset', Asset)
-
when 'DeletedEntry'
-
fetch_custom_resource_mapping(item, 'DeletedEntry', DeletedEntry)
-
when 'DeletedAsset'
-
fetch_custom_resource_mapping(item, 'DeletedAsset', DeletedAsset)
-
end
-
end
-
-
1
def fetch_custom_resource_mapping(item, type, default_class)
-
1062
resources = resource_mapping[type]
-
1062
return default_class if resources.nil?
-
-
1062
return resources if resources.is_a?(Class)
-
return resources[item] if resources.respond_to?(:call)
-
-
default_class
-
end
-
-
1
def fetch_array_class
-
70
return SyncPage if sync?
-
70
::Contentful::Array
-
end
-
-
1
def localized?
-
1100
return true if @localized
-
1006
return true if array? && sync?
-
1006
false
-
end
-
-
1
def array?
-
2086
json.fetch('sys', {}).fetch('type', '') == 'Array'
-
end
-
-
1
def sync?
-
158
json.fetch('nextSyncUrl', nil) || json.fetch('nextPageUrl', nil)
-
end
-
-
# The default mapping for #detect_resource_class
-
1
def default_resource_mapping
-
1080
DEFAULT_RESOURCE_MAPPING.dup
-
end
-
-
# The default entry mapping
-
1
def default_entry_mapping
-
1080
DEFAULT_ENTRY_MAPPING.dup
-
end
-
end
-
end
-
1
module Contentful
-
# Method to retrieve references (incoming links) for a given entry or asset
-
1
module ResourceReferences
-
# Gets a collection of entries which links to current entry
-
#
-
# @param [Contentful::Client] client
-
# @param [Hash] query
-
#
-
# @return [Contentful::Array<Contentful::Entry>, false]
-
1
def incoming_references(client = nil, query = {})
-
2
return false unless client
-
-
2
query = is_a?(Contentful::Entry) ? query.merge(links_to_entry: id) : query.merge(links_to_asset: id)
-
-
2
client.entries(query)
-
end
-
end
-
end
-
1
require_relative 'base_resource'
-
1
require_relative 'locale'
-
-
1
module Contentful
-
# Resource class for Space.
-
# https://www.contentful.com/developers/documentation/content-delivery-api/#spaces
-
1
class Space < BaseResource
-
1
attr_reader :name, :locales
-
-
1
def initialize(item, *)
-
super
-
-
@name = item.fetch('name', nil)
-
@locales = item.fetch('locales', []).map { |locale| Locale.new(locale) }
-
end
-
-
# @private
-
1
def reload(client = nil)
-
return client.space unless client.nil?
-
-
false
-
end
-
end
-
end
-
1
module Contentful
-
# Utility methods used by the contentful gem
-
1
module Support
-
1
class << self
-
# Transforms CamelCase into snake_case (taken from zucker)
-
#
-
# @param [String] object camelCaseName
-
# @param [Boolean] skip if true, skips returns original object
-
#
-
# @return [String] snake_case_name
-
1
def snakify(object, skip = false)
-
23898
return object if skip
-
-
String(object)
-
.gsub(/::/, '/')
-
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
-
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
-
.tr('-', '_')
-
23025
.downcase
-
end
-
-
1
def unresolvable?(value, errors)
-
2187
return true if value.nil?
-
-
2561
errors.any? { |i| i.fetch('details', {}).fetch('id', nil) == value['sys']['id'] }
-
end
-
-
# Checks if value is a link
-
#
-
# @param value
-
#
-
# @return [true, false]
-
1
def link?(value)
-
4302
value.is_a?(::Hash) &&
-
value.fetch('sys', {}).fetch('type', '') == 'Link'
-
end
-
-
# Checks if value is an array of links
-
#
-
# @param value
-
#
-
# @return [true, false]
-
1
def link_array?(value)
-
2726
return link?(value[0]) if value.is_a?(::Array) && !value.empty?
-
-
2189
false
-
end
-
-
# Returns the resource that matches the link
-
#
-
# @param [Hash] link
-
# @param [::Array] includes
-
#
-
# @return [Hash]
-
1
def resource_for_link(link, includes)
-
1015
includes.detect do |i|
-
2685
i['sys']['id'] == link['sys']['id'] &&
-
i['sys']['type'] == link['sys']['linkType']
-
end
-
end
-
-
# Returns combined include array from an API Response
-
#
-
# @param [Hash] json JSON Response
-
# @param [Bool] raw Response pre-proccessed?
-
#
-
# @return [Array]
-
1
def includes_from_response(json, raw = true)
-
70
includes = if raw
-
70
json['items'].dup
-
else
-
json['items'].map(&:raw)
-
end
-
-
70
%w[Entry Asset].each do |type|
-
140
if json.fetch('includes', {}).key?(type)
-
70
includes.concat(json['includes'].fetch(type, []))
-
end
-
end
-
-
70
includes
-
end
-
end
-
end
-
end
-
1
require_relative 'resource_builder'
-
1
require_relative 'deleted_entry'
-
1
require_relative 'deleted_asset'
-
1
require_relative 'sync_page'
-
-
1
module Contentful
-
# Resource class for Sync.
-
# @see _ https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/synchronization
-
1
class Sync
-
1
attr_reader :next_sync_url
-
-
1
def initialize(client, options_or_url)
-
@client = client
-
@next_sync_url = nil
-
@first_page_options_or_url = options_or_url
-
end
-
-
# Iterates over all pages of the current sync
-
#
-
# @note Please Keep in Mind: Iterating fires a new request for each page
-
#
-
# @yield [Contentful::SyncPage]
-
1
def each_page
-
page = first_page
-
yield page if block_given?
-
-
until completed?
-
page = page.next_page
-
yield page if block_given?
-
end
-
end
-
-
# Returns the first sync result page
-
#
-
# @return [Contentful::SyncPage]
-
1
def first_page
-
get(@first_page_options_or_url)
-
end
-
-
# Returns false as long as last sync page has not been reached
-
#
-
# @return [Boolean]
-
1
def completed?
-
# rubocop:disable Style/DoubleNegation
-
!!next_sync_url
-
# rubocop:enable Style/DoubleNegation
-
end
-
-
# Directly iterates over all resources that have changed
-
#
-
# @yield [Contentful::Entry, Contentful::Asset]
-
1
def each_item(&block)
-
each_page do |page|
-
page.each_item(&block)
-
end
-
end
-
-
# @private
-
1
def get(options_or_url)
-
page = fetch_page(options_or_url)
-
-
return page if @client.configuration[:raw_mode]
-
-
link_page_to_sync! page
-
update_sync_state_from! page
-
-
page
-
end
-
-
1
private
-
-
1
def fetch_page(options_or_url)
-
return Request.new(@client, options_or_url).get if options_or_url.is_a? String
-
Request.new(@client, @client.environment_url('/sync'), options_or_url).get
-
end
-
-
1
def link_page_to_sync!(page)
-
page.instance_variable_set :@sync, self
-
end
-
-
1
def update_sync_state_from!(page)
-
@next_sync_url = page.next_sync_url
-
end
-
end
-
end
-
1
require_relative 'base_resource'
-
1
require_relative 'array_like'
-
-
1
module Contentful
-
# Wrapper Class for Sync results
-
1
class SyncPage < BaseResource
-
1
include Contentful::ArrayLike
-
-
1
attr_reader :sync, :items, :next_sync_url, :next_page_url
-
-
1
def initialize(item,
-
configuration = {
-
default_locale: Contentful::Client::DEFAULT_CONFIGURATION[:default_locale]
-
}, *)
-
super(item, configuration, true)
-
-
@items = item.fetch('items', [])
-
@next_sync_url = item.fetch('nextSyncUrl', nil)
-
@next_page_url = item.fetch('nextPageUrl', nil)
-
end
-
-
# @private
-
1
def inspect
-
"<#{repr_name} next_sync_url='#{next_sync_url}' last_page=#{last_page?}>"
-
end
-
-
# Requests next sync page from API
-
#
-
# @return [Contentful::SyncPage, void]
-
1
def next_page
-
sync.get(next_page_url) if next_page?
-
end
-
-
# Returns wether there is a next sync page
-
#
-
# @return [Boolean]
-
1
def next_page?
-
# rubocop:disable Style/DoubleNegation
-
!!next_page_url
-
# rubocop:enable Style/DoubleNegation
-
end
-
-
# Returns wether it is the last sync page
-
#
-
# @return [Boolean]
-
1
def last_page?
-
!next_page_url
-
end
-
end
-
end
-
1
def create_client(options = {})
-
54
Contentful::Client.new({
-
space: 'cfexampleapi',
-
access_token: 'b4c0n73n7fu1',
-
}.merge(options))
-
end
-
1
require 'multi_json'
-
-
1
def raw_fixture(which, status = 200, _as_json = false, headers = {})
-
object = Object.new
-
allow(object).to receive(:status) { status }
-
allow(object).to receive(:headers) { headers }
-
allow(object).to receive(:to_s) { File.read File.dirname(__FILE__) + "/../fixtures/json_responses/#{which}.json" }
-
allow(object).to receive(:body) { object.to_s }
-
allow(object).to receive(:[]) { |key| object.headers[key] }
-
-
object
-
end
-
-
1
def json_fixture(which, _as_json = false)
-
MultiJson.load(
-
File.read File.dirname(__FILE__) + "/../fixtures/json_responses/#{which}.json"
-
)
-
end
-
1
require 'vcr'
-
-
1
VCR.configure do |c|
-
1
c.cassette_library_dir = 'spec/fixtures/vcr_cassettes'
-
1
c.ignore_localhost = true
-
1
c.hook_into :webmock
-
1
c.default_cassette_options = { record: :once }
-
end
-
-
1
def vcr(name, &block)
-
54
VCR.use_cassette(name, &block)
-
end
-
-
1
def expect_vcr(name, &block)
-
expect { VCR.use_cassette(name, &block) }
-
end