dataset manipulations in memory has got now its own adapter + new association and default_attribute methods for the mapper (which enables embedded collections) + refactor the page yaml_loader and sanitizer accordingly

did committed Feb 22, 2015
commit 8edbfcc37236601743e683d4e2bbf91f85aa2e44
Showing 35 changed files with 1250 additions and 966 deletions
locomotive/steam/adapters/filesystem.rb b/lib/locomotive/steam/adapters/filesystem.rb +35 -14
@@ @@ -1,7 +1,4 @@
- require_relative 'filesystem/dataset'
- require_relative 'filesystem/order'
- require_relative 'filesystem/condition'
- require_relative 'filesystem/query'
+ require_relative 'memory'
require_relative 'filesystem/simple_cache_store'
@@ @@ -9,6 +6,10 @@ require_relative 'filesystem/yaml_loader'
require_relative 'filesystem/yaml_loaders/site'
require_relative 'filesystem/yaml_loaders/page'
+ require_relative 'filesystem/sanitizer'
+ require_relative 'filesystem/sanitizers/simple'
+ require_relative 'filesystem/sanitizers/page'
+
module Locomotive::Steam
class FilesystemAdapter < Struct.new(:site_path)
@@ @@ -23,6 +24,10 @@ module Locomotive::Steam
build_yaml_loaders(cache)
end
+ register :sanitizers do
+ build_sanitizers
+ end
+
def initialize(site_path)
super
@datasets = {}
@@ @@ -43,7 +48,7 @@ module Locomotive::Steam
private
def _query(mapper, scope, &block)
- Locomotive::Steam::Adapters::Filesystem::Query.new(all(mapper, scope), scope.locale, &block)
+ Locomotive::Steam::Adapters::Memory::Query.new(all(mapper, scope), scope.locale, &block)
end
def memoized_dataset(mapper, scope)
@@ @@ -52,18 +57,22 @@ module Locomotive::Steam
end
def dataset(mapper, scope)
- Locomotive::Steam::Adapters::Filesystem::Dataset.new(mapper.name).tap do |dataset|
+ Locomotive::Steam::Adapters::Memory::Dataset.new(mapper.name).tap do |dataset|
@datasets[mapper.name] = dataset
+ populate_dataset(dataset, mapper, scope)
+ end
+ end
+ def populate_dataset(dataset, mapper, scope)
+ sanitizers[mapper.name].with(scope) do |sanitizer|
collection(mapper, scope).each do |attributes|
entity = mapper.to_entity(attributes)
-
- # assign the site_id to the entity + sanitize attributes
- # specific to the Filesystem adapter
- entity[:site_id] = scope.site.id if scope.site
-
dataset.insert(entity)
+
+ sanitizer.apply_to(entity)
end
+
+ sanitizer.apply_to(dataset)
end
end
@@ @@ -73,12 +82,24 @@ module Locomotive::Steam
def build_yaml_loaders(cache)
%i(site page).inject({}) do |memo, name|
- _name = name.to_s.singularize.camelize
- klass = "Locomotive::Steam::Adapters::Filesystem::YAMLLoaders::#{_name}".constantize
- memo[name] = klass.new(site_path, cache)
+ memo[name] = build_klass('YAMLLoaders', name).new(site_path, cache)
+ memo
end
end
+ def build_sanitizers
+ hash = Hash.new { build_klass('Sanitizers', :simple).new }
+ %i(pages).inject(hash) do |memo, name|
+ memo[name] = build_klass('Sanitizers', name).new
+ memo
+ end
+ end
+
+ def build_klass(type, name)
+ _name = name.to_s.singularize.camelize
+ "Locomotive::Steam::Adapters::Filesystem::#{type}::#{_name}".constantize
+ end
+
end
end
locomotive/steam/adapters/filesystem/concerns/queryable.rb b/lib/locomotive/steam/adapters/filesystem/concerns/queryable.rb +0 -65
@@ @@ -1,65 +0,0 @@
- # module Locomotive
- # module Steam
- # module Adapters
- # module Filesystem
- # module Concerns
-
- # module Queryable
-
- # extend ActiveSupport::Concern
-
- # def query(*args, &block)
- # _locale = respond_to?(:current_locale) ? current_locale : nil
- # Filesystem::Query.new(memoized_collection(*args), _locale, &block)
- # end
-
- # private
-
- # def localized_attribute(object, method)
- # if (values = object.send(method)).is_a?(Hash)
- # values[current_locale]
- # else
- # values
- # end
- # end
-
- # def memoized_collection(*args)
- # return @collection if @collection
-
- # @collection = collection(*args)
- # end
-
- # def collection(*args)
- # _collection = loader.list_of_attributes(*args).map do |attributes|
- # collection_options[:model].new(attributes)
- # end
-
- # sanitize!(_collection)
- # end
-
- # def sanitize!(collection)
- # sanitizer.try(:apply_to, collection) || collection
- # end
-
- # def sanitizer
- # return unless (klass = collection_options[:sanitizer])
- # klass.new(site.default_locale, site.locales)
- # end
-
- # module ClassMethods
-
- # def set_collection(options = {})
- # class_eval do
- # define_method(:collection_options) { options }
- # end
- # end
-
- # end
-
- # end
-
- # end
- # end
- # end
- # end
- # end
locomotive/steam/adapters/filesystem/condition.rb b/lib/locomotive/steam/adapters/filesystem/condition.rb +0 -103
@@ @@ -1,103 +0,0 @@
- module Locomotive::Steam
- module Adapters
- module Filesystem
-
- class Condition
-
- class UnsupportedOperator < StandardError; end
-
- OPERATORS = %i(== eq ne neq matches gt gte lt lte size all in nin).freeze
-
- attr_reader :field, :operator, :value
-
- def initialize(operator_and_field, value, locale)
- @locale = locale.try(:to_sym)
- @operator_and_field, @value = operator_and_field, value
- @operator, @field = :==, operator_and_field
-
- decode_operator_and_field!
- end
-
- def matches?(entry)
- entry_value = entry_value(entry)
-
- adapt_operator!(entry_value)
- case @operator
- when :== then entry_value == @value
- when :eq then entry_value == @value
- when :ne then entry_value != @value
- when :neq then entry_value != @value
- when :matches then @value =~ entry_value
- when :gt then entry_value > @value
- when :gte then entry_value >= @value
- when :lt then entry_value < @value
- when :lte then entry_value <= @value
- when :size then entry_value.size == @value
- when :all then array_contains?([*@value], entry_value)
- when :in, :nin then value_is_in_entry_value?(entry_value)
- else
- raise UnknownConditionInScope.new("#{@operator} is unknown or not implemented.")
- end
- end
-
- def to_s
- "#{field} #{operator} #{@value.to_s}"
- end
-
- protected
-
- def entry_value(entry)
- value = entry.send(@field)
-
- if value.respond_to?(:translations)
- value[@locale]
- else
- value
- end
- end
-
- def decode_operator_and_field!
- if match = @operator_and_field.match(/^(?<field>[a-z0-9_-]+)\.(?<operator>.*)$/)
- @field = match[:field].to_sym
- @operator = match[:operator].to_sym
- check_operator!
- end
-
- @operator = :matches if @value.is_a?(Regexp)
- end
-
- def adapt_operator!(value)
- case value
- when Array
- @operator = :in if @operator == :==
- end
- end
-
- def value_is_in_entry_value?(value)
- _matches = if value.is_a?(Array)
- array_contains?([*value], [*@value])
- else
- [*@value].include?(value)
- end
- @operator == :in ? _matches : !_matches
- end
-
- private
-
- def check_operator!
- raise UnsupportedOperator.new unless OPERATORS.include?(@operator)
- end
-
- def array_contains?(source, target)
- if target.size == 0
- source.size == 0
- else
- source & target == target
- end
- end
-
- end
-
- end
- end
- end
locomotive/steam/adapters/filesystem/dataset.rb b/lib/locomotive/steam/adapters/filesystem/dataset.rb +0 -75
@@ @@ -1,75 +0,0 @@
- module Locomotive::Steam
- module Adapters
- module Filesystem
-
- class Dataset
-
- class PrimaryKey
- def initialize
- @current = 0
- end
-
- def increment!
- yield(@current += 1)
- @current
- end
- end
-
- attr_reader :records, :name
-
- def initialize(name)
- @name = name
- clear!
- end
-
- def insert(record)
- @primary_key.increment! do |id|
- record[identity] = id
- records[id] = record
- end
- end
-
- def update(record)
- records[record[identity]] = records[record[identity]].deep_merge(record)
- end
-
- def delete(id)
- records.delete(id)
- end
-
- def size
- records.size
- end
-
- def all
- records.values
- end
-
- def find(id)
- records.fetch(id) do
- raise Locomotive::Steam::Repository::RecordNotFound, "could not find #{name} with #{identity} = #{id}"
- end
- end
-
- def exists?(id)
- !!id && records.has_key?(id)
- end
-
- # def query
- # Query.new(self)
- # end
-
- def clear!
- @records = {}
- @primary_key = PrimaryKey.new
- end
-
- private
-
- def identity
- @identity ||= :_id
- end
- end
- end
- end
- end
locomotive/steam/adapters/filesystem/order.rb b/lib/locomotive/steam/adapters/filesystem/order.rb +0 -59
@@ @@ -1,59 +0,0 @@
- module Locomotive::Steam
- module Adapters
- module Filesystem
-
- class Order
-
- attr_reader :list
-
- def initialize(*args)
- strings = args.compact
-
- @list = (case args.size
- when 0 then []
- when 1 then args.first.split(',').collect { |s| build(s.strip) }
- else
- args.collect { |s| build(s) }
- end)
- end
-
- def empty?
- @list.empty?
- end
-
- def apply_to(entry, locale)
- @list.collect do |(name, direction)|
- value = entry.send(name)
- asc?(direction) ? Asc.new(value) : Desc.new(value)
- end
- end
-
- def asc?(direction)
- direction.nil? || direction == :asc
- end
-
- private
-
- def build(string)
- pattern = string.include?('.') ? '.' : ' '
- string.downcase.split(pattern).map(&:to_sym)
- end
-
- class Direction
- attr_reader :obj
- def initialize(obj); @obj = obj; end
- end
-
- class Asc < Direction
- def <=>(other); @obj <=> other.obj; end
- end
-
- class Desc < Direction
- def <=>(other); other.obj <=> @obj; end
- end
-
- end
-
- end
- end
- end
locomotive/steam/adapters/filesystem/query.rb b/lib/locomotive/steam/adapters/filesystem/query.rb +0 -100
@@ @@ -1,100 +0,0 @@
- require 'forwardable'
-
- module Locomotive::Steam
- module Adapters
- module Filesystem
-
- class Query
-
- include Enumerable
- extend Forwardable
-
- def_delegators :all, :each, :to_s, :to_a, :empty?, :size
-
- alias :length :size
- alias :count :size
-
- attr_reader :conditions
-
- def initialize(dataset, locale = nil, &block)
- @dataset = dataset
- @conditions = []
- @sorting = nil
- @limit = nil
- @offset = 0
- @locale = locale
- instance_eval(&block) if block_given?
- end
-
- def where(conditions = {})
- @conditions += conditions.map { |name, value| Condition.new(name, value, @locale) }
- self
- end
-
- def +(query)
- @conditions += query.conditions
- self
- end
-
- def order_by(*args)
- @sorting = Order.new(*args)
- end
-
- def limit(num)
- @limit = num
- self
- end
-
- def offset(num)
- @offset = num
- self
- end
-
- def ==(other)
- if other.kind_of? Array
- all == other
- else
- super
- end
- end
-
- def all
- limited sorted(filtered)
- end
-
- def sorted(entries)
- return entries if @sorting.empty?
-
- entries.sort_by { |entry| @sorting.apply_to(entry, @locale) }
- end
-
- def limited(entries)
- return [] if @limit == 0
- return entries if @offset == 0 && @limit.nil?
-
- subentries = entries.drop(@offset || 0)
- if @limit.kind_of? Integer
- subentries.take(@limit)
- else
- subentries
- end
- end
-
- def filtered
- @dataset.all.dup.find_all do |entry|
- accepted = true
-
- @conditions.each do |_condition|
- unless _condition.matches?(entry)
- accepted = false
- break # no to go further
- end
- end
- accepted
- end
- end # filtered
-
- end
- end
- end
- end
locomotive/steam/adapters/filesystem/sanitizer.rb b/lib/locomotive/steam/adapters/filesystem/sanitizer.rb +45 -0
@@ @@ -0,0 +1,45 @@
+ module Locomotive::Steam
+ module Adapters
+ module Filesystem
+
+ module Sanitizer
+
+ extend Forwardable
+
+ def_delegators :@scope, :site, :locale, :locales, :default_locale
+
+ attr_reader :scope
+
+ def setup(scope)
+ @scope = scope
+ self
+ end
+
+ def with(scope, &block)
+ setup(scope)
+ yield(self)
+ end
+
+ def apply_to(entity_or_dataset)
+ if entity_or_dataset.respond_to?(:all)
+ apply_to_dataset(entity_or_dataset)
+ else
+ apply_to_entity(entity_or_dataset)
+ end
+ end
+
+ def apply_to_dataset(dataset)
+ dataset
+ end
+
+ def apply_to_entity(entity)
+ entity
+ end
+
+ alias :current_locale :locale
+
+ end
+
+ end
+ end
+ end
locomotive/steam/adapters/filesystem/sanitizers/page.rb b/lib/locomotive/steam/adapters/filesystem/sanitizers/page.rb +155 -145
@@ @@ -1,145 +1,155 @@
- # module Locomotive
- # module Steam
- # module Repositories
- # module Filesystem
- # module Sanitizers
-
- # class Page < Struct.new(:default_locale, :locales)
-
- # def initialize(default_locale, locales)
- # super
- # @content_types = {}
- # @localized = {}
- # locales.each { |locale| @localized[locale] = {} }
- # end
-
- # def apply_to(collection)
- # sorted_collection(collection).each do |page|
- # locales.each do |locale|
- # set_fullpath_for(page, locale)
- # modify_if_templatized(page, locale)
- # build_editable_elements(page, locale)
- # use_default_locale_template_path(page, locale)
- # set_default_redirect_type(page, locale)
- # end
- # end
- # end
-
- # # If the page does not have a template in a locale
- # # then use the template of the default locale
- # #
- # def use_default_locale_template_path(page, locale)
- # paths = page.template_path
-
- # if paths[locale] == false
- # paths[locale] = paths[default_locale]
- # end
- # end
-
- # def set_default_redirect_type(page, locale)
- # if page.redirect_url[locale]
- # page.attributes[:redirect_type] ||= 301
- # end
- # end
-
- # def build_editable_elements(page, locale)
- # elements = page.editable_elements[locale] || {}
- # elements.stringify_keys!
-
- # elements.each do |name, content|
- # segments = name.split('/')
- # block, slug = segments[0..-2].join('/'), segments.last
- # block = nil if block.blank?
-
- # elements[name] = Filesystem::Models::EditableElement.new(block, slug, content)
- # end
- # end
-
- # def modify_if_templatized(page, locale)
- # if page.templatized?
- # # change the slug of a templatized page
- # page[:slug][locale] = 'content-type-template'
-
- # # this also means to change the fullpath
- # if page[:fullpath][locale]
- # page[:fullpath][locale].gsub!(/[^\/]+$/, 'content-type-template')
- # end
-
- # # make sure its children will have its content type
- # set_content_type(page._fullpath, page.content_type)
- # elsif content_type = fetch_content_type(parent_fullpath(page))
- # # not a templatized page but it becomes one because
- # # its parent is one of them
- # page[:content_type] = content_type
- # end
- # end
-
- # def set_fullpath_for(page, locale)
- # page._fullpath ||= page.attributes.delete(:_fullpath)
-
- # slug = fullpath = page.slug[locale].try(:dasherize)
-
- # return if slug.blank?
-
- # if page.depth > 1
- # base = parent_fullpath(page)
- # fullpath = (fetch_localized_fullpath(base, locale) || base) + '/' + slug
- # end
-
- # set_localized_fullpath(page._fullpath, fullpath, locale)
- # page[:fullpath][locale] = fullpath
- # end
-
- # def depth(page)
- # return page.depth if page.depth
-
- # page.depth = page[:_fullpath].split('/').size
-
- # slug = get_slug(page)
-
- # if page.depth == 1 && %w(index 404).include?(slug)
- # page.depth = 0
- # end
-
- # page.depth
- # end
-
- # def get_slug(page)
- # if page.slug.is_a?(Hash)
- # page.slug.values.compact.first
- # else
- # page.slug
- # end
- # end
-
- # def sorted_collection(collection)
- # collection.sort_by { |page| depth(page) }
- # end
-
- # def parent_fullpath(page)
- # page._fullpath.split('/')[0..-2].join('/')
- # end
-
- # def fetch_content_type(fullpath)
- # @content_types[fullpath]
- # end
-
- # def set_content_type(fullpath, value)
- # @content_types[fullpath] = value
- # end
-
- # def fetch_localized_fullpath(fullpath, locale)
- # @localized[locale][fullpath]
- # end
-
- # def set_localized_fullpath(fullpath, value, locale)
- # @localized[locale][fullpath] = value
- # end
-
- # end
-
- # end
- # end
- # end
- # end
- # end
+ module Locomotive::Steam
+ module Adapters
+ module Filesystem
+ module Sanitizers
+
+ class Page # < Struct.new(:default_locale, :locales)
+
+ include Adapters::Filesystem::Sanitizer
+
+ # def initialize(default_locale, locales)
+ # super
+ # @content_types = {}
+ # @localized = {}
+ # locales.each { |locale| @localized[locale] = {} }
+ # end
+
+ def setup(scope)
+ super.tap do
+ @ids = {}
+ @content_types = {}
+ @localized = Hash.new { {} }
+ end
+ end
+
+ def apply_to_entity(entity)
+ entity[:site_id] = scope.site.id if scope.site
+
+ # required to get the parent_id
+ @ids[entity._fullpath] = entity._id
+
+ locales.each do |locale|
+ set_default_redirect_type(entity, locale)
+ end
+ end
+
+ def apply_to_dataset(dataset)
+ sorted_collection(dataset.records).each do |page|
+ locales.each do |locale|
+ set_parent_id(page)
+ set_fullpath_for(page, locale)
+ modify_if_templatized(page, locale)
+ use_default_locale_template_path(page, locale)
+ end
+ end
+ end
+
+ # when this is called, the @ids hash has been populated completely
+ def set_parent_id(page)
+ page.parent_id = @ids[parent_fullpath(page)]
+ end
+
+ # If the page does not have a template in a locale
+ # then use the template of the default locale
+ #
+ def use_default_locale_template_path(page, locale)
+ paths = page.template_path
+
+ if paths[locale] == false
+ paths[locale] = paths[default_locale]
+ end
+ end
+
+ def set_default_redirect_type(page, locale)
+ if page.redirect_url[locale]
+ page.attributes[:redirect_type] ||= 301
+ end
+ end
+
+ def modify_if_templatized(page, locale)
+ if page.templatized?
+ # change the slug of a templatized page
+ page[:slug][locale] = 'content-type-template'
+
+ # this also means to change the fullpath
+ if page[:fullpath][locale]
+ page[:fullpath][locale].gsub!(/[^\/]+$/, 'content-type-template')
+ end
+
+ # make sure its children will have its content type
+ set_content_type(page._fullpath, page.content_type)
+ elsif content_type = fetch_content_type(parent_fullpath(page))
+ # not a templatized page but it becomes one because
+ # its parent is one of them
+ page[:content_type] = content_type
+ end
+ end
+
+ def set_fullpath_for(page, locale)
+ page._fullpath ||= page.attributes.delete(:_fullpath)
+
+ slug = fullpath = page.slug[locale].try(:dasherize)
+
+ return if slug.blank?
+
+ if page.depth > 1
+ base = parent_fullpath(page)
+ fullpath = (fetch_localized_fullpath(base, locale) || base) + '/' + slug
+ end
+
+ set_localized_fullpath(page._fullpath, fullpath, locale)
+ page[:fullpath][locale] = fullpath
+ end
+
+ def depth(page)
+ return page.depth if page.depth
+
+ page.depth = page[:_fullpath].split('/').size
+
+ slug = get_slug(page)
+
+ if page.depth == 1 && %w(index 404).include?(slug)
+ page.depth = 0
+ end
+
+ page.depth
+ end
+
+ def get_slug(page)
+ if page.slug.is_a?(Hash)
+ page.slug.values.compact.first
+ else
+ page.slug
+ end
+ end
+
+ def sorted_collection(collection)
+ collection.sort_by { |page| depth(page) }
+ end
+
+ def parent_fullpath(page)
+ page._fullpath.split('/')[0..-2].join('/')
+ end
+
+ def fetch_content_type(fullpath)
+ @content_types[fullpath]
+ end
+
+ def set_content_type(fullpath, value)
+ @content_types[fullpath] = value
+ end
+
+ def fetch_localized_fullpath(fullpath, locale)
+ @localized[locale][fullpath]
+ end
+
+ def set_localized_fullpath(fullpath, value, locale)
+ @localized[locale][fullpath] = value
+ end
+
+ end
+
+ end
+ end
+ end
+ end
locomotive/steam/adapters/filesystem/sanitizers/simple.rb b/lib/locomotive/steam/adapters/filesystem/sanitizers/simple.rb +15 -0
@@ @@ -0,0 +1,15 @@
+ module Locomotive::Steam
+ module Adapters
+ module Filesystem
+ module Sanitizers
+
+ class Simple
+
+ include Adapters::Filesystem::Sanitizer
+
+ end
+
+ end
+ end
+ end
+ end
locomotive/steam/adapters/filesystem/yaml_loaders/page.rb b/lib/locomotive/steam/adapters/filesystem/yaml_loaders/page.rb +37 -2
@@ @@ -41,9 +41,9 @@ module Locomotive
{
title: { locale => attributes.delete(:title) || (default_locale == locale ? slug.humanize : nil) },
slug: { locale => attributes.delete(:slug) || slug },
- editable_elements: { locale => attributes.delete(:editable_elements) },
template_path: { locale => template_path(filepath, attributes, locale) },
redirect_url: { locale => attributes.delete(:redirect_url) },
+ editable_elements: build_editable_elements(attributes.delete(:editable_elements), locale),
_fullpath: fullpath
}.merge(attributes)
end
@@ @@ -54,10 +54,11 @@ module Locomotive
leaf[:title][locale] = attributes.delete(:title) || slug.humanize
leaf[:slug][locale] = attributes.delete(:slug) || slug
- leaf[:editable_elements][locale] = attributes.delete(:editable_elements)
leaf[:template_path][locale] = template_path(filepath, attributes, locale)
leaf[:redirect_url][locale] = attributes.delete(:redirect_url)
+ update_editable_elements(leaf, attributes.delete(:editable_elements), locale)
+
leaf.merge!(attributes)
end
@@ @@ -100,6 +101,40 @@ module Locomotive
filepath.gsub(path, '').gsub(/^\//, '')
end
+ def build_editable_elements(list, locale)
+ return [] if list.blank?
+
+ list.map do |name, content|
+ build_editable_element(name, content, locale)
+ end
+ end
+
+ def update_editable_elements(leaf, list, locale)
+ return if list.blank?
+
+ list.each do |name, content|
+ if el = find_editable_element(leaf, name)
+ el[:content][locale] = content
+ else
+ leaf[:editable_elements] << build_editable_element(name, content, locale)
+ end
+ end
+ end
+
+ def find_editable_element(leaf, name)
+ leaf[:editable_elements].find do |el|
+ [el[:block], el[:slug]].join('/') == name
+ end
+ end
+
+ def build_editable_element(name, content, locale)
+ segments = name.to_s.split('/')
+ block, slug = segments[0..-2].join('/'), segments.last
+ block = nil if block.blank?
+
+ { block: block, slug: slug, content: { locale => content } }
+ end
+
end
end
locomotive/steam/adapters/memory.rb b/lib/locomotive/steam/adapters/memory.rb +42 -0
@@ @@ -0,0 +1,42 @@
+ require_relative 'memory/order'
+ require_relative 'memory/condition'
+ require_relative 'memory/query'
+ require_relative 'memory/dataset'
+
+ module Locomotive::Steam
+
+ class MemoryAdapter < Struct.new(:collection)
+
+ def all(mapper, scope)
+ memoized_dataset(mapper, scope)
+ end
+
+ def query(mapper, scope, &block)
+ _query(mapper, scope, &block)
+ end
+
+ private
+
+ def _query(mapper, scope, &block)
+ Locomotive::Steam::Adapters::Memory::Query.new(all(mapper, scope), scope.locale, &block)
+ end
+
+ def memoized_dataset(mapper, scope)
+ return @dataset if @dataset
+ dataset(mapper, scope)
+ end
+
+ def dataset(mapper, scope)
+ Locomotive::Steam::Adapters::Memory::Dataset.new(mapper.name).tap do |dataset|
+ collection.each do |attributes|
+ entity = mapper.to_entity(attributes)
+ dataset.insert(entity)
+ end
+ end
+ end
+
+ end
+
+ end
+
+
locomotive/steam/adapters/memory/condition.rb b/lib/locomotive/steam/adapters/memory/condition.rb +103 -0
@@ @@ -0,0 +1,103 @@
+ module Locomotive::Steam
+ module Adapters
+ module Memory
+
+ class Condition
+
+ class UnsupportedOperator < StandardError; end
+
+ OPERATORS = %i(== eq ne neq matches gt gte lt lte size all in nin).freeze
+
+ attr_reader :field, :operator, :value
+
+ def initialize(operator_and_field, value, locale)
+ @locale = locale.try(:to_sym)
+ @operator_and_field, @value = operator_and_field, value
+ @operator, @field = :==, operator_and_field
+
+ decode_operator_and_field!
+ end
+
+ def matches?(entry)
+ entry_value = entry_value(entry)
+
+ adapt_operator!(entry_value)
+ case @operator
+ when :== then entry_value == @value
+ when :eq then entry_value == @value
+ when :ne then entry_value != @value
+ when :neq then entry_value != @value
+ when :matches then @value =~ entry_value
+ when :gt then entry_value > @value
+ when :gte then entry_value >= @value
+ when :lt then entry_value < @value
+ when :lte then entry_value <= @value
+ when :size then entry_value.size == @value
+ when :all then array_contains?([*@value], entry_value)
+ when :in, :nin then value_is_in_entry_value?(entry_value)
+ else
+ raise UnknownConditionInScope.new("#{@operator} is unknown or not implemented.")
+ end
+ end
+
+ def to_s
+ "#{field} #{operator} #{@value.to_s}"
+ end
+
+ protected
+
+ def entry_value(entry)
+ value = entry.send(@field)
+
+ if value.respond_to?(:translations)
+ value[@locale]
+ else
+ value
+ end
+ end
+
+ def decode_operator_and_field!
+ if match = @operator_and_field.match(/^(?<field>[a-z0-9_-]+)\.(?<operator>.*)$/)
+ @field = match[:field].to_sym
+ @operator = match[:operator].to_sym
+ check_operator!
+ end
+
+ @operator = :matches if @value.is_a?(Regexp)
+ end
+
+ def adapt_operator!(value)
+ case value
+ when Array
+ @operator = :in if @operator == :==
+ end
+ end
+
+ def value_is_in_entry_value?(value)
+ _matches = if value.is_a?(Array)
+ array_contains?([*value], [*@value])
+ else
+ [*@value].include?(value)
+ end
+ @operator == :in ? _matches : !_matches
+ end
+
+ private
+
+ def check_operator!
+ raise UnsupportedOperator.new unless OPERATORS.include?(@operator)
+ end
+
+ def array_contains?(source, target)
+ if target.size == 0
+ source.size == 0
+ else
+ source & target == target
+ end
+ end
+
+ end
+
+ end
+ end
+ end
locomotive/steam/adapters/memory/dataset.rb b/lib/locomotive/steam/adapters/memory/dataset.rb +75 -0
@@ @@ -0,0 +1,75 @@
+ module Locomotive::Steam
+ module Adapters
+ module Memory
+
+ class Dataset
+
+ class PrimaryKey
+ def initialize
+ @current = 0
+ end
+
+ def increment!
+ yield(@current += 1)
+ @current
+ end
+ end
+
+ attr_reader :records, :name
+
+ def initialize(name)
+ @name = name
+ clear!
+ end
+
+ def insert(record)
+ @primary_key.increment! do |id|
+ record[identity] = id
+ records[id] = record
+ end
+ end
+
+ def update(record)
+ records[record[identity]] = records[record[identity]].deep_merge(record)
+ end
+
+ def delete(id)
+ records.delete(id)
+ end
+
+ def size
+ records.size
+ end
+
+ def all
+ records.values
+ end
+
+ def find(id)
+ records.fetch(id) do
+ raise Locomotive::Steam::Repository::RecordNotFound, "could not find #{name} with #{identity} = #{id}"
+ end
+ end
+
+ def exists?(id)
+ !!id && records.has_key?(id)
+ end
+
+ # def query
+ # Query.new(self)
+ # end
+
+ def clear!
+ @records = {}
+ @primary_key = PrimaryKey.new
+ end
+
+ private
+
+ def identity
+ @identity ||= :_id
+ end
+ end
+ end
+ end
+ end
locomotive/steam/adapters/memory/order.rb b/lib/locomotive/steam/adapters/memory/order.rb +59 -0
@@ @@ -0,0 +1,59 @@
+ module Locomotive::Steam
+ module Adapters
+ module Memory
+
+ class Order
+
+ attr_reader :list
+
+ def initialize(*args)
+ strings = args.compact
+
+ @list = (case args.size
+ when 0 then []
+ when 1 then args.first.split(',').collect { |s| build(s.strip) }
+ else
+ args.collect { |s| build(s) }
+ end)
+ end
+
+ def empty?
+ @list.empty?
+ end
+
+ def apply_to(entry, locale)
+ @list.collect do |(name, direction)|
+ value = entry.send(name)
+ asc?(direction) ? Asc.new(value) : Desc.new(value)
+ end
+ end
+
+ def asc?(direction)
+ direction.nil? || direction == :asc
+ end
+
+ private
+
+ def build(string)
+ pattern = string.include?('.') ? '.' : ' '
+ string.downcase.split(pattern).map(&:to_sym)
+ end
+
+ class Direction
+ attr_reader :obj
+ def initialize(obj); @obj = obj; end
+ end
+
+ class Asc < Direction
+ def <=>(other); @obj <=> other.obj; end
+ end
+
+ class Desc < Direction
+ def <=>(other); other.obj <=> @obj; end
+ end
+
+ end
+
+ end
+ end
+ end
locomotive/steam/adapters/memory/query.rb b/lib/locomotive/steam/adapters/memory/query.rb +100 -0
@@ @@ -0,0 +1,100 @@
+ require 'forwardable'
+
+ module Locomotive::Steam
+ module Adapters
+ module Memory
+
+ class Query
+
+ include Enumerable
+ extend Forwardable
+
+ def_delegators :all, :each, :to_s, :to_a, :empty?, :size
+
+ alias :length :size
+ alias :count :size
+
+ attr_reader :conditions
+
+ def initialize(dataset, locale = nil, &block)
+ @dataset = dataset
+ @conditions = []
+ @sorting = nil
+ @limit = nil
+ @offset = 0
+ @locale = locale
+ instance_eval(&block) if block_given?
+ end
+
+ def where(conditions = {})
+ @conditions += conditions.map { |name, value| Condition.new(name, value, @locale) }
+ self
+ end
+
+ def +(query)
+ @conditions += query.conditions
+ self
+ end
+
+ def order_by(*args)
+ @sorting = Order.new(*args)
+ end
+
+ def limit(num)
+ @limit = num
+ self
+ end
+
+ def offset(num)
+ @offset = num
+ self
+ end
+
+ def ==(other)
+ if other.kind_of? Array
+ all == other
+ else
+ super
+ end
+ end
+
+ def all
+ limited sorted(filtered)
+ end
+
+ def sorted(entries)
+ return entries if @sorting.blank?
+
+ entries.sort_by { |entry| @sorting.apply_to(entry, @locale) }
+ end
+
+ def limited(entries)
+ return [] if @limit == 0
+ return entries if @offset == 0 && @limit.nil?
+
+ subentries = entries.drop(@offset || 0)
+ if @limit.kind_of? Integer
+ subentries.take(@limit)
+ else
+ subentries
+ end
+ end
+
+ def filtered
+ @dataset.all.dup.find_all do |entry|
+ accepted = true
+
+ @conditions.each do |_condition|
+ unless _condition.matches?(entry)
+ accepted = false
+ break # no to go further
+ end
+ end
+ accepted
+ end
+ end # filtered
+
+ end
+ end
+ end
+ end
locomotive/steam/entities/editable_element.rb b/lib/locomotive/steam/entities/editable_element.rb +13 -0
@@ @@ -0,0 +1,13 @@
+ module Locomotive::Steam
+
+ class EditableElement
+
+ include Locomotive::Steam::Models::Entity
+
+ attr_accessor :_parent
+
+ # TODO
+
+ end
+
+ end
locomotive/steam/models.rb b/lib/locomotive/steam/models.rb +1 -0
@@ @@ -1,5 +1,6 @@
require_relative 'models/concerns/validation'
require_relative 'models/i18n_field'
+ require_relative 'models/association'
require_relative 'models/entity'
require_relative 'models/mapper'
require_relative 'models/scope'
locomotive/steam/models/association.rb b/lib/locomotive/steam/models/association.rb +29 -0
@@ @@ -0,0 +1,29 @@
+ require 'locomotive/steam/adapters/memory'
+ require 'morphine'
+
+ module Locomotive::Steam
+ module Models
+
+ # Note: represents an embedded collection
+ class Association < SimpleDelegator
+
+ include Morphine
+
+ register :adapter do
+ Locomotive::Steam::MemoryAdapter.new(nil)
+ end
+
+ def initialize(repository_klass, collection)
+ adapter.collection = collection
+ @repository = repository_klass.new(adapter)
+ super(@repository)
+ end
+
+ def attach(name, entity)
+ @repository.send(:"#{name}=", entity)
+ end
+
+ end
+
+ end
+ end
locomotive/steam/models/mapper.rb b/lib/locomotive/steam/models/mapper.rb +55 -8
@@ @@ -3,27 +3,43 @@ module Locomotive::Steam
class Mapper
- attr_reader :name, :options, :localized_attributes
+ attr_reader :name, :options, :default_attributes, :localized_attributes, :associations
+
+ def initialize(name, options, repository, &block)
+ @name, @options, @repository = name, options, repository
- def initialize(name, options, &block)
- @name, @options = name, options
@localized_attributes = []
+ @default_attributes = []
+ @associations = []
instance_eval(&block) if block_given?
end
- def set_localized_attributes(*args)
+ def localized_attributes(*args)
@localized_attributes += [*args]
end
+ def default_attribute(name, value)
+ @default_attributes += [[name.to_sym, value]]
+ end
+
+ # Note: only works for embedded-type associations
+ def association(name, repository_klass)
+ @associations += [[name.to_sym, repository_klass]]
+ end
+
def to_entity(attributes)
- entity_klass.new(serialize(attributes))
+ entity_klass.new(serialize(attributes)).tap do |entity|
+ attach_entity_to_associations(entity)
+ set_default_attributes(entity)
+ end
end
def serialize(attributes)
- localized_attributes.each do |name|
- attributes[name] = I18nField.new(name, attributes[name])
- end
+ serialize_localized_attributes(attributes)
+
+ serialize_associations(attributes)
+
attributes
end
@@ @@ -31,6 +47,37 @@ module Locomotive::Steam
options[:entity]
end
+ private
+
+ # create a proxy class for each localized attribute
+ def serialize_localized_attributes(attributes)
+ @localized_attributes.each do |name|
+ attributes[name] = I18nField.new(name, attributes[name])
+ end
+ end
+
+ # build the embedded associations
+ def serialize_associations(attributes)
+ @associations.each do |name, repository_klass|
+ attributes[name] = Association.new(repository_klass, attributes[name])
+ end
+ end
+
+ def attach_entity_to_associations(entity)
+ @associations.each do |(name, _)|
+ key = name.to_s.singularize.to_sym
+ entity[name].attach(key, entity)
+ end
+ end
+
+ def set_default_attributes(entity)
+ @default_attributes.each do |(name, value)|
+ # _value = value.respond_to?(:call) ? @repository.instance_eval(&value) : value
+ _value = value.respond_to?(:call) ? value.call(@repository) : value
+ entity.send(:"#{name}=", _value)
+ end
+ end
+
end
end
locomotive/steam/models/repository.rb b/lib/locomotive/steam/models/repository.rb +1 -1
@@ @@ -43,7 +43,7 @@ module Locomotive::Steam
def mapper
name, options, block = mapper_options
- @mapper ||= Mapper.new(name, options, &block)
+ @mapper ||= Mapper.new(name, options, self, &block)
end
def scope
locomotive/steam/repositories/editable_element_repository.rb b/lib/locomotive/steam/repositories/editable_element_repository.rb +21 -0
@@ @@ -0,0 +1,21 @@
+ module Locomotive
+ module Steam
+
+ class EditableElementRepository
+
+ include Models::Repository
+
+ attr_accessor :page
+
+ mapping :editable_elements, entity: EditableElement do
+ localized_attributes :content, :source, :default_content, :default_source_url
+
+ default_attribute :page, -> (repository) { repository.page }
+ end
+
+ # TODO
+
+ end
+
+ end
+ end
locomotive/steam/repositories/page_repository.rb b/lib/locomotive/steam/repositories/page_repository.rb +14 -7
@@ @@ -5,8 +5,12 @@ module Locomotive
include Models::Repository
+ # Entity mapping
mapping :pages, entity: Page do
- set_localized_attributes :title, :slug, :permalink, :editable_elements, :template, :template_path, :redirect_url, :fullpath, :seo_title, :meta_description, :meta_keywords
+ localized_attributes :title, :slug, :permalink, :editable_elements, :template, :template_path, :redirect_url, :fullpath, :seo_title, :meta_description, :meta_keywords
+
+ # embedded association
+ association :editable_elements, EditableElementRepository
end
# Engine: site.pages.ordered_pages(conditions) [WIP]
@@ @@ -17,20 +21,22 @@ module Locomotive
end.all
end
- # Engine: site.pages.where(handle: handle).first
+ # Engine: site.pages.where(handle: handle).first [TODO]
def by_handle(handle)
query { where(handle: handle) }.first
end
+ # [TODO]
def by_fullpath(path)
query { where(fullpath: path) }.first
end
+ # [TODO]
def matching_fullpath(list)
all('fullpath.in' => list)
end
- # Engine: ???
+ # Engine: ??? [TODO]
def template_for(entry, handle = nil)
conditions = { templatized?: true, content_type: entry.try(:content_type_slug) }
@@ @@ -41,11 +47,12 @@ module Locomotive
end
end
+ # [TODO]
def root
query { where(fullpath: 'index') }.first
end
- # Engine: page.parent
+ # Engine: page.parent [TODO]
def parent_of(page)
return nil if page.nil? || page.index?
@@ @@ -57,7 +64,7 @@ module Locomotive
by_fullpath(path)
end
- # Engine: page.ancestors_and_self
+ # Engine: page.ancestors_and_self [TODO]
def ancestors_of(page)
return [] if page.nil?
@@ @@ -69,7 +76,7 @@ module Locomotive
all('fullpath.in' => ['index'] + paths)
end
- # Engine: page.children
+ # Engine: page.children [TODO]
def children_of(page)
return [] if page.nil?
@@ @@ -82,7 +89,7 @@ module Locomotive
all(conditions)
end
- # Engine: page.editable_elements
+ # Engine: page.editable_elements [TODO]
def editable_elements_of(page)
return nil if page.nil?
localized_attribute(page, :editable_elements).values
locomotive/steam/repositories/site_repository.rb b/lib/locomotive/steam/repositories/site_repository.rb +1 -1
@@ @@ -6,7 +6,7 @@ module Locomotive
include Models::Repository
mapping :sites, entity: Site do
- set_localized_attributes :seo_title, :meta_description, :meta_keywords
+ localized_attributes :seo_title, :meta_description, :meta_keywords
end
def by_handle_or_domain(handle, domain)
spec/unit/adapters/filesystem/condition_spec.rb +0 -124
@@ @@ -1,124 +0,0 @@
- require 'spec_helper'
-
- require_relative '../../../../lib/locomotive/steam/adapters/filesystem/condition.rb'
-
- describe Locomotive::Steam::Adapters::Filesystem::Condition do
-
- let(:entry) { instance_double('Site', { title: { en: 'Awesome Site' }, content: 'foo' }) }
- let(:locale) { :en }
- let(:field) { :title }
- let(:operator) { :eq }
- let(:name) { "#{field}.#{operator}"}
- let(:value) { 'Awesome Site' }
-
- subject { Locomotive::Steam::Adapters::Filesystem::Condition.new(name, value, locale) }
-
- describe '#entry_value' do
- context 'i18n' do
- let(:name) { 'title.eq' }
- let(:value) { 'Awesome Site' }
-
- context 'single entry' do
- specify('should be match') do
- expect(subject.matches?(entry)).to eq true
- end
-
- specify('return value') do
- expect(subject.send(:entry_value, entry)).to eq(value)
- end
- end
- end
- context 'regular way' do
- let(:name) { 'content.eq' }
- let(:value) { 'foo' }
-
- context 'single entry' do
- specify('should be match') do
- expect(subject.matches?(entry)).to eq true
- end
-
- specify('return value') do
- expect(subject.send(:entry_value, entry)).to eq(value)
- end
- end
- end
- end
-
- describe '#decode_operator_and_field!' do
- before { subject.send(:decode_operator_and_field!) }
-
- context 'with normal value' do
- specify('name should be left part of dot') { expect(subject.field).to eq(field) }
- specify('operator should be right part of dot') { expect(subject.operator).to eq(operator) }
- specify('right_operand should be value') { expect(subject.value).to eq(value) }
- end
-
- context 'with regex value' do
- let(:value) { /^[a-z]$/ }
- specify('operator should be matchtes') { expect(subject.operator).to eq(:matches) }
- end
- end
-
- describe '#decode_operator_and_field!' do
- context 'with unsupported operator' do
- let(:name) { 'domains.unsupported' }
- specify('should be throw Exception') do
- expect do
- subject.send(:decode_operator_and_field!)
- end.to raise_error Locomotive::Steam::Adapters::Filesystem::Condition::UnsupportedOperator
- end
- end
- end
-
- describe '#adapt_operator!' do
- let(:name) { 'domains.==' }
- before do
- subject.send(:decode_operator_and_field!)
- subject.send(:adapt_operator!, value)
- end
- context 'with single value' do
- let(:value) { 'sample.example.com' }
- specify('operator should be :==') { expect(subject.operator).to eq(:==) }
- end
- context 'with array of values' do
- let(:value) { ['sample.example.com'] }
- specify('operator should be :in') { expect(subject.operator).to eq(:in) }
- end
- end
-
- describe '#array_contains?' do
- let(:source) { [1, 2, 3, 4] }
- let(:target) { [1, 2, 3] }
- context 'with target contains in source' do
- specify('should be true') do
- expect(subject.send(:array_contains?, source, target)).to eq true
- end
- end
- end
-
- describe '#value_in_right_operand?' do
- context 'value contains in right operand' do
- let(:value) { [1, 2, 3, 4] }
- let(:right_operand) { [1, 2, 3] }
-
- before do
- allow(subject).to receive(:operator).and_return(operator)
- allow(subject).to receive(:right_operand).and_return(right_operand)
- end
-
- context 'with operator :in' do
- let(:operator) { :in }
- specify('should return true') do
- expect(subject.send(:value_is_in_entry_value?, value)).to eq true
- end
- end
-
- context 'with other operator' do
- let(:operator) { :nin }
- specify('should not return true') do
- expect(subject.send(:value_is_in_entry_value?, value)).to eq false
- end
- end
- end
- end
- end
spec/unit/adapters/filesystem/dataset_spec.rb +0 -73
@@ @@ -1,73 +0,0 @@
- require 'spec_helper'
-
- require_relative '../../../../lib/locomotive/steam/adapters/filesystem/dataset.rb'
-
- describe Locomotive::Steam::Adapters::Filesystem::Dataset do
-
- let(:john) do
- {
- firstname: 'John',
- lastname: 'Doe',
- email: 'john@example.com',
- age: 24
- }
- end
-
- let(:jane) do
- {
- firstname: 'Jane',
- lastname: 'Doe',
- email: 'jane@example.com',
- age: 20
- }
- end
-
- let(:alex) do
- {
- firstname: 'Alex',
- lastname: 'Turam',
- email: 'alex@example.com',
- age: 26
- }
- end
-
- subject { Locomotive::Steam::Adapters::Filesystem::Dataset.new(:foo) } #(loader) }
-
- before do
- [john.to_hash, jane.to_hash, alex.to_hash].each do |record|
- subject.insert record
- end
- end
-
- describe '#all' do
- it { expect(subject.all).to eq [john.to_hash, jane.to_hash, alex.to_hash] }
- end
-
- describe '#find' do
- specify do
- expect(subject.find(john[:_id])).to eq(john.to_hash)
- end
- end
-
- describe '#update' do
- before do
- subject.update(jane.to_hash.merge(lastname: 'birkin'))
- end
-
- specify do
- expect(subject.find(jane[:_id]).fetch(:lastname)).to eq('birkin')
- end
- end
-
- describe '#exists?' do
- let(:dataset) { Locomotive::Steam::Adapters::Filesystem::Dataset.new(:dummy) }
- before do
- dataset.instance_variable_set('@records', { 1 => 'Record 1', 2 => 'Record 2' })
- end
-
- it { expect(dataset.exists?(2)).to eq true }
- it { expect(dataset.exists?(3)).to eq false }
- it { expect(dataset.exists?(nil)).to eq false }
-
- end
- end
spec/unit/adapters/filesystem/order_spec.rb +0 -67
@@ @@ -1,67 +0,0 @@
- require 'spec_helper'
-
- require_relative '../../../../lib/locomotive/steam/adapters/filesystem/order.rb'
-
- describe Locomotive::Steam::Adapters::Filesystem::Order do
-
- let(:order) { Locomotive::Steam::Adapters::Filesystem::Order.new(*input) }
-
- describe '#list' do
-
- subject { order.list }
-
- let(:input) { nil }
- it { is_expected.to eq [] }
-
- context 'a string' do
-
- let(:input) { 'name' }
- it { is_expected.to eq [[:name]] }
-
- end
-
- context 'two strings' do
-
- let(:input) { ['name', 'date.desc'] }
- it { is_expected.to eq [[:name], [:date, :desc]] }
-
- end
-
- context 'a string with a comma' do
-
- let(:input) { 'name, date desc' }
- it { is_expected.to eq [[:name], [:date, :desc]] }
-
- end
-
- end
-
- describe '#apply_to' do
-
- subject { order.apply_to(entry, :en) }
-
- let(:input) { 'title, date desc' }
- let(:entry) { instance_double('Entry', title: 'foo', date: Time.now) }
- it { expect(subject.map(&:class)).to eq([Locomotive::Steam::Adapters::Filesystem::Order::Asc, Locomotive::Steam::Adapters::Filesystem::Order::Desc]) }
-
- end
-
- describe 'sort' do
-
- let(:array) {
- [
- instance_double('Entry1', id: 1, title: 'b', position: 1),
- instance_double('Entry2', id: 2, title: 'b', position: 2),
- instance_double('Entry3', id: 3, title: 'a', position: 3),
- instance_double('Entry3', id: 4, title: 'c', position: 1)
- ]
- }
- let(:input) { 'title, position desc' }
-
- subject { array.sort_by { |entry| order.apply_to(entry, :en) } }
-
- it { expect(subject.map(&:id)).to eq([3, 2, 1, 4]) }
-
- end
-
- end
spec/unit/adapters/filesystem/query_spec.rb +0 -64
@@ @@ -1,64 +0,0 @@
- require 'spec_helper'
-
- require_relative '../../../../lib/locomotive/steam/adapters/filesystem/dataset.rb'
- require_relative '../../../../lib/locomotive/steam/adapters/filesystem/condition.rb'
- require_relative '../../../../lib/locomotive/steam/adapters/filesystem/query.rb'
-
- describe Locomotive::Steam::Adapters::Filesystem::Query do
-
- let(:entry_1) { OpenStruct.new(name: 'foo', id: 1) }
- let(:entry_2) { OpenStruct.new(name: 'bar', id: 2) }
- let(:entry_3) { OpenStruct.new(name: 'zone', id: 3) }
- let(:records) { { 1 => entry_1, 2 => entry_2, 3 => entry_3 } }
- let(:dataset) { Locomotive::Steam::Adapters::Filesystem::Dataset.new(:test) }
- let(:locale) { :en }
-
- let(:query) { Locomotive::Steam::Adapters::Filesystem::Query }
-
- before { allow(dataset).to receive(:records).and_return(records) }
-
- describe '#limited' do
- specify do
- expect(
- query.new(dataset, locale) do
- limit(1)
- end.all
- ).to eq([entry_1])
- end
- end
-
- describe '#order_by' do
-
- context 'asc' do
- specify do
- expect(
- query.new(dataset, locale) do
- order_by('name asc')
- end.all.map(&:name)
- ).to eq(['bar', 'foo', 'zone'])
- end
- end
-
- context 'desc' do
- specify do
- expect(
- query.new(dataset, locale) do
- order_by('name desc')
- end.all.map(&:name)
- ).to eq(['zone', 'foo', 'bar'])
- end
- end
- end
-
- describe '#where' do
- specify do
- expect(
- query.new(dataset, locale) do
- where('name.eq' => 'foo').
- where('id.lt' => 2)
- end.all.map(&:name)
- ).to eq(['foo'])
- end
- end
-
- end
spec/unit/adapters/filesystem_adapter_spec.rb +45 -0
@@ @@ -0,0 +1,45 @@
+ require 'spec_helper'
+
+ require_relative '../../../lib/locomotive/steam/adapters/filesystem.rb'
+
+ describe Locomotive::Steam::FilesystemAdapter do
+
+ let(:mapper) { instance_double('Mapper', name: :test) }
+ let(:scope) { instance_double('Scope', site: site, locale: nil) }
+ let(:adapter) { Locomotive::Steam::FilesystemAdapter.new(nil) }
+
+ describe '#query' do
+
+ let(:collection) { [OpenStruct.new(site_id: 42, name: 'Hello world')] }
+
+ before do
+ allow(mapper).to receive(:to_entity) { |arg| arg }
+ allow(adapter).to receive(:collection).and_return(collection)
+ end
+
+ subject { adapter.query(mapper, scope) { where(name: 'Hello world') } }
+
+ context 'not scoped by a site' do
+
+ let(:site) { nil }
+ it { expect(subject.first.name).to eq 'Hello world' }
+
+ end
+
+ context 'scoped by a site' do
+
+ let(:site) { instance_double('Site', id: 42) }
+ it { expect(subject.first.name).to eq 'Hello world' }
+
+ context 'unknown site id' do
+
+ let(:site) { instance_double('Site', id: 1) }
+ it { expect(subject.first).to eq nil }
+
+ end
+
+ end
+
+ end
+
+ end
spec/unit/adapters/filesystem_spec.rb +0 -45
@@ @@ -1,45 +0,0 @@
- require 'spec_helper'
-
- require_relative '../../../lib/locomotive/steam/adapters/filesystem.rb'
-
- describe Locomotive::Steam::FilesystemAdapter do
-
- let(:mapper) { instance_double('Mapper', name: :test) }
- let(:scope) { instance_double('Scope', site: site, locale: nil) }
- let(:adapter) { Locomotive::Steam::FilesystemAdapter.new(nil) }
-
- describe '#query' do
-
- let(:collection) { [OpenStruct.new(site_id: 42, name: 'Hello world')] }
-
- before do
- allow(mapper).to receive(:to_entity) { |arg| arg }
- allow(adapter).to receive(:collection).and_return(collection)
- end
-
- subject { adapter.query(mapper, scope) { where(name: 'Hello world') } }
-
- context 'not scoped by a site' do
-
- let(:site) { nil }
- it { expect(subject.first.name).to eq 'Hello world' }
-
- end
-
- context 'scoped by a site' do
-
- let(:site) { instance_double('Site', id: 42) }
- it { expect(subject.first.name).to eq 'Hello world' }
-
- context 'unknown site id' do
-
- let(:site) { instance_double('Site', id: 1) }
- it { expect(subject.first).to eq nil }
-
- end
-
- end
-
- end
-
- end
spec/unit/adapters/memory/condition_spec.rb +125 -0
@@ @@ -0,0 +1,125 @@
+ require 'spec_helper'
+
+ require_relative '../../../../lib/locomotive/steam/adapters/memory/condition.rb'
+
+ describe Locomotive::Steam::Adapters::Memory::Condition do
+
+ let(:title) { instance_double('Title', translations: { en: 'Awesome Site' }, :[] => 'Awesome Site') }
+ let(:entry) { instance_double('Site', { title: title, content: 'foo' }) }
+ let(:locale) { :en }
+ let(:field) { :title }
+ let(:operator) { :eq }
+ let(:name) { "#{field}.#{operator}"}
+ let(:value) { 'Awesome Site' }
+
+ subject { Locomotive::Steam::Adapters::Memory::Condition.new(name, value, locale) }
+
+ describe '#entry_value' do
+ context 'i18n' do
+ let(:name) { 'title.eq' }
+ let(:value) { 'Awesome Site' }
+
+ context 'single entry' do
+ specify('match') do
+ expect(subject.matches?(entry)).to eq true
+ end
+
+ specify('return value') do
+ expect(subject.send(:entry_value, entry)).to eq(value)
+ end
+ end
+ end
+ context 'regular way' do
+ let(:name) { 'content.eq' }
+ let(:value) { 'foo' }
+
+ context 'single entry' do
+ specify('should be match') do
+ expect(subject.matches?(entry)).to eq true
+ end
+
+ specify('return value') do
+ expect(subject.send(:entry_value, entry)).to eq(value)
+ end
+ end
+ end
+ end
+
+ describe '#decode_operator_and_field!' do
+ before { subject.send(:decode_operator_and_field!) }
+
+ context 'with normal value' do
+ specify('name should be left part of dot') { expect(subject.field).to eq(field) }
+ specify('operator should be right part of dot') { expect(subject.operator).to eq(operator) }
+ specify('right_operand should be value') { expect(subject.value).to eq(value) }
+ end
+
+ context 'with regex value' do
+ let(:value) { /^[a-z]$/ }
+ specify('operator should be matchtes') { expect(subject.operator).to eq(:matches) }
+ end
+ end
+
+ describe '#decode_operator_and_field!' do
+ context 'with unsupported operator' do
+ let(:name) { 'domains.unsupported' }
+ specify('should be throw Exception') do
+ expect do
+ subject.send(:decode_operator_and_field!)
+ end.to raise_error Locomotive::Steam::Adapters::Memory::Condition::UnsupportedOperator
+ end
+ end
+ end
+
+ describe '#adapt_operator!' do
+ let(:name) { 'domains.==' }
+ before do
+ subject.send(:decode_operator_and_field!)
+ subject.send(:adapt_operator!, value)
+ end
+ context 'with single value' do
+ let(:value) { 'sample.example.com' }
+ specify('operator should be :==') { expect(subject.operator).to eq(:==) }
+ end
+ context 'with array of values' do
+ let(:value) { ['sample.example.com'] }
+ specify('operator should be :in') { expect(subject.operator).to eq(:in) }
+ end
+ end
+
+ describe '#array_contains?' do
+ let(:source) { [1, 2, 3, 4] }
+ let(:target) { [1, 2, 3] }
+ context 'with target contains in source' do
+ specify('should be true') do
+ expect(subject.send(:array_contains?, source, target)).to eq true
+ end
+ end
+ end
+
+ describe '#value_in_right_operand?' do
+ context 'value contains in right operand' do
+ let(:value) { [1, 2, 3, 4] }
+ let(:right_operand) { [1, 2, 3] }
+
+ before do
+ allow(subject).to receive(:operator).and_return(operator)
+ allow(subject).to receive(:right_operand).and_return(right_operand)
+ end
+
+ context 'with operator :in' do
+ let(:operator) { :in }
+ specify('should return true') do
+ expect(subject.send(:value_is_in_entry_value?, value)).to eq true
+ end
+ end
+
+ context 'with other operator' do
+ let(:operator) { :nin }
+ specify('should not return true') do
+ expect(subject.send(:value_is_in_entry_value?, value)).to eq false
+ end
+ end
+ end
+ end
+ end
spec/unit/adapters/memory/dataset_spec.rb +73 -0
@@ @@ -0,0 +1,73 @@
+ require 'spec_helper'
+
+ require_relative '../../../../lib/locomotive/steam/adapters/memory/dataset.rb'
+
+ describe Locomotive::Steam::Adapters::Memory::Dataset do
+
+ let(:john) do
+ {
+ firstname: 'John',
+ lastname: 'Doe',
+ email: 'john@example.com',
+ age: 24
+ }
+ end
+
+ let(:jane) do
+ {
+ firstname: 'Jane',
+ lastname: 'Doe',
+ email: 'jane@example.com',
+ age: 20
+ }
+ end
+
+ let(:alex) do
+ {
+ firstname: 'Alex',
+ lastname: 'Turam',
+ email: 'alex@example.com',
+ age: 26
+ }
+ end
+
+ subject { Locomotive::Steam::Adapters::Memory::Dataset.new(:foo) } #(loader) }
+
+ before do
+ [john.to_hash, jane.to_hash, alex.to_hash].each do |record|
+ subject.insert record
+ end
+ end
+
+ describe '#all' do
+ it { expect(subject.all).to eq [john.to_hash, jane.to_hash, alex.to_hash] }
+ end
+
+ describe '#find' do
+ specify do
+ expect(subject.find(john[:_id])).to eq(john.to_hash)
+ end
+ end
+
+ describe '#update' do
+ before do
+ subject.update(jane.to_hash.merge(lastname: 'birkin'))
+ end
+
+ specify do
+ expect(subject.find(jane[:_id]).fetch(:lastname)).to eq('birkin')
+ end
+ end
+
+ describe '#exists?' do
+ let(:dataset) { Locomotive::Steam::Adapters::Memory::Dataset.new(:dummy) }
+ before do
+ dataset.instance_variable_set('@records', { 1 => 'Record 1', 2 => 'Record 2' })
+ end
+
+ it { expect(dataset.exists?(2)).to eq true }
+ it { expect(dataset.exists?(3)).to eq false }
+ it { expect(dataset.exists?(nil)).to eq false }
+
+ end
+ end
spec/unit/adapters/memory/order_spec.rb +67 -0
@@ @@ -0,0 +1,67 @@
+ require 'spec_helper'
+
+ require_relative '../../../../lib/locomotive/steam/adapters/memory/order.rb'
+
+ describe Locomotive::Steam::Adapters::Memory::Order do
+
+ let(:order) { Locomotive::Steam::Adapters::Memory::Order.new(*input) }
+
+ describe '#list' do
+
+ subject { order.list }
+
+ let(:input) { nil }
+ it { is_expected.to eq [] }
+
+ context 'a string' do
+
+ let(:input) { 'name' }
+ it { is_expected.to eq [[:name]] }
+
+ end
+
+ context 'two strings' do
+
+ let(:input) { ['name', 'date.desc'] }
+ it { is_expected.to eq [[:name], [:date, :desc]] }
+
+ end
+
+ context 'a string with a comma' do
+
+ let(:input) { 'name, date desc' }
+ it { is_expected.to eq [[:name], [:date, :desc]] }
+
+ end
+
+ end
+
+ describe '#apply_to' do
+
+ subject { order.apply_to(entry, :en) }
+
+ let(:input) { 'title, date desc' }
+ let(:entry) { instance_double('Entry', title: 'foo', date: Time.now) }
+ it { expect(subject.map(&:class)).to eq([Locomotive::Steam::Adapters::Memory::Order::Asc, Locomotive::Steam::Adapters::Memory::Order::Desc]) }
+
+ end
+
+ describe 'sort' do
+
+ let(:array) {
+ [
+ instance_double('Entry1', id: 1, title: 'b', position: 1),
+ instance_double('Entry2', id: 2, title: 'b', position: 2),
+ instance_double('Entry3', id: 3, title: 'a', position: 3),
+ instance_double('Entry3', id: 4, title: 'c', position: 1)
+ ]
+ }
+ let(:input) { 'title, position desc' }
+
+ subject { array.sort_by { |entry| order.apply_to(entry, :en) } }
+
+ it { expect(subject.map(&:id)).to eq([3, 2, 1, 4]) }
+
+ end
+
+ end
spec/unit/adapters/memory/query_spec.rb +65 -0
@@ @@ -0,0 +1,65 @@
+ require 'spec_helper'
+
+ require_relative '../../../../lib/locomotive/steam/adapters/memory/dataset.rb'
+ require_relative '../../../../lib/locomotive/steam/adapters/memory/condition.rb'
+ require_relative '../../../../lib/locomotive/steam/adapters/memory/order.rb'
+ require_relative '../../../../lib/locomotive/steam/adapters/memory/query.rb'
+
+ describe Locomotive::Steam::Adapters::Memory::Query do
+
+ let(:entry_1) { OpenStruct.new(name: 'foo', id: 1) }
+ let(:entry_2) { OpenStruct.new(name: 'bar', id: 2) }
+ let(:entry_3) { OpenStruct.new(name: 'zone', id: 3) }
+ let(:records) { { 1 => entry_1, 2 => entry_2, 3 => entry_3 } }
+ let(:dataset) { Locomotive::Steam::Adapters::Memory::Dataset.new(:test) }
+ let(:locale) { :en }
+
+ let(:query) { Locomotive::Steam::Adapters::Memory::Query }
+
+ before { allow(dataset).to receive(:records).and_return(records) }
+
+ describe '#limited' do
+ specify do
+ expect(
+ query.new(dataset, locale) do
+ limit(1)
+ end.all
+ ).to eq([entry_1])
+ end
+ end
+
+ describe '#order_by' do
+
+ context 'asc' do
+ specify do
+ expect(
+ query.new(dataset, locale) do
+ order_by('name asc')
+ end.all.map(&:name)
+ ).to eq(['bar', 'foo', 'zone'])
+ end
+ end
+
+ context 'desc' do
+ specify do
+ expect(
+ query.new(dataset, locale) do
+ order_by('name desc')
+ end.all.map(&:name)
+ ).to eq(['zone', 'foo', 'bar'])
+ end
+ end
+ end
+
+ describe '#where' do
+ specify do
+ expect(
+ query.new(dataset, locale) do
+ where('name.eq' => 'foo').
+ where('id.lt' => 2)
+ end.all.map(&:name)
+ ).to eq(['foo'])
+ end
+ end
+
+ end
spec/unit/adapters/memory_adapter_spec.rb +28 -0
@@ @@ -0,0 +1,28 @@
+ require 'spec_helper'
+
+ require_relative '../../../lib/locomotive/steam/adapters/memory.rb'
+
+ describe Locomotive::Steam::MemoryAdapter do
+
+ let(:collection) { [OpenStruct.new(name: 'Hello world')] }
+ let(:mapper) { instance_double('Mapper', name: :test) }
+ let(:scope) { instance_double('Scope', locale: nil) }
+ let(:adapter) { Locomotive::Steam::MemoryAdapter.new(collection) }
+
+ before { allow(mapper).to receive(:to_entity) { |arg| arg } }
+
+ describe '#all' do
+
+ subject { adapter.all(mapper, scope) }
+ it { expect(subject.size).to eq 1 }
+
+ end
+
+ describe '#query' do
+
+ subject { adapter.query(mapper, scope) { where(name: 'Hello world') } }
+ it { expect(subject.size).to eq 1 }
+
+ end
+
+ end
spec/unit/models/mapper_spec.rb +46 -13
@@ @@ -2,14 +2,15 @@ require 'spec_helper'
describe Locomotive::Steam::Models::Mapper do
- let(:name) { 'pages' }
- let(:options) { { entity: MyPage } }
- let(:block) { nil }
- let(:mapper) { Locomotive::Steam::Models::Mapper.new(name, options, &block) }
+ let(:repository) { instance_double('Repository') }
+ let(:name) { 'pages' }
+ let(:options) { { entity: MyPage } }
+ let(:block) { nil }
+ let(:mapper) { Locomotive::Steam::Models::Mapper.new(name, options, repository, &block) }
describe '#localized attributes' do
- let(:block) { ->(_) { set_localized_attributes(:foo, :bar) } }
+ let(:block) { ->(_) { localized_attributes(:foo, :bar) } }
subject { mapper.localized_attributes }
it { is_expected.to eq [:foo, :bar] }
@@ @@ -18,24 +19,56 @@ describe Locomotive::Steam::Models::Mapper do
describe '#to_entity' do
- let(:block) { ->(_) { set_localized_attributes(:title) } }
- let(:attributes) { { title: { 'en' => 'Hello world' } } }
-
subject { mapper.to_entity(attributes) }
- it { expect(subject.attributes[:title].class).to eq Locomotive::Steam::Models::I18nField }
- it { expect(subject.attributes[:title][:en]).to eq('Hello world') }
- context 'string value for the localized field' do
+ describe 'default attributes' do
let(:attributes) { { title: 'Hello world' } }
+ let(:repository) { instance_double('Repository', my_site: 42) }
+ let(:block) { ->(_) { default_attribute(:site, -> (repository) { repository.my_site }) } }
+
+ it { expect(subject.site).to eq 42 }
+
+ end
+
+ describe 'association' do
+
+ let(:attributes) { { parents: [instance_double('Page', title: 'Hello world')] } }
+ let(:klass) { instance_double('RepositoryKlass')}
+ let(:block) { ->(_) { association(:parents, BlankRepository) } }
+
+ it { expect(subject.parents).not_to eq nil }
+
+ end
+ describe 'localized attributes' do
+
+ let(:block) { ->(_) { localized_attributes(:title) } }
+ let(:attributes) { { title: { 'en' => 'Hello world' } } }
+
+ it { expect(subject.attributes[:title].class).to eq Locomotive::Steam::Models::I18nField }
it { expect(subject.attributes[:title][:en]).to eq('Hello world') }
- it { expect(subject.attributes[:title][:fr]).to eq('Hello world') }
+
+ context 'string value for the localized field' do
+
+ let(:attributes) { { title: 'Hello world' } }
+
+ it { expect(subject.attributes[:title][:en]).to eq('Hello world') }
+ it { expect(subject.attributes[:title][:fr]).to eq('Hello world') }
+
+ end
end
end
- class MyPage < Struct.new(:attributes); end
+ class MyPage
+ include Locomotive::Steam::Models::Entity
+ attr_accessor :site
+ end
+
+ class BlankRepository < Struct.new(:adapter)
+ attr_accessor :parent
+ end
end