lib/rdf/query/hash_pattern_normalizer.rb
module RDF; class Query
##
# An RDF query pattern normalizer.
class HashPatternNormalizer
##
# A counter that can be incremented and decremented.
class Counter
##
# The offset (or initial value) for this counter.
#
# @return [Numeric]
attr_reader :offset
##
# The increment for this counter.
#
# @return [Numeric]
attr_reader :increment
##
# @param [Numeric] offset
# the offset (or initial value) for this counter.
# @param [Numeric] increment
# the increment for this counter.
def initialize(offset = 0, increment = 1)
@offset = offset
@increment = increment
@value = @offset
end
##
# Decrements this counter, and returns the new value.
#
# @return [RDF::Query::HashPatternNormalizer::Counter]
def decrement!
@value -= @increment
self
end
##
# Increments this counter, and returns the new value.
#
# @return [RDF::Query::HashPatternNormalizer::Counter]
def increment!
@value += @increment
self
end
##
# Returns a floating point representation of this counter.
#
# @return [Float]
def to_f
@value.to_f
end
##
# Returns an integer representation of this counter.
#
# @return [Integer]
def to_i
@value.to_i
end
## Returns a string representation of this counter.
#
# @return [String]
def to_s
@value.to_s
end
end # RDF::Query::HashPatternNormalizer::Counter
class << self
##
# Returns the normalization of the specified `hash_pattern`.
#
# @overload normalize!(hash_pattern, **options)
# @param [Hash{Symbol => Object}] hash_pattern (Hash.new)
# the query pattern as a hash.
# @param [Hash{Symbol => Object}] **options
# any additional normalization options.
# @option options [String] :anonymous_subject_format ("__%s__")
# the string format for anonymous subjects.
# @return [Hash{Symbol => Object}]
# the resulting query pattern as a normalized hash.
def normalize!(*args)
hash_pattern = args.shift
options = args.shift || {}
anonymous_subject_format = options.fetch(:anonymous_subject_format, '__%s__')
raise ArgumentError, "invalid hash pattern: #{hash_pattern.inspect}" unless hash_pattern.is_a?(Hash)
counter = RDF::Query::HashPatternNormalizer::Counter.new
hash_pattern.inject({}) { |acc, pair|
subject, predicate_to_object = pair
ensure_absence_of_duplicate_subjects!(acc, subject)
normalized_predicate_to_object = normalize_hash!(predicate_to_object, acc, counter, anonymous_subject_format)
ensure_absence_of_duplicate_subjects!(acc, subject)
acc[subject] = normalized_predicate_to_object
acc
}
end
private
##
# @private
def ensure_absence_of_duplicate_subjects!(acc, subject)
raise "duplicate subject #{subject.inspect} in normalized hash pattern: #{acc.inspect}" if acc.key?(subject)
return
end
##
# @private
def normalize_array!(array, *args)
raise ArgumentError, "invalid array pattern: #{array.inspect}" unless array.is_a?(Array)
array.collect { |object|
normalize_object!(object, *args)
}
end
##
# @private
def normalize_hash!(hash, *args)
raise ArgumentError, "invalid hash pattern: #{hash.inspect}" unless hash.is_a?(Hash)
hash.inject({}) { |acc, pair|
acc[pair.first] = normalize_object!(pair.last, *args)
acc
}
end
##
# @private
def normalize_object!(object, *args)
case object
when Array then normalize_array!(object, *args)
when Hash then replace_hash_with_anonymous_subject!(object, *args)
else object
end
end
##
# @private
def replace_hash_with_anonymous_subject!(hash, acc, counter, anonymous_subject_format)
raise ArgumentError, "invalid hash pattern: #{hash.inspect}" unless hash.is_a?(Hash)
subject = (anonymous_subject_format % counter.increment!).to_sym
ensure_absence_of_duplicate_subjects!(acc, subject)
normalized_hash = normalize_hash!(hash, acc, counter, anonymous_subject_format)
ensure_absence_of_duplicate_subjects!(acc, subject)
acc[subject] = normalized_hash
subject
end
end
##
# The options for this hash pattern normalizer.
#
# @return [Hash{Symbol => Object}]
attr_reader :options
##
# @param [Hash{Symbol => Object}] options (Hash.new)
# any additional normalization options.
# @option options [String] :anonymous_subject_format ("__%s__")
# the string format for anonymous subjects.
def initialize(**options)
@options = options.dup
end
##
# Equivalent to calling `self.class.normalize!(hash_pattern, self.options)`.
#
# @param [Hash{Symbol => Object}] hash_pattern
# the query pattern as a hash.
# @return [Hash{Symbol => Object}]
# the resulting query pattern as a normalized hash.
def normalize!(hash_pattern)
self.class.normalize!(hash_pattern, @options)
end
end # RDF::Query::HashPatternNormalizer
end; end # RDF::Query