Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

optionally return results as literals #18

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ context = RDF::Graph.new << [uri, RDF::Vocab::DC.title, "Some Title"]

program = Ldpath::Program.parse my_program
output = program.evaluate uri, context: context
# => { ... }
# => {"title"=>["Some Title"]}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the expected result should be for documentation purposes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That documents the normal case, but the thing being added in this PR with maintain_literals is still (I think?) entirely undocumented. As the simplest possible thing, since ldpath doesn't seem super documented in general anyway but that's not a problem for this PR, what about just adding antoher examples to the README with maintain_literals: true, and what that would return?

Also, just confirm you think maintain_literals is the right name of this arg? Preferable to, oh, use_literals or return_literals or return_rdf_literals or rdf_literals: true or something like that? I think it's up to you, I just think it's good to at least publicly consider if it's the best one we can think of before locking it into a release from which it can't be changed without backwards incompat.

I'm not totally locked into any of these ideas, just trying my best to give a responsible non-rubberstamping review on code I'm not actually at all familiar with. Disagreement welcome you can just tell me "nah, I know this code better and it's right how it is", and I'll just approve! Thanks!

```

## Compatibility
Expand Down
28 changes: 20 additions & 8 deletions lib/ldpath/field_mapping.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ def initialize(name:, selector:, field_type: nil, options: {})
@options = options
end

def evaluate(program, uri, context)
def evaluate(program, uri, context, maintain_literals: false)
case selector
when Ldpath::Selector
return to_enum(:evaluate, program, uri, context) unless block_given?
return to_enum(:evaluate, program, uri, context, maintain_literals: maintain_literals) unless block_given?

selector.evaluate(program, uri, context).each do |value|
yield transform_value(value)
selector.evaluate(program, uri, context, maintain_literals: maintain_literals).each do |value|
yield transform_value(value, maintain_literals: maintain_literals)
end
when RDF::Literal
Array(selector.canonicalize.object)
Expand All @@ -26,18 +26,30 @@ def evaluate(program, uri, context)

private

def transform_value(value)
v = if value.is_a? RDF::Literal
def transform_value(value, maintain_literals: false)
v = if value.is_a?(RDF::Literal) && !maintain_literals
value.canonicalize.object
else
value
end

if field_type
RDF::Literal.new(v.to_s, datatype: field_type).canonicalize.object
if field_type && !same_type(v, field_type)
v_literal = RDF::Literal.new(v.to_s, datatype: field_type)
maintain_literals ? v_literal : v_literal.canonicalize.object
else
v
end
end

def same_type(object, field_type)
Copy link
Contributor

@jcoyne jcoyne Nov 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be same_type? Should it be a private method?

case object
when RDF::Literal
object.comperable_datatype? field_type
when RDF::URI
field_type.to_s.end_with? 'anyURI'
else
false
end
end
end
end
2 changes: 2 additions & 0 deletions lib/ldpath/parser.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Parslet parser for parsing ldpath programs.
# @see https://kschiess.github.io/parslet/parser.html Parslet arser documentation
require 'parslet'

module Ldpath
Expand Down
31 changes: 28 additions & 3 deletions lib/ldpath/program.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,31 @@
# Parse and evaluate an ldpath program.
# @see https://kschiess.github.io/parslet/documentation.html Parslet Documentation
# @see https://marmotta.apache.org/ldpath/language.html LDPath Language Reference
module Ldpath
class Program
ParseError = Class.new StandardError

class << self

# Parse ldpath program and apply transforms.
# @param program [String] the program to be parsed
# @param transform_context [Hash] see parslet documentation for more info
# @return [Ldpath::Program] instance of this class that can be evaluated on a graph
def parse(program, transform_context = {})
ast = transform.apply load(program), transform_context

Ldpath::Program.new ast.compact, transform_context
end

# Load the ldpath program using the ldpath parser.
# @param program [String] ldpath program
# @raise [ParseError] exception raised if parse fails
# @return [Hash, Array, Parslet::Slice] PORO (Plain old Ruby object) result tree
# @example ldpath program (see spec/ldpath_program_spec.rb for a more details example program)
# @prefix dcterms : <http://purl.org/dc/terms/> ;
# title = dcterms:title :: xsd:string ;
# parent_title = dcterms:isPartOf / dcterms:title :: xsd:string ;
# int_value = <info:intProperty>[^^xsd:integer] :: xsd:integer ;
def load(program)
parser.parse(program, reporter: Parslet::ErrorReporter::Deepest.new)
rescue Parslet::ParseFailed => e
Expand Down Expand Up @@ -36,10 +53,18 @@ def initialize(mappings, default_loader: Ldpath::Loaders::Direct.new, prefixes:

end

def evaluate(uri, context: nil, limit_to_context: false)
result = Ldpath::Result.new(self, uri, context: context, limit_to_context: limit_to_context)
# Evaluate an ldpath program returning values extracted from the graph and dereferencing the subject
# to get additional context unless limit_to_context==false.
# @param uri [RDF::URI] subject URI for matching triples from the graph
# @param context [RDF::Graph] the graph from which to extract values
# @param limit_to_context [Boolean] if true, only draw values from the passed in context; otherwise, will make curl requests to gather additional context
# @param maintain_literals [Boolean] if true, will return values that are RDF::Literals as RDF::Literals; otherwise, returns canonicalize form (e.g. String, Integer, etc.)
# @return [Array<RDF::Literal, Object>] the extracted values based on the ldpath with values that can be of type RDF::URI, RDF::Literal, String, Integer, etc.,
# based on the value in the graph and the value of maintain_literals.
def evaluate(uri, context: nil, limit_to_context: false, maintain_literals: false)
result = Ldpath::Result.new(self, uri, context: context, limit_to_context: limit_to_context, maintain_literals: maintain_literals)
unless filters.empty?
return {} unless filters.all? { |f| f.evaluate(result, uri, result.context) }
return {} unless filters.all? { |f| f.evaluate(result, uri, result.context, maintain_literals: maintain_literals) }
end

result.to_hash
Expand Down
9 changes: 7 additions & 2 deletions lib/ldpath/result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ class Result
include Ldpath::Functions
attr_reader :program, :uri, :cache, :loaded

def initialize(program, uri, cache: RDF::Util::Cache.new, context: nil, limit_to_context: false)
def initialize(program, uri, cache: RDF::Util::Cache.new, context: nil, limit_to_context: false, maintain_literals: false)
@program = program
@uri = uri
@cache = cache
@loaded = {}
@context = context
@limit_to_context = limit_to_context
@maintain_literals = maintain_literals
end

def loading(uri, context)
Expand Down Expand Up @@ -59,7 +60,7 @@ def meta
private

def evaluate(mapping)
mapping.evaluate(self, uri, context)
mapping.evaluate(self, uri, context, maintain_literals: maintain_literals?)
end

def function_method?(function)
Expand All @@ -73,5 +74,9 @@ def mappings
def limit_to_context?
@limit_to_context
end

def maintain_literals?
@maintain_literals
end
end
end
45 changes: 24 additions & 21 deletions lib/ldpath/selectors.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module Ldpath
class Selector
def evaluate(program, uris, context)
return to_enum(:evaluate, program, uris, context) unless block_given?
def evaluate(program, uris, context, maintain_literals: false)
return to_enum(:evaluate, program, uris, context, maintain_literals: maintain_literals) unless block_given?
enum_wrap(uris).map do |uri|
loading program, uri, context
enum_flatten_one(evaluate_one(uri, context)).each do |x|
Expand Down Expand Up @@ -55,15 +55,15 @@ def initialize(fname, arguments = [])
@arguments = Array(arguments)
end

def evaluate(program, uris, context)
return to_enum(:evaluate, program, uris, context) unless block_given?
def evaluate(program, uris, context, maintain_literals: false)
return to_enum(:evaluate, program, uris, context, maintain_literals: maintain_literals) unless block_given?

enum_wrap(uris).map do |uri|
loading program, uri, context
args = arguments.map do |i|
case i
when Selector
i.evaluate(program, uri, context)
i.evaluate(program, uri, context, maintain_literals: maintain_literals)
else
i
end
Expand Down Expand Up @@ -138,14 +138,14 @@ def initialize(property, repeat)
@repeat = repeat
end

def evaluate(program, uris, context)
return to_enum(:evaluate, program, uris, context) unless block_given?
def evaluate(program, uris, context, maintain_literals: false)
return to_enum(:evaluate, program, uris, context, maintain_literals: maintain_literals) unless block_given?

input = enum_wrap(uris)

(0..repeat.max).each_with_index do |i, idx|
break if input.none? || (repeat.max == Ldpath::Transform::Infinity && idx > 25) # we're probably lost..
input = property.evaluate program, input, context
input = property.evaluate program, input, context, maintain_literals: maintain_literals

next unless idx >= repeat.min

Expand All @@ -165,19 +165,20 @@ def initialize(left, right)
end

class PathSelector < CompoundSelector
def evaluate(program, uris, context, &block)
return to_enum(:evaluate, program, uris, context) unless block_given?
def evaluate(program, uris, context, maintain_literals: false, &block)
return to_enum(:evaluate, program, uris, context, maintain_literals: maintain_literals) unless block_given?

output = left.evaluate(program, uris, context)
right.evaluate(program, output, context, &block)
output = left.evaluate(program, uris, context, maintain_literals: maintain_literals)
right.evaluate(program, output, context, maintain_literals: maintain_literals, &block)
end
end

class UnionSelector < CompoundSelector
def evaluate(program, uris, context)
return to_enum(:evaluate, program, uris, context) unless block_given?
def evaluate(program, uris, context, maintain_literals: false)
return to_enum(:evaluate, program, uris, context, maintain_literals: maintain_literals) unless block_given?

enum_union(left.evaluate(program, uris, context), right.evaluate(program, uris, context)).each do |x|
enum_union(left.evaluate(program, uris, context, maintain_literals: maintain_literals),
right.evaluate(program, uris, context, maintain_literals: maintain_literals)).each do |x|
yield x
end
end
Expand All @@ -198,10 +199,11 @@ def enum_union(left, right)
end

class IntersectionSelector < CompoundSelector
def evaluate(program, uris, context)
return to_enum(:evaluate, program, uris, context) unless block_given?
def evaluate(program, uris, context, maintain_literals: false)
return to_enum(:evaluate, program, uris, context, maintain_literals: maintain_literals) unless block_given?

result = left.evaluate(program, uris, context).to_a & right.evaluate(program, uris, context).to_a
result = left.evaluate(program, uris, context, maintain_literals: maintain_literals).to_a &
right.evaluate(program, uris, context, maintain_literals: maintain_literals).to_a

result.each do |x|
yield x
Expand All @@ -216,10 +218,11 @@ def initialize(identifier, tap)
@tap = tap
end

def evaluate(program, uris, context)
return to_enum(:evaluate, program, uris, context) unless block_given?
def evaluate(program, uris, context, maintain_literals: false)
return to_enum(:evaluate, program, uris, context, maintain_literals: maintain_literals) unless block_given?

program.meta[identifier] = tap.evaluate(program, uris, context).map { |x| RDF::Literal.new(x.to_s).canonicalize.object }
program.meta[identifier] = tap.evaluate(program, uris, context, maintain_literals: maintain_literals)
.map { |x| RDF::Literal.new(x.to_s).canonicalize.object }

enum_wrap(uris).map do |uri|
loading program, uri, context
Expand Down
31 changes: 16 additions & 15 deletions lib/ldpath/tests.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ def initialize(delegate, test)
@test = test
end

def evaluate(program, uris, context)
return to_enum(:evaluate, program, uris, context) unless block_given?
def evaluate(program, uris, context, maintain_literals: false)
return to_enum(:evaluate, program, uris, context, maintain_literals: maintain_literals) unless block_given?

entries = delegate.evaluate program, uris, context
entries = delegate.evaluate program, uris, context, maintain_literals: maintain_literals
entries.select do |uri|
result = enum_wrap(test.evaluate(program, uri, context)).any? do |x|
result = enum_wrap(test.evaluate(program, uri, context, maintain_literals: maintain_literals)).any? do |x|
x
end
yield uri if result
Expand All @@ -26,7 +26,7 @@ def initialize(lang)
@lang = lang
end

def evaluate(_program, uri, _context)
def evaluate(_program, uri, _context, maintain_literals: false)
return unless uri.literal?

uri if (lang.to_s == "none" && !uri.has_language?) || uri.language.to_s == lang.to_s
Expand All @@ -39,7 +39,7 @@ def initialize(type)
@type = type
end

def evaluate(program, uri, _context)
def evaluate(program, uri, _context, maintain_literals: false)
return unless uri.literal?

uri if uri.has_datatype? && uri.datatype == type
Expand All @@ -53,8 +53,8 @@ def initialize(delegate)
@delegate = delegate
end

def evaluate(program, uri, context)
!enum_wrap(delegate.evaluate(program, uri, context)).any? { |x| x }
def evaluate(program, uri, context, maintain_literals: false)
!enum_wrap(delegate.evaluate(program, uri, context, maintain_literals: maintain_literals)).any? { |x| x }
end
end

Expand All @@ -66,8 +66,9 @@ def initialize(left, right)
@right = right
end

def evaluate(program, uri, context)
left.evaluate(program, uri, context).any? || right.evaluate(program, uri, context).any?
def evaluate(program, uri, context, maintain_literals: false)
left.evaluate(program, uri, context, maintain_literals: maintain_literals).any? ||
right.evaluate(program, uri, context, maintain_literals: maintain_literals).any?
end
end

Expand All @@ -79,9 +80,9 @@ def initialize(left, right)
@right = right
end

def evaluate(program, uri, context)
left.evaluate(program, uri, context).any? &&
right.evaluate(program, uri, context).any?
def evaluate(program, uri, context, maintain_literals: false)
left.evaluate(program, uri, context, maintain_literals: maintain_literals).any? &&
right.evaluate(program, uri, context, maintain_literals: maintain_literals).any?
end
end

Expand All @@ -93,8 +94,8 @@ def initialize(left, right)
@right = right
end

def evaluate(program, uri, context)
left.evaluate(program, uri, context).include?(right)
def evaluate(program, uri, context, maintain_literals: false)
left.evaluate(program, uri, context, maintain_literals: maintain_literals).include?(right)
end
end
end
11 changes: 11 additions & 0 deletions lib/ldpath/transform.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# Support Parslet Hash transforms.
# @see https://kschiess.github.io/parslet/transform.html Parslet transform documentation
module Ldpath
class Transform < Parslet::Transform
attr_reader :prefixes

class << self
# Default set of prefixes that can be used in an ldpath program without defining.
def default_prefixes
@default_prefixes ||= {
"rdf" => RDF::Vocabulary.new("http://www.w3.org/1999/02/22-rdf-syntax-ns#"),
Expand All @@ -20,6 +23,14 @@ def default_prefixes
end
end

# Applies transformations to a tree that is generated by Parslet::Parser
# or a simple parslet. Transformation will proceed down the tree, replacing
# parts/all of it with new objects. The resulting object will be returned.
# @param obj [Object] Plain Old Ruby Object (PORO) Abstract Syntax Tree (ast) to transform
# @param context [] start context to inject into the bindings.
# @return object from the resulting transformations
# @see https://kschiess.github.io/parslet/transform.html Parslet transform documentation for more information on parameters and processing
# @see https://en.wikipedia.org/wiki/Abstract_syntax_tree Abstract syntax tree description
def apply(obj, context = nil)
context ||= {}
context[:filters] ||= []
Expand Down
Loading