diff --git a/VERSION b/VERSION index f092941a..e650c01d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.2.8 +3.2.9 diff --git a/lib/rdf/cli.rb b/lib/rdf/cli.rb index 6efb2d6b..0606c9e9 100644 --- a/lib/rdf/cli.rb +++ b/lib/rdf/cli.rb @@ -25,6 +25,7 @@ rdf/xsd shacl shex + yaml_ld ).each do |ser| begin require ser @@ -177,7 +178,10 @@ def to_hash # * `parse` Boolean value to determine if input files should automatically be parsed into `repository`. # * `help` used for the CLI help output. # * `lambda` code run to execute command. - # * `filter` Option values that must match for command to be used + # * `filter` value is a Hash whose keys are matched against selected command options. All specified `key/value` pairs are compared against the equivalent key in the current invocation. + # If an Array, option value (as a string) must match any value of the array (as a string) + # If a Proc, it is passed the option value and must return `true`. + # Otherwise, the option value (as a string) must equal the `value` (as a string). # * `control` Used to indicate how (if) command is displayed # * `repository` Use this repository, if set # * `options` an optional array of `RDF::CLI::Option` describing command-specific options. @@ -505,9 +509,22 @@ def self.exec(args, output: $stdout, option_parser: nil, messages: {}, **options # Make sure any selected command isn't filtered out cmds.each do |c| COMMANDS[c.to_sym].fetch(:filter, {}).each do |opt, val| - if options[opt].to_s != val.to_s - usage(option_parser, banner: "Command #{c.inspect} requires #{opt}: #{val}, not #{options.fetch(opt, 'null')}") - raise ArgumentError, "Incompatible command #{c} used with option #{opt}=#{options[opt]}" + case val + when Array + unless val.map(&:to_s).include?(options[opt].to_s) + usage(option_parser, banner: "Command #{c.inspect} requires #{opt} in #{val.map(&:to_s).inspect}, not #{options.fetch(opt, 'null')}") + raise ArgumentError, "Incompatible command #{c} used with option #{opt}=#{options[opt]}" + end + when Proc + unless val.call(options[opt]) + usage(option_parser, banner: "Command #{c.inspect} #{opt} inconsistent with #{options.fetch(opt, 'null')}") + raise ArgumentError, "Incompatible command #{c} used with option #{opt}=#{options[opt]}" + end + else + unless val.to_s == options[opt].to_s + usage(option_parser, banner: "Command #{c.inspect} requires compatible value for #{opt}, not #{options.fetch(opt, 'null')}") + raise ArgumentError, "Incompatible command #{c} used with option #{opt}=#{options[opt]}" + end end end @@ -594,7 +611,14 @@ def self.commands(format: nil, **options) # Subset commands based on filter options cmds = COMMANDS.reject do |k, c| c.fetch(:filter, {}).any? do |opt, val| - options.merge(format: format)[opt].to_s != val.to_s + case val + when Array + !val.map(&:to_s).include?(options[opt].to_s) + when Proc + !val.call(options[opt]) + else + val.to_s != options[opt].to_s + end end end diff --git a/lib/rdf/model/literal/date.rb b/lib/rdf/model/literal/date.rb index 242cde3d..0641004e 100644 --- a/lib/rdf/model/literal/date.rb +++ b/lib/rdf/model/literal/date.rb @@ -3,10 +3,11 @@ module RDF; class Literal # A date literal. # # @see http://www.w3.org/TR/xmlschema11-2/#date + # @see https://www.w3.org/TR/xmlschema11-2/#rf-lexicalMappings-datetime # @since 0.2.1 class Date < Temporal DATATYPE = RDF::URI("http://www.w3.org/2001/XMLSchema#date") - GRAMMAR = %r(\A(-?\d{4}-\d{2}-\d{2})((?:[\+\-]\d{2}:\d{2})|UTC|GMT|Z)?\Z).freeze + GRAMMAR = %r(\A(#{YEARFRAG}-#{MONTHFRAG}-#{DAYFRAG})(#{TZFRAG})?\z).freeze FORMAT = '%Y-%m-%d'.freeze ## diff --git a/lib/rdf/model/literal/datetime.rb b/lib/rdf/model/literal/datetime.rb index db86f882..32e2c2ae 100644 --- a/lib/rdf/model/literal/datetime.rb +++ b/lib/rdf/model/literal/datetime.rb @@ -3,10 +3,22 @@ module RDF; class Literal # A date/time literal. # # @see http://www.w3.org/TR/xmlschema11-2/#dateTime + # @see https://www.w3.org/TR/xmlschema11-2/#rf-lexicalMappings-datetime # @since 0.2.1 class DateTime < Temporal DATATYPE = RDF::URI("http://www.w3.org/2001/XMLSchema#dateTime") - GRAMMAR = %r(\A(-?(?:\d{4}|[1-9]\d{4,})-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?)((?:[\+\-]\d{2}:\d{2})|UTC|GMT|Z)?\Z).freeze + GRAMMAR = %r(\A + (#{YEARFRAG} + -#{MONTHFRAG} + -#{DAYFRAG} + T + (?: + (?: + #{HOURFRAG} + :#{MINUTEFRAG} + :#{SECONDFRAG}) + | #{EODFRAG})) + (#{TZFRAG})?\z)x.freeze FORMAT = '%Y-%m-%dT%H:%M:%S.%L'.freeze ## diff --git a/lib/rdf/model/literal/temporal.rb b/lib/rdf/model/literal/temporal.rb index 8cd2673d..2e7571c5 100644 --- a/lib/rdf/model/literal/temporal.rb +++ b/lib/rdf/model/literal/temporal.rb @@ -10,6 +10,15 @@ class Temporal < Literal |(?:(?-)?PT(?
\d{1,2})H(?:(?\d{1,2})M)?) \z)x.freeze + YEARFRAG = %r(-?(?:(?:[1-9]\d{3,})|(?:0\d{3}))) + MONTHFRAG = %r((?:(?:0[1-9])|(?:1[0-2]))) + DAYFRAG = %r((?:(?:0[1-9])|(?:[12]\d)|(?:3[01]))) + HOURFRAG = %r((?:[01]\d)|(?:2[0-3])) + MINUTEFRAG = %r([0-5]\d) + SECONDFRAG = %r([0-5]\d(?:\.\d+)?) + EODFRAG = %r(24:00:00(?:\.0+)?) + TZFRAG = %r((?:[\+\-]\d{2}:\d{2})|UTC|GMT|Z) + ## # Compares this literal to `other` for sorting purposes. # diff --git a/lib/rdf/model/literal/time.rb b/lib/rdf/model/literal/time.rb index 7d3a4d7b..7d6fdcb7 100644 --- a/lib/rdf/model/literal/time.rb +++ b/lib/rdf/model/literal/time.rb @@ -8,10 +8,11 @@ module RDF; class Literal # following time zone indicator. # # @see http://www.w3.org/TR/xmlschema11-2/#time + # @see https://www.w3.org/TR/xmlschema11-2/#rf-lexicalMappings-datetime # @since 0.2.1 class Time < Temporal DATATYPE = RDF::URI("http://www.w3.org/2001/XMLSchema#time") - GRAMMAR = %r(\A(\d{2}:\d{2}:\d{2}(?:\.\d+)?)((?:[\+\-]\d{2}:\d{2})|UTC|GMT|Z)?\Z).freeze + GRAMMAR = %r(\A((?:#{HOURFRAG}:#{MINUTEFRAG}:#{SECONDFRAG})|#{EODFRAG})(#{TZFRAG})?\z).freeze FORMAT = '%H:%M:%S.%L'.freeze ## diff --git a/spec/cli_spec.rb b/spec/cli_spec.rb index 7e4e5a7a..76e0c51c 100644 --- a/spec/cli_spec.rb +++ b/spec/cli_spec.rb @@ -248,13 +248,37 @@ end end - it "complains if filtered command is attempted" do - RDF::CLI.add_command(:filtered, filter: {output_format: :nquads}) - expect do - expect do - RDF::CLI.exec(["filtered"], output_format: :ntriples) - end.to raise_error(ArgumentError) - end.to write(%(Command "filtered" requires output_format: nquads, not ntriples)).to(:output) + describe "filter" do + context "complains if filtered command is attempted" do + before(:each) {RDF::CLI::COMMANDS.delete(:filtered)} + + it "single value" do + RDF::CLI.add_command(:filtered, filter: {output_format: :nquads}) + expect do + expect do + RDF::CLI.exec(["filtered"], output_format: :ntriples) + end.to raise_error(ArgumentError) + end.to write(%(Command "filtered" requires compatible value for output_format, not ntriples)).to(:output) + end + + it "Array of values" do + RDF::CLI.add_command(:filtered, filter: {output_format: %i{nquads turtle}}) + expect do + expect do + RDF::CLI.exec(["filtered"], output_format: :ntriples) + end.to raise_error(ArgumentError) + end.to write(%(Command "filtered" requires output_format in ["nquads", "turtle"], not ntriples)).to(:output) + end + + it "Proc" do + RDF::CLI.add_command(:filtered, filter: {output_format: ->(v) {v == :nquads}}) + expect do + expect do + RDF::CLI.exec(["filtered"], output_format: :ntriples) + end.to raise_error(ArgumentError) + end.to write(%(Command "filtered" output_format inconsistent with ntriples)).to(:output) + end + end end it "uses repository specified in command" do