module WCC::Contentful::ModelMethods

This module is included by all {WCC::Contentful::Model models} and defines instance methods that are not dynamically generated.

@api Model

Constants

MODEL_LAYER_CONTEXT_KEYS

The set of options keys that are specific to the Model layer and shouldn't be passed down to the Store layer.

Public Instance Methods

resolve(depth: 1, fields: nil, context: sys.context.to_h, **options) click to toggle source

Resolves all links in an entry to the specified depth.

Each link in the entry is recursively retrieved from the store until the given depth is satisfied. Depth resolution is unlimited, circular references will be resolved to the same object.

@param [Fixnum] depth how far to recursively resolve. Must be >= 1 @param [Array<String, Symbol>] fields (optional) A subset of fields whose

links should be resolved.  Defaults to all fields.

@param [Hash] context passed to the resolved model's `new` function to provide

contextual information ex. current locale.
See {WCC::Contentful::ModelSingletonMethods#find Model#find}, {WCC::Contentful::Sys#context}

@param [Hash] options The remaining optional parameters, defined below @option options [Symbol] circular_reference Determines how circular references are

handled.  `:raise` causes a {WCC::Contentful::CircularReferenceError} to be raised,
`:ignore` will cause the field to remain unresolved, and any other value (or nil)
will cause the field to point to the previously resolved ruby object for that ID.
# File lib/wcc/contentful/model_methods.rb, line 34
def resolve(depth: 1, fields: nil, context: sys.context.to_h, **options)
  raise ArgumentError, "Depth must be > 0 (was #{depth})" unless depth && depth > 0
  return self if resolved?(depth: depth, fields: fields)

  fields = fields.map { |f| f.to_s.camelize(:lower) } if fields.present?
  fields ||= self.class::FIELDS

  typedef = self.class.content_type_definition
  links = fields.select { |f| %i[Asset Link].include?(typedef.fields[f].type) }

  raw_link_ids =
    links.map { |field_name| raw.dig('fields', field_name, sys.locale) }
      .flat_map do |raw_value|
        _try_map(raw_value) { |v| v.dig('sys', 'id') if v.dig('sys', 'type') == 'Link' }
      end
  raw_link_ids = raw_link_ids.compact
  backlinked_ids = (context[:backlinks]&.map { |m| m.id } || [])

  has_unresolved_raw_links = (raw_link_ids - backlinked_ids).any?
  if has_unresolved_raw_links
    raw =
      _instrument 'resolve', id: id, depth: depth, backlinks: backlinked_ids do
        # use include param to do resolution
        self.class.store(context[:preview])
          .find_by(content_type: self.class.content_type,
                   filter: { 'sys.id' => id },
                   options: context.except(*MODEL_LAYER_CONTEXT_KEYS).merge!({
                     include: [depth, 10].min
                   }))
      end
    unless raw
      raise WCC::Contentful::ResolveError, "Cannot find #{self.class.content_type} with ID #{id}"
    end

    @raw = raw.freeze
    links.each { |f| instance_variable_set('@' + f, raw.dig('fields', f, sys.locale)) }
  end

  links.each { |f| _resolve_field(f, depth, context, options) }
  self
end
resolved?(depth: 1, fields: nil) click to toggle source

Determines whether the object has been resolved up to the prescribed depth.

# File lib/wcc/contentful/model_methods.rb, line 77
def resolved?(depth: 1, fields: nil)
  raise ArgumentError, "Depth must be > 0 (was #{depth})" unless depth && depth > 0

  fields = fields.map { |f| f.to_s.camelize(:lower) } if fields.present?
  fields ||= self.class::FIELDS

  typedef = self.class.content_type_definition
  links = fields.select { |f| %i[Asset Link].include?(typedef.fields[f].type) }
  links.all? { |f| _resolved_field?(f, depth) }
end
to_h(stack = nil) click to toggle source

Turns the current model into a hash representation as though it had been retrieved from the Contentful API.

This differs from `#raw` in that it recursively includes the `#to_h` of resolved links. It also sets the fields to the value for the entry's `#sys.locale`, as though the entry had been retrieved from the API with `locale={#sys.locale}` rather than `locale=*`.

# File lib/wcc/contentful/model_methods.rb, line 95
def to_h(stack = nil)
  raise WCC::Contentful::CircularReferenceError.new(stack, id) if stack&.include?(id)

  stack = [*stack, id]
  typedef = self.class.content_type_definition
  fields =
    typedef.fields.each_with_object({}) do |(name, field_def), h|
      if field_def.type == :Link || field_def.type == :Asset
        if _resolved_field?(name, 0)
          val = public_send(name)
          val =
            _try_map(val) { |v| v.to_h(stack) }
        else
          ids = field_def.array ? public_send("#{name}_ids") : public_send("#{name}_id")
          val =
            _try_map(ids) do |id|
              {
                'sys' => {
                  'type' => 'Link',
                  'linkType' => field_def.type == :Asset ? 'Asset' : 'Entry',
                  'id' => id
                }
              }
            end
        end
      else
        val = public_send(name)
        val = _try_map(val) { |v| v.respond_to?(:to_h) ? v.to_h.stringify_keys! : v }
      end

      h[name] = val
    end

  {
    'sys' => { 'locale' => @sys.locale }.merge!(@raw['sys']),
    'fields' => fields
  }
end

Protected Instance Methods

_instrumentation_event_prefix() click to toggle source
# File lib/wcc/contentful/model_methods.rb, line 138
def _instrumentation_event_prefix
  '.model.contentful.wcc'
end

Private Instance Methods

_resolve_field(field_name, depth = 1, context = {}, options = {}) click to toggle source
# File lib/wcc/contentful/model_methods.rb, line 144
def _resolve_field(field_name, depth = 1, context = {}, options = {})
  return if depth <= 0

  var_name = '@' + field_name
  return unless val = instance_variable_get(var_name)

  context = sys.context.to_h.merge(context)
  # load a single link from a raw link or entry, by either finding it via the API
  # or instantiating it directly from a raw entry
  load =
    ->(raw) {
      id = raw.dig('sys', 'id')
      already_resolved = context[:backlinks]&.find { |m| m.id == id }

      new_context = context.merge({ backlinks: [self, *context[:backlinks]].freeze })

      if already_resolved && %i[ignore raise].include?(options[:circular_reference])
        raise WCC::Contentful::CircularReferenceError.new(
          new_context[:backlinks].map(&:id).reverse,
          id
        )
      end

      # Use the already resolved circular reference, or resolve a link, or
      # instantiate from already resolved raw entry data.
      m = already_resolved ||
        if raw.dig('sys', 'type') == 'Link'
          _instrument 'resolve',
            id: self.id, depth: depth, backlinks: context[:backlinks]&.map(&:id) do
            WCC::Contentful::Model.find(id, options: new_context)
          end
        else
          WCC::Contentful::Model.new_from_raw(raw, new_context)
        end

      m.resolve(depth: depth - 1, context: new_context, **options) if m && depth > 1
      m
    }

  begin
    val = _try_map(val) { |v| load.call(v) }
    val = val.compact if val.is_a? Array

    instance_variable_set(var_name + '_resolved', val)
  rescue WCC::Contentful::CircularReferenceError
    raise unless options[:circular_reference] == :ignore
  end
end
_resolved_field?(field_name, depth = 1) click to toggle source
# File lib/wcc/contentful/model_methods.rb, line 193
def _resolved_field?(field_name, depth = 1)
  var_name = '@' + field_name
  raw = instance_variable_get(var_name)
  return true if raw.nil? || (raw.is_a?(Array) && raw.all?(&:nil?))
  return false unless val = instance_variable_get(var_name + '_resolved')
  return true if depth <= 1

  return val.resolved?(depth: depth - 1) unless val.is_a? Array

  val.all? { |i| i.nil? || i.resolved?(depth: depth - 1) }
end
_try_map(val) { |item| ... } click to toggle source
# File lib/wcc/contentful/model_methods.rb, line 205
def _try_map(val)
  if val.is_a? Array
    return val&.map do |item|
      yield item unless item.nil?
    end
  end

  yield val unless val.nil?
end