-
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 = '', *)
-
17
super(item, configuration)
-
-
17
@endpoint = endpoint
-
17
@total = item.fetch('total', nil)
-
17
@limit = item.fetch('limit', nil)
-
17
@skip = item.fetch('skip', nil)
-
17
@items = item.fetch('items', [])
-
end
-
-
# @private
-
1
def marshal_dump
-
3
super.merge(endpoint: endpoint)
-
end
-
-
# @private
-
1
def marshal_load(raw_object)
-
3
super
-
3
@endpoint = raw_object[:endpoint]
-
3
@total = raw.fetch('total', nil)
-
3
@limit = raw.fetch('limit', nil)
-
3
@skip = raw.fetch('skip', nil)
-
3
@items = raw.fetch('items', []).map do |item|
-
7
require_relative 'resource_builder'
-
ResourceBuilder.new(
-
item.raw,
-
raw_object[:configuration].merge(includes_for_single: Support.includes_from_response(raw, false)),
-
7
item.respond_to?(:localized) ? item.localized : false
-
7
).run
-
end
-
end
-
-
# @private
-
1
def inspect
-
2
"<#{repr_name} total=#{total} skip=#{skip} limit=#{limit}>"
-
end
-
-
# Simplifies pagination
-
#
-
# @return [Contentful::Array, false]
-
1
def next_page(client = nil)
-
1
return false if client.nil?
-
-
1
new_skip = (skip || 0) + (limit || DEFAULT_LIMIT)
-
1
client.send(endpoint.delete('/'), 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)
-
11
items.each(&block)
-
end
-
1
alias each each_item
-
-
# Delegates to items#empty?
-
#
-
# @return [Boolean]
-
1
def empty?
-
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
-
4
items.last
-
end
-
end
-
end
-
1
require_relative 'fields_resource'
-
1
require_relative 'file'
-
-
1
module Contentful
-
# Resource class for Asset.
-
# https://www.contentful.com/developers/documentation/content-delivery-api/#assets
-
1
class Asset < FieldsResource
-
# @private
-
1
def marshal_dump
-
{
-
configuration: @configuration,
-
raw: raw
-
4
}
-
end
-
-
# @private
-
1
def marshal_load(raw_object)
-
4
super(raw_object)
-
4
create_files!
-
4
define_asset_methods!
-
end
-
-
# @private
-
1
def inspect
-
"<#{repr_name} id='#{sys[:id]}' url='#{url}'>"
-
end
-
-
1
def initialize(*)
-
12
super
-
12
create_files!
-
12
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'
-
# @see _ https://www.contentful.com/developers/documentation/content-delivery-api/#image-asset-resizing
-
#
-
# @return [String] Image API URL
-
1
def image_url(options = {})
-
8
query = {
-
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],
-
fit: options[:fit],
-
fl: options[:fl]
-
56
}.reject { |_k, v| v.nil? }
-
-
8
if query.empty?
-
8
file.url
-
else
-
"#{file.url}?#{URI.encode_www_form(query)}"
-
end
-
end
-
-
1
alias url image_url
-
-
1
private
-
-
1
def create_files!
-
16
file_json = raw.fetch('fields', {}).fetch('file', nil)
-
16
return if file_json.nil?
-
-
32
is_localized = file_json.keys.none? { |f| %w(fileName contentType details url).include? f }
-
16
if is_localized
-
locales.each do |locale|
-
@fields[locale][:file] = ::Contentful::File.new(file_json[locale.to_s] || {})
-
end
-
else
-
16
@fields[internal_resource_locale][:file] = ::Contentful::File.new(file_json)
-
end
-
end
-
-
1
def define_asset_methods!
-
16
define_singleton_method :description do
-
fields.fetch(:description, nil)
-
end
-
-
16
define_singleton_method :file do |wanted_locale = nil|
-
8
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
-
-
1
def initialize(item, configuration = {}, _localized = false, _includes = [], depth = 0)
-
183
@raw = item
-
183
@default_locale = configuration[:default_locale]
-
183
@depth = depth
-
183
@sys = hydrate_sys
-
183
@configuration = configuration
-
-
183
define_sys_methods!
-
end
-
-
# @private
-
1
def inspect
-
2
"<#{repr_name} id='#{sys[:id]}'>"
-
end
-
-
# Definition of equality
-
1
def ==(other)
-
4
self.class == other.class && sys[:id] == other.sys[:id]
-
end
-
-
# @private
-
1
def marshal_dump
-
{
-
configuration: @configuration,
-
raw: raw
-
7
}
-
end
-
-
# @private
-
1
def marshal_load(raw_object)
-
16
@raw = raw_object[:raw]
-
16
@configuration = raw_object[:configuration]
-
16
@default_locale = @configuration[:default_locale]
-
16
@sys = hydrate_sys
-
16
@depth = 0
-
16
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)
-
1
return client.send(Support.snakify(self.class.name.split('::').last), id) unless client.nil?
-
-
1
false
-
end
-
-
1
private
-
-
1
def define_sys_methods!
-
199
@sys.each do |k, v|
-
844
define_singleton_method k do
-
31
v
-
end
-
end
-
end
-
-
1
def hydrate_sys
-
199
result = {}
-
199
raw.fetch('sys', {}).each do |k, v|
-
844
if %w(space contentType).include?(k)
-
95
v = build_link(v)
-
749
elsif %w(createdAt updatedAt deletedAt).include?(k)
-
164
v = DateTime.parse(v)
-
end
-
844
result[Support.snakify(k).to_sym] = v
-
end
-
199
result
-
end
-
-
1
protected
-
-
1
def repr_name
-
4
self.class
-
end
-
-
1
def internal_resource_locale
-
98
sys.fetch(:locale, nil) || default_locale
-
end
-
-
1
def build_link(item)
-
97
require_relative 'link'
-
97
::Contentful::Link.new(item)
-
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,
-
dynamic_entries: :manual,
-
api_url: 'cdn.contentful.com',
-
api_version: 1,
-
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,
-
application_name: nil,
-
application_version: nil,
-
integration_name: nil,
-
integration_version: nil
-
}
-
# Rate Limit Reset Header Key
-
1
RATE_LIMIT_RESET_HEADER_KEY = 'x-contentful-ratelimit-reset'
-
-
1
attr_reader :configuration, :logger, :proxy
-
-
# Wraps the actual HTTP request via proxy
-
# @private
-
1
def self.get_http(url, query, headers = {}, proxy = {})
-
16
if proxy[:host]
-
HTTP[headers].via(proxy[:host], proxy[:port], proxy[:username], proxy[:password]).get(url, params: query)
-
else
-
16
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] :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 [::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 = {})
-
14
@configuration = default_configuration.merge(given_configuration)
-
14
normalize_configuration!
-
14
validate_configuration!
-
14
setup_logger
-
-
14
update_dynamic_entry_cache! if configuration[:dynamic_entries] == :auto
-
end
-
-
# @private
-
1
def setup_logger
-
14
@logger = configuration[:logger]
-
14
logger.level = configuration[:log_level] if logger
-
end
-
-
# @private
-
1
def proxy_params
-
{
-
host: configuration[:proxy_host],
-
port: configuration[:proxy_port],
-
username: configuration[:proxy_username],
-
password: configuration[:proxy_password]
-
16
}
-
end
-
-
# Returns the default configuration
-
# @private
-
1
def default_configuration
-
14
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, '/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 = {})
-
14
Request.new(self, '/content_types', query).get
-
end
-
-
# Gets a specific entry
-
#
-
# @param [String] id
-
# @param [Hash] query
-
#
-
# @return [Contentful::Entry]
-
1
def entry(id, query = {})
-
normalize_select!(query)
-
query['sys.id'] = id
-
entries = Request.new(self, '/entries', query).get
-
-
return entries if configuration[:raw_mode]
-
-
entries.first
-
end
-
-
# Gets a collection of entries
-
#
-
# @param [Hash] query
-
#
-
# @return [Contentful::Array<Contentful::Entry>]
-
1
def entries(query = {})
-
2
normalize_select!(query)
-
2
Request.new(self, '/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, '/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, '/assets', query).get
-
end
-
-
# Returns the base url for all of the client's requests
-
# @private
-
1
def base_url
-
16
"http#{configuration[:secure] ? 's' : ''}://#{configuration[:api_url]}/spaces/#{configuration[:space]}"
-
end
-
-
# Returns the formatted part of the X-Contentful-User-Agent header
-
# @private
-
1
def format_user_agent_header(key, values)
-
48
header = "#{key} #{values[:name]}"
-
48
header = "#{header}/#{values[:version]}" if values[:version]
-
48
"#{header};"
-
end
-
-
# Returns the X-Contentful-User-Agent sdk data
-
# @private
-
1
def sdk_info
-
16
{ name: 'contentful.rb', version: ::Contentful::VERSION }
-
end
-
-
# Returns the X-Contentful-User-Agent app data
-
# @private
-
1
def app_info
-
16
{ name: configuration[:application_name], version: configuration[:application_version] }
-
end
-
-
# Returns the X-Contentful-User-Agent integration data
-
# @private
-
1
def integration_info
-
16
{ name: configuration[:integration_name], version: configuration[:integration_version] }
-
end
-
-
# Returns the X-Contentful-User-Agent platform data
-
# @private
-
1
def platform_info
-
16
{ name: 'ruby', version: RUBY_VERSION }
-
end
-
-
# Returns the X-Contentful-User-Agent os data
-
# @private
-
1
def os_info
-
16
os_name = case ::RbConfig::CONFIG['host_os']
-
when /(cygwin|mingw|mswin|windows)/i then 'Windows'
-
16
when /(darwin|macruby|mac os)/i then 'macOS'
-
when /(linux|bsd|aix|solarix)/i then 'Linux'
-
end
-
16
{ name: os_name, version: Gem::Platform.local.version }
-
end
-
-
# Returns the X-Contentful-User-Agent
-
# @private
-
1
def contentful_user_agent
-
16
header = {
-
'sdk' => sdk_info,
-
'app' => app_info,
-
'integration' => integration_info,
-
'platform' => platform_info,
-
'os' => os_info
-
}
-
-
16
result = []
-
16
header.each do |key, values|
-
80
next unless values[:name]
-
48
result << format_user_agent_header(key, values)
-
end
-
16
result.join(' ')
-
end
-
-
# Returns the headers used for the HTTP requests
-
# @private
-
1
def request_headers
-
16
headers = { 'X-Contentful-User-Agent' => contentful_user_agent }
-
16
headers['Authorization'] = "Bearer #{configuration[:access_token]}" if configuration[:authentication_mechanism] == :header
-
16
headers['Content-Type'] = "application/vnd.contentful.delivery.v#{configuration[:api_version].to_i}+json" if configuration[:api_version]
-
16
headers['Accept-Encoding'] = 'gzip' if configuration[:gzip_encoded]
-
16
headers
-
end
-
-
# Patches a query hash with the client configurations for queries
-
# @private
-
1
def request_query(query)
-
16
if configuration[:authentication_mechanism] == :query_string
-
query['access_token'] = configuration[:access_token]
-
end
-
16
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)
-
16
retries_left = configuration[:max_rate_limit_retries]
-
16
result = nil
-
16
begin
-
16
response = run_request(request)
-
-
16
return response if !build_resource || configuration[:raw_mode]
-
-
16
return fail_response(response) if response.status != :ok
-
-
16
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.response.raw[RATE_LIMIT_RESET_HEADER_KEY].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
-
-
16
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)
-
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)
-
16
url = request.absolute? ? request.url : base_url + request.url
-
16
logger.info(request: { url: url, query: request.query, header: request_headers }) if logger
-
16
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)
-
16
logger.debug(response: response) if logger
-
configuration[:resource_builder].new(
-
response.object,
-
configuration,
-
16
(response.request.query || {}).fetch(:locale, nil) == '*',
-
0,
-
response.request.endpoint
-
16
).run
-
end
-
-
# Use this method together with the client's :dynamic_entries configuration.
-
# See README for details.
-
# @private
-
1
def update_dynamic_entry_cache!
-
2
content_types(limit: 1000).map do |ct|
-
4
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)
-
2
return unless query.key?(:select)
-
-
query[:select] = query[:select].split(',').map(&:strip) if query[:select].is_a? String
-
query[:select] = query[:select].reject { |p| p.start_with?('sys.') }
-
query[:select] << 'sys' unless query[:select].include?('sys')
-
end
-
-
1
def normalize_configuration!
-
70
[:space, :access_token, :api_url, :default_locale].each { |s| configuration[s] = configuration[s].to_s }
-
14
configuration[:authentication_mechanism] = configuration[:authentication_mechanism].to_sym
-
end
-
-
1
def validate_configuration!
-
14
fail ArgumentError, 'You will need to initialize a client with a :space' if configuration[:space].empty?
-
14
fail ArgumentError, 'You will need to initialize a client with an :access_token' if configuration[:access_token].empty?
-
14
fail ArgumentError, 'The client configuration needs to contain an :api_url' if configuration[:api_url].empty?
-
14
fail ArgumentError, 'The client configuration needs to contain a :default_locale' if configuration[:default_locale].empty?
-
14
fail ArgumentError, 'The :api_version must be a positive number or nil' unless configuration[:api_version].to_i >= 0
-
14
fail ArgumentError, 'The authentication mechanism must be :header or :query_string' unless [:header, :query_string].include?(
-
configuration[:authentication_mechanism]
-
)
-
14
fail ArgumentError, 'The :dynamic_entries mode must be :auto or :manual' unless [:auto, :manual].include?(
-
configuration[:dynamic_entries]
-
)
-
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, *)
-
49
super
-
-
49
@name = item.fetch('name', nil)
-
49
@description = item.fetch('description', nil)
-
244
@fields = item.fetch('fields', []).map { |field| Field.new(field) }
-
49
@display_field = item.fetch('displayField', nil)
-
end
-
-
# Field definition for field
-
1
def field_for(field_id)
-
19
fields.detect { |f| Support.snakify(f.id) == Support.snakify(field_id) }
-
end
-
-
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)
-
8
@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)
-
4
@cache[space_id] ||= {}
-
4
@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 'fields_resource'
-
1
require_relative 'content_type_cache'
-
-
1
module Contentful
-
# Resource class for Entry.
-
# @see _ https://www.contentful.com/developers/documentation/content-delivery-api/#entries
-
1
class Entry < FieldsResource
-
# Returns true for resources that are entries
-
1
def entry?
-
true
-
end
-
-
1
private
-
-
1
def coerce(field_id, value, includes)
-
23
return build_nested_resource(value, includes) if Support.link?(value)
-
9
return coerce_link_array(value, includes) if Support.link_array?(value)
-
-
8
content_type = ContentTypeCache.cache_get(sys[:space].id, sys[:content_type].id)
-
-
8
unless content_type.nil?
-
8
content_type_field = content_type.field_for(field_id)
-
8
return content_type_field.coerce(value) unless content_type_field.nil?
-
end
-
-
super(field_id, value, includes)
-
end
-
-
1
def coerce_link_array(value, includes)
-
1
items = []
-
1
value.each do |link|
-
2
items << build_nested_resource(link, includes)
-
end
-
-
1
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)
-
16
if @depth < @configuration.fetch(:max_include_resolution_depth, 20)
-
16
resource = Support.resource_for_link(value, includes)
-
16
return resolve_include(resource, includes) unless resource.nil?
-
end
-
-
2
build_link(value)
-
end
-
-
1
def resolve_include(resource, includes)
-
14
require_relative 'resource_builder'
-
-
ResourceBuilder.new(
-
resource,
-
@configuration.merge(
-
includes_for_single:
-
@configuration.fetch(:includes_for_single, []) + includes
-
),
-
localized,
-
@depth + 1,
-
includes
-
14
).run
-
end
-
-
1
def known_link?(name)
-
9
field_name = name.to_sym
-
9
return true if known_contentful_object?(fields[field_name])
-
2
fields[field_name].is_a?(Enumerable) && fields[field_name].any? { |object| known_contentful_object?(object) }
-
end
-
-
1
def known_contentful_object?(object)
-
10
(object.is_a?(Contentful::Entry) || object.is_a?(Contentful::Asset))
-
end
-
-
1
protected
-
-
1
def repr_name
-
2
"#{super}[#{sys[:content_type].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)
-
@response = response
-
super @response.error_message
-
end
-
-
# Shortcut for creating specialized error classes
-
# USAGE rescue Contentful::Error[404]
-
1
def self.[](error_status_code)
-
case error_status_code
-
when 404
-
NotFound
-
when 400
-
BadRequest
-
when 403
-
AccessDenied
-
when 401
-
Unauthorized
-
when 429
-
RateLimitExceeded
-
when 500
-
ServerError
-
when 503
-
ServiceUnavailable
-
else
-
Error
-
end
-
end
-
end
-
-
# 404
-
1
class NotFound < Error; end
-
-
# 400
-
1
class BadRequest < Error; end
-
-
# 403
-
1
class AccessDenied < Error; end
-
-
# 401
-
1
class Unauthorized < Error; end
-
-
# 429
-
1
class RateLimitExceeded < Error; end
-
-
# 500
-
1
class ServerError < Error; end
-
-
# 503
-
1
class ServiceUnavailable < Error; end
-
-
# Raised when response is no valid json
-
1
class UnparsableJson < Error; end
-
-
# Raised when response is not parsable as a Contentful::Resource
-
1
class UnparsableResource < StandardError; 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
-
}
-
-
1
attr_reader :raw, :id, :name, :type, :link_type, :items, :required, :localized
-
-
1
def initialize(json)
-
230
@raw = json
-
230
@id = json.fetch('id', nil)
-
230
@name = json.fetch('name', nil)
-
230
@type = json.fetch('type', nil)
-
230
@link_type = json.fetch('linkType', nil)
-
230
@items = json.key?('items') ? Field.new(json.fetch('items', {})) : nil
-
230
@required = json.fetch('required', false)
-
230
@localized = json.fetch('localized', false)
-
end
-
-
# Coerces value to proper type
-
1
def coerce(value)
-
8
return value if type.nil?
-
-
8
options = {}
-
8
options[:coercion_class] = KNOWN_TYPES[items.type] unless items.nil?
-
8
KNOWN_TYPES[type].new(value, options).coerce
-
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
-
-
1
def initialize(item, _configuration, localized = false, includes = [], *)
-
20
super
-
-
20
@localized = localized
-
20
@fields = hydrate_fields(includes)
-
-
20
define_fields_methods!
-
end
-
-
# Returns all fields of the asset
-
#
-
# @return [Hash] fields for Resource on selected locale
-
1
def fields(wanted_locale = nil)
-
53
wanted_locale = internal_resource_locale if wanted_locale.nil?
-
53
@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
-
remapped_fields = {}
-
locales.each do |locale|
-
fields(locale).each do |name, value|
-
remapped_fields[name] ||= {}
-
remapped_fields[name][locale.to_sym] = value
-
end
-
end
-
-
remapped_fields
-
end
-
-
# Provides a list of the available locales for a Resource
-
1
def locales
-
@fields.keys
-
end
-
-
# @private
-
1
def marshal_dump
-
{
-
configuration: @configuration,
-
raw: raw_with_links,
-
localized: localized
-
5
}
-
end
-
-
# @private
-
1
def marshal_load(raw_object)
-
9
super(raw_object)
-
9
@localized = raw_object[:localized]
-
9
@fields = hydrate_fields(raw_object[:configuration].fetch(:includes_for_single, []))
-
9
define_fields_methods!
-
end
-
-
# @private
-
1
def raw_with_links
-
14
links = fields.keys.select { |property| known_link?(property) }
-
5
processed_raw = raw.clone
-
5
raw['fields'].each do |k, v|
-
9
processed_raw['fields'][k] = links.include?(Support.snakify(k).to_sym) ? send(Support.snakify(k)) : v
-
end
-
-
5
processed_raw
-
end
-
-
1
private
-
-
1
def define_fields_methods!
-
29
fields.each do |k, v|
-
55
define_singleton_method k do
-
17
v
-
end
-
end
-
end
-
-
1
def hydrate_fields(includes)
-
29
return {} unless raw.key?('fields')
-
-
29
locale = internal_resource_locale
-
29
result = { locale => {} }
-
-
29
if localized
-
raw['fields'].each do |name, locales|
-
locales.each do |loc, value|
-
result[loc] ||= {}
-
result[loc][Support.snakify(name).to_sym] = coerce(
-
Support.snakify(name),
-
value,
-
includes
-
)
-
end
-
end
-
else
-
29
raw['fields'].each do |name, value|
-
55
result[locale][Support.snakify(name).to_sym] = coerce(
-
Support.snakify(name),
-
value,
-
includes
-
)
-
end
-
end
-
-
29
result
-
end
-
-
1
protected
-
-
1
def coerce(_field_id, value, _includes)
-
32
value
-
end
-
end
-
end
-
1
module Contentful
-
# An Assets's file info
-
1
class File
-
1
attr_reader :file_name, :content_type, :details, :url
-
1
def initialize(json)
-
16
@file_name = json.fetch('fileName', nil)
-
16
@content_type = json.fetch('contentType', nil)
-
16
@details = json.fetch('details', nil)
-
16
@url = json.fetch('url', nil)
-
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
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
-
1
attr_reader :code, :name, :default
-
-
1
def initialize(json)
-
@code = json.fetch('code', nil)
-
@name = json.fetch('name', nil)
-
@default = json.fetch('default', false)
-
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)
-
16
@client = client
-
16
@endpoint = endpoint
-
-
16
@query = (normalize_query(query) if query && !query.empty?)
-
-
16
if id
-
@type = :single
-
@id = URI.escape(id)
-
else
-
16
@type = :multi
-
16
@id = nil
-
end
-
end
-
-
# Returns the final URL, relative to a contentful space
-
1
def url
-
16
"#{@endpoint}#{@type == :single ? "/#{id}" : ''}"
-
end
-
-
# Delegates the actual HTTP work to the client
-
1
def get
-
16
client.get(self)
-
end
-
-
# Returns true if endpoint is an absolute url
-
1
def absolute?
-
16
@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)
-
6
Hash[
-
query.map do |key, value|
-
[
-
8
key.to_sym,
-
8
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
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
-
}
-
# 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, endpoint = nil)
-
37
@json = json
-
37
@default_locale = configuration.fetch(:default_locale, ::Contentful::Client::DEFAULT_CONFIGURATION[:default_locale])
-
37
@resource_mapping = default_resource_mapping.merge(configuration.fetch(:resource_mapping, {}))
-
37
@entry_mapping = default_entry_mapping.merge(configuration.fetch(:entry_mapping, {}))
-
37
@includes_for_single = configuration.fetch(:includes_for_single, [])
-
37
@localized = localized
-
37
@depth = depth
-
37
@endpoint = endpoint
-
37
@configuration = configuration
-
end
-
-
# Starts the parsing process.
-
#
-
# @return [Contentful::Resource, Contentful::Error]
-
1
def run
-
37
return build_array if array?
-
21
build_single
-
rescue UnparsableResource => error
-
error
-
end
-
-
1
private
-
-
1
def build_array
-
16
includes = fetch_includes
-
16
result = json['items'].map do |item|
-
48
build_item(item, includes)
-
end
-
16
array_class = fetch_array_class
-
16
array_class.new(json.dup.merge('items' => result), @configuration, endpoint)
-
end
-
-
1
def build_single
-
21
includes = @includes_for_single
-
21
build_item(json, includes)
-
end
-
-
1
def build_item(item, includes = [])
-
69
buildables = %w(Entry Asset ContentType Space DeletedEntry DeletedAsset)
-
248
item_type = buildables.detect { |b| b.to_s == item['sys']['type'] }
-
69
fail UnparsableResource, 'Item type is not known, could not parse' if item_type.nil?
-
69
item_class = resource_class(item)
-
-
69
item_class.new(item, @configuration, localized?, includes, depth)
-
end
-
-
1
def fetch_includes
-
16
Support.includes_from_response(json)
-
end
-
-
1
def resource_class(item)
-
69
return fetch_custom_resource_class(item) if %w(Entry DeletedEntry Asset DeletedAsset).include?(item['sys']['type'])
-
49
resource_mapping[item['sys']['type']]
-
end
-
-
1
def fetch_custom_resource_class(item)
-
20
case item['sys']['type']
-
when 'Entry'
-
8
resource_class = entry_mapping[item['sys']['contentType']['sys']['id']]
-
8
return resource_class unless resource_class.nil?
-
-
6
return fetch_custom_resource_mapping(item, 'Entry', Entry)
-
when 'Asset'
-
12
return fetch_custom_resource_mapping(item, 'Asset', Asset)
-
when 'DeletedEntry'
-
return fetch_custom_resource_mapping(item, 'DeletedEntry', DeletedEntry)
-
when 'DeletedAsset'
-
return fetch_custom_resource_mapping(item, 'DeletedAsset', DeletedAsset)
-
end
-
end
-
-
1
def fetch_custom_resource_mapping(item, type, default_class)
-
18
resources = resource_mapping[type]
-
18
return default_class if resources.nil?
-
-
18
return resources if resources.is_a?(Class)
-
return resources[item] if resources.respond_to?(:call)
-
-
default_class
-
end
-
-
1
def fetch_array_class
-
16
return SyncPage if sync?
-
16
::Contentful::Array
-
end
-
-
1
def localized?
-
69
return true if @localized
-
69
return true if array? && sync?
-
69
false
-
end
-
-
1
def array?
-
106
json.fetch('sys', {}).fetch('type', '') == 'Array'
-
end
-
-
1
def sync?
-
64
json.fetch('nextSyncUrl', nil) || json.fetch('nextPageUrl', nil)
-
end
-
-
# The default mapping for #detect_resource_class
-
1
def default_resource_mapping
-
37
DEFAULT_RESOURCE_MAPPING.dup
-
end
-
-
# The default entry mapping
-
1
def default_entry_mapping
-
37
DEFAULT_ENTRY_MAPPING.dup
-
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
-
#
-
# @return [String] snake_case_name
-
1
def snakify(object)
-
String(object)
-
.gsub(/::/, '/')
-
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
-
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
-
.tr('-', '_')
-
994
.downcase
-
end
-
-
# Checks if value is a link
-
#
-
# @param value
-
#
-
# @return [true, false]
-
1
def link?(value)
-
value.is_a?(::Hash) &&
-
26
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)
-
9
return link?(value[0]) if value.is_a?(::Array) && !value.empty?
-
-
6
false
-
end
-
-
# Returns the resource that matches the link
-
#
-
# @param [Hash] link
-
# @param [::Array] includes
-
#
-
# @return [Hash]
-
1
def resource_for_link(link, includes)
-
16
includes.detect do |i|
-
i['sys']['id'] == link['sys']['id'] &&
-
63
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)
-
23
includes = if raw
-
16
json['items'].dup
-
else
-
7
json['items'].map(&:raw)
-
end
-
-
23
%w(Entry Asset).each do |type|
-
46
if json.fetch('includes', {}).key?(type)
-
7
includes.concat(json['includes'].fetch(type, []))
-
end
-
end
-
-
23
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, '/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 = {})
-
14
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)
-
object = Object.new
-
allow(object).to receive(:status) { status }
-
allow(object).to receive(:headers) { {} }
-
allow(object).to receive(:to_s) { File.read File.dirname(__FILE__) + "/../fixtures/json_responses/#{which}.json" }
-
-
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)
-
14
VCR.use_cassette(name, &block)
-
end
-
-
1
def expect_vcr(name, &block)
-
expect { VCR.use_cassette(name, &block) }
-
end