lib/wcc/contentful/store/base.rb in wcc-contentful-0.4.0.pre.rc vs lib/wcc/contentful/store/base.rb in wcc-contentful-1.0.0.pre.rc1

- old
+ new

@@ -1,22 +1,23 @@ # frozen_string_literal: true +require_relative './interface' + # @api Store module WCC::Contentful::Store # This is the base class for stores which implement #index, and therefore # must be kept up-to-date via the Sync API. - # @abstract At a minimum subclasses should override {#find}, {#find_all}, {#set}, + # @abstract At a minimum subclasses should override {#find}, {#execute}, {#set}, # and #{delete}. As an alternative to overriding set and delete, the subclass # can override {#index}. Index is called when a webhook triggers a sync, to # update the store. + # + # To implement a new store, you should include the rspec_examples in your rspec + # tests for the store. See spec/wcc/contentful/store/memory_store_spec.rb for + # an example. class Base - # Finds an entry by it's ID. The returned entry is a JSON hash - # @abstract Subclasses should implement this at a minimum to provide data - # to the WCC::Contentful::Model API. - def find(_id) - raise NotImplementedError, "#{self.class} does not implement #find" - end + include WCC::Contentful::Store::Interface # Sets the value of the entry with the given ID in the store. # @abstract def set(_id, _value) raise NotImplementedError, "#{self.class} does not implement #set" @@ -26,10 +27,26 @@ # @abstract def delete(_id) raise NotImplementedError, "#{self.class} does not implement #delete" end + # Executes a WCC::Contentful::Store::Query object created by {#find_all} or + # {#find_by}. Implementations should override this to translate the query's + # conditions into a query against the datastore. + # + # For a very naiive implementation see WCC::Contentful::Store::MemoryStore#execute + # @abstract + def execute(_query) + raise NotImplementedError, "#{self.class} does not implement #execute" + end + + # Returns true if this store can persist entries and assets which are + # retrieved from the sync API. + def index? + true + end + # Processes a data point received via the Sync API. This can be a published # entry or asset, or a 'DeletedEntry' or 'DeletedAsset'. The default # implementation calls into #set and #delete to perform the appropriate # operations in the store. def index(json) @@ -74,136 +91,34 @@ q.first end # Finds all entries of the given content type. A content type is required. # - # @abstract Subclasses should implement this at a minimum to provide data - # to the {WCC::Contentful::Model} API. + # Subclasses may override this to provide their own query implementation, + # or else override #execute to run the query after it has been parsed. # @param [String] content_type The ID of the content type to search for. # @param [Hash] options An optional set of additional parameters to the query # defining for example include depth. Not all store implementations respect all options. # @return [Query] A query object that exposes methods to apply filters - # rubocop:disable Lint/UnusedMethodArgument def find_all(content_type:, options: nil) - raise NotImplementedError, "#{self.class} does not implement find_all" + Query.new( + self, + content_type: content_type, + options: options + ) end - # rubocop:enable Lint/UnusedMethodArgument def initialize @mutex = Concurrent::ReentrantReadWriteLock.new end def ensure_hash(val) raise ArgumentError, 'Value must be a Hash' unless val.is_a?(Hash) end - protected + private attr_reader :mutex - - # The base class for query objects returned by find_all. Subclasses should - # override the #result method to return an array-like containing the query - # results. - class Query - delegate :first, to: :result - delegate :map, to: :result - delegate :count, to: :result - - OPERATORS = %i[ - eq - ne - all - in - nin - exists - lt - lte - gt - gte - query - match - ].freeze - - # @abstract Subclasses should provide this in order to fetch the results - # of the query. - def result - raise NotImplementedError - end - - def initialize(store) - @store = store - end - - # @abstract Subclasses can either override this method to properly respond - # to find_by query objects, or they can define a method for each supported - # operator. Ex. `#eq`, `#ne`, `#gt`. - def apply_operator(operator, field, expected, context = nil) - respond_to?(operator) || - raise(ArgumentError, "Operator not implemented: #{operator}") - - public_send(operator, field, expected, context) - end - - # Called with a filter object by {Base#find_by} in order to apply the filter. - def apply(filter, context = nil) - filter.reduce(self) do |query, (field, value)| - if value.is_a?(Hash) - if op?(k = value.keys.first) - query.apply_operator(k.to_sym, field.to_s, value[k], context) - else - query.nested_conditions(field, value, context) - end - else - query.apply_operator(:eq, field.to_s, value) - end - end - end - - protected - - # naive implementation recursively descends the graph to turns links into - # the actual entry data. This calls {Base#find} for each link and so it is - # very inefficient. - # - # @abstract Override this to provide a more efficient implementation for - # a given store. - def resolve_includes(entry, depth) - return entry unless entry && depth && depth > 0 && fields = entry['fields'] - - fields.each do |(_name, locales)| - # TODO: handle non-* locale - locales.each do |(locale, val)| - locales[locale] = - if val.is_a? Array - val.map { |e| resolve_link(e, depth) } - else - resolve_link(val, depth) - end - end - end - - entry - end - - def resolve_link(val, depth) - return val unless val.is_a?(Hash) && val.dig('sys', 'type') == 'Link' - return val unless included = @store.find(val.dig('sys', 'id')) - - resolve_includes(included, depth - 1) - end - - private - - def op?(key) - OPERATORS.include?(key.to_sym) - end - - def sys?(field) - field.to_s =~ /sys\./ - end - - def id?(field) - field.to_sym == :id - end - end end end + +require_relative './query'