Skip to content

Commit

Permalink
Merge 38db77f into 4170b56
Browse files Browse the repository at this point in the history
  • Loading branch information
gburgett committed Jan 29, 2020
2 parents 4170b56 + 38db77f commit 3a73424
Show file tree
Hide file tree
Showing 17 changed files with 1,091 additions and 569 deletions.
3 changes: 3 additions & 0 deletions .rubocop.yml
Expand Up @@ -76,6 +76,9 @@ Lint/AmbiguousBlockAssociation:
Exclude:
- '*/spec/generators/**/*.rb'

Lint/UnneededCopDisableDirective:
Enabled: false

Style/EmptyMethod:
EnforcedStyle: expanded

Expand Down
5 changes: 5 additions & 0 deletions .rubocop_todo.yml
Expand Up @@ -12,6 +12,11 @@ Layout/ClosingParenthesisIndentation:
Exclude:
- '*/lib/wcc/contentful/store/postgres_store.rb'

# Cop blows up on this file
Layout/IndentFirstParameter:
Exclude:
- '*/lib/wcc/contentful/store/postgres_store.rb'

# Offense count: 239
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
Expand Down
6 changes: 6 additions & 0 deletions wcc-contentful/lib/wcc/contentful.rb
Expand Up @@ -35,6 +35,12 @@ class << self
attr_reader :configuration

attr_reader :types

# Gets all queryable locales.
# Reserved for future use.
def locales
@locales ||= { 'en-US' => {} }.freeze
end
end

# Configures the WCC::Contentful gem to talk to a Contentful space.
Expand Down
55 changes: 49 additions & 6 deletions wcc-contentful/lib/wcc/contentful/middleware/store.rb
Expand Up @@ -44,8 +44,8 @@ def find_by(options: nil, **args)
def find_all(options: nil, **args)
Query.new(
store.find_all(**args.merge(options: options)),
self,
options
middleware: self,
options: options
)
end

Expand Down Expand Up @@ -85,10 +85,22 @@ def transform(entry)
entry
end

class Query < WCC::Contentful::Store::Base::Query
attr_reader :wrapped_query, :middleware, :options
class Query
delegate :first,
:map,
:flat_map,
:count,
:select,
:reject,
:take,
:take_while,
:drop,
:drop_while,
:zip,
:to_a,
to: :to_enum

delegate :apply, :apply_operator, to: :wrapped_query
attr_reader :wrapped_query, :middleware, :options

def to_enum
result =
Expand All @@ -102,10 +114,41 @@ def to_enum
result.map { |x| middleware.transform(x) }
end

def initialize(wrapped_query, middleware, options)
def apply(filter, context = nil)
self.class.new(
wrapped_query.apply(filter, context),
middleware: middleware,
options: options,
**@extra
)
end

def apply_operator(operator, field, expected, context = nil)
self.class.new(
wrapped_query.apply_operator(operator, field, expected, context),
middleware: middleware,
options: options,
**@extra
)
end

WCC::Contentful::Store::Query::OPERATORS.each do |op|
# @see #apply_operator
define_method(op) do |field, expected, context = nil|
self.class.new(
wrapped_query.public_send(op, field, expected, context),
middleware: middleware,
options: options,
**@extra
)
end
end

def initialize(wrapped_query, middleware:, options: nil, **extra)
@wrapped_query = wrapped_query
@middleware = middleware
@options = options
@extra = extra
end
end
end
139 changes: 26 additions & 113 deletions wcc-contentful/lib/wcc/contentful/store/base.rb
Expand Up @@ -4,10 +4,14 @@
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
Expand All @@ -28,7 +32,18 @@ def delete(_id)
raise NotImplementedError, "#{self.class} does not implement #delete"
end

# Returns true if this store can index values coming back from the sync API.
# 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
Expand Down Expand Up @@ -81,17 +96,19 @@ def find_by(content_type:, filter: nil, options: nil)

# 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
Expand All @@ -104,111 +121,7 @@ def ensure_hash(val)
protected

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,
:map,
:flat_map,
:count,
:select,
:reject,
:take,
:take_while,
:drop,
:drop_while,
:zip,
:to_a,
to: :to_enum

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 to_enum
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

WCC::Contentful::LinkVisitor.new(entry, :Link, :Asset, depth: depth).map! do |val|
resolve_link(val)
end
end

def resolve_link(val)
return val unless val.is_a?(Hash) && val.dig('sys', 'type') == 'Link'
return val unless included = @store.find(val.dig('sys', 'id'))

included
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'
50 changes: 44 additions & 6 deletions wcc-contentful/lib/wcc/contentful/store/cdn_adapter.rb
Expand Up @@ -44,39 +44,57 @@ def find_by(content_type:, filter: nil, options: nil)

def find_all(content_type:, options: nil)
Query.new(
store: self,
self,
client: @client,
relation: { content_type: content_type },
options: options
)
end

class Query < Base::Query
class Query
include Enumerable

delegate :count, to: :response
delegate :each, to: :to_enum

def to_enum
return response.items unless @options[:include]

response.items.map { |e| resolve_includes(e, @options[:include]) }
end

def initialize(store:, client:, relation:, options: nil, **extra)
def initialize(store, client:, relation:, options: nil, **extra)
raise ArgumentError, 'Client cannot be nil' unless client.present?
raise ArgumentError, 'content_type must be provided' unless relation[:content_type].present?

super(store)
@store = store
@client = client
@relation = relation
@options = options || {}
@extra = extra || {}
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

def apply_operator(operator, field, expected, context = nil)
op = operator == :eq ? nil : operator
param = parameter(field, operator: op, context: context, locale: true)

self.class.new(
store: @store,
@store,
client: @client,
relation: @relation.merge(param => expected),
options: @options,
Expand All @@ -92,14 +110,26 @@ def nested_conditions(field, conditions, context)
end
end

Base::Query::OPERATORS.each do |op|
WCC::Contentful::Store::Query::OPERATORS.each do |op|
define_method(op) do |field, expected, context = nil|
apply_operator(op, field, expected, context)
end
end

private

def op?(key)
WCC::Contentful::Store::Query::OPERATORS.include?(key.to_sym)
end

def sys?(field)
field.to_s =~ /sys\./
end

def id?(field)
field.to_sym == :id
end

def response
@response ||=
if @relation[:content_type] == 'Asset'
Expand All @@ -111,6 +141,14 @@ def response
end
end

def resolve_includes(entry, depth)
return entry unless entry && depth && depth > 0

WCC::Contentful::LinkVisitor.new(entry, :Link, :Asset, depth: depth).map! do |val|
resolve_link(val)
end
end

def resolve_link(val)
return val unless val.is_a?(Hash) && val.dig('sys', 'type') == 'Link'
return val unless included = response.includes[val.dig('sys', 'id')]
Expand Down

0 comments on commit 3a73424

Please sign in to comment.