lib/active_triples/rdf_source.rb in active-triples-0.10.2 vs lib/active_triples/rdf_source.rb in active-triples-0.11.0

- old
+ new

@@ -73,12 +73,12 @@ end define_model_callbacks :persist end - delegate :query, :each, :load!, :count, :has_statement?, :to => :graph - delegate :to_base, :term?, :escape, :to => :to_term + delegate :query, :each, :load!, :count, :has_statement?, to: :graph + delegate :to_base, :term?, :escape, to: :to_term ## # Initialize an instance of this resource class. Defaults to a # blank node subject. In addition to RDF::Graph parameters, you # can pass in a URI and/or a parent to build a resource from a @@ -91,25 +91,23 @@ # @todo move this logic out to a Builder? def initialize(*args, &block) resource_uri = args.shift unless args.first.is_a?(Hash) @rdf_subject = get_uri(resource_uri) if resource_uri - unless args.first.is_a?(Hash) || args.empty? + if args.first.is_a?(Hash) || args.empty? + set_persistence_strategy(RepositoryStrategy) + else set_persistence_strategy(ParentStrategy) persistence_strategy.parent = args.shift - else - set_persistence_strategy(RepositoryStrategy) end - @graph = RDF::Graph.new(*args, &block) + persistence_strategy.graph = RDF::Graph.new(*args, &block) reload # Append type to graph if necessary. Array.wrap(self.class.type).each do |type| - unless self.get_values(:type).include?(type) - self.get_values(:type) << type - end + get_values(:type) << type unless get_values(:type).include?(type) end end ## # Compares self to other for {RDF::Term} equality. @@ -126,16 +124,16 @@ end ## # Gives a hash containing both the registered and unregistered attributes of # the resource. Unregistered attributes are given with full URIs. - # - # @example + # + # @example # class WithProperties # include ActiveTriples::RDFSource # property :title, predicate: RDF::Vocab::DC.title - # property :creator, predicate: RDF::Vocab::DC.creator, + # property :creator, predicate: RDF::Vocab::DC.creator, # class_name: 'Agent' # end # # class Agent; include ActiveTriples::RDFSource; end # @@ -144,11 +142,11 @@ # resource.attributes # # => {"id"=>"g47123700054720", "title"=>[], "creator"=>[]} # # resource.creator.build # resource.title << ['Comet in Moominland', 'Christmas in Moominvalley'] - + # # resource.attributes # # => {"id"=>"g47123700054720", # # "title"=>["Comet in Moominland", "Christmas in Moominvalley"], # # "creator"=>[#<Agent:0x2adbd76f1a5c(#<Agent:0x0055b7aede34b8>)>]} # @@ -169,29 +167,37 @@ unregistered_predicates.map { |uri| attrs[uri.to_s] = get_values(uri) } attrs end def attributes=(values) - raise ArgumentError, "values must be a Hash, you provided #{values.class}" unless values.kind_of? Hash + raise(ArgumentError, "values must be a Hash. Got: #{values.class}") unless + values.is_a? Hash + values = values.with_indifferent_access id = values.delete(:id) - set_subject!(id) if node? + set_subject!(id) if node? && id && get_uri(id).uri? + values.each do |key, value| if reflections.has_property?(key) - set_value(rdf_subject, key, value) - elsif nested_attributes_options.keys.map { |k| "#{k}_attributes" }.include?(key) + set_value(key, value) + elsif nested_attributes_options + .keys.any? { |k| key == "#{k}_attributes" } send("#{key}=".to_sym, value) else - raise ArgumentError, "No association found for name `#{key}'. Has it been defined yet?" + raise ArgumentError, "No association found for name `#{key}'. " \ + 'Has it been defined yet?' end end end - def serializable_hash(options = nil) - attrs = (fields.map { |f| f.to_s }) << 'id' - hash = super(:only => attrs) + ## + # @return [Hash] + def serializable_hash(*) + attrs = fields.map(&:to_s) << 'id' + hash = super(only: attrs) unregistered_predicates.map { |uri| hash[uri.to_s] = get_values(uri) } + hash end ## # Returns a serialized string representation of self. @@ -202,31 +208,37 @@ # @see RDF::Enumerable#dump # # @param args [Array<Object>] # @return [String] def dump(*args) - if args.first == :jsonld and respond_to?(:jsonld_context) + if args.first == :jsonld && respond_to?(:jsonld_context) args << {} unless args.last.is_a?(Hash) args.last[:context] ||= jsonld_context end super end ## # Delegate parent to the persistence strategy if possible # - # @todo establish a better pattern for this. `#parent` has been a public + # @todo establish a better pattern for this. `#parent` has been a public # method in the past, but it's probably time to deprecate it. def parent - persistence_strategy.respond_to?(:parent) ? persistence_strategy.parent : nil + return persistence_strategy.parent if + persistence_strategy.respond_to?(:parent) + + nil end ## # @todo deprecate/remove # @see #parent def parent=(parent) - persistence_strategy.respond_to?(:parent=) ? (persistence_strategy.parent = parent) : nil + return persistence_strategy.parent = parent if + persistence_strategy.respond_to?(:parent=) + + nil end ## # Gives the representation of this RDFSource as an RDF::Term # @@ -235,27 +247,27 @@ # # @see RDF::Term#to_term def rdf_subject @rdf_subject ||= RDF::Node.new end - alias_method :to_term, :rdf_subject + alias to_term rdf_subject ## # Returns `nil` as the `graph_name`. This behavior mimics an `RDF::Graph` # with no graph name, or one without named graph support. # - # @note: it's possible to think of an `RDFSource` as "supporting named + # @note: it's possible to think of an `RDFSource` as "supporting named # graphs" in the sense that the `#rdf_subject` is an implied graph name. - # For RDF.rb's purposes, however, it has a nil graph name: when + # For RDF.rb's purposes, however, it has a nil graph name: when # enumerating statements, we treat them as triples. # # @return [nil] # @sse RDF::Graph.graph_name def graph_name nil end - + ## # @return [String] A string identifier for the resource; '' if the # resource is a node def humanize node? ? '' : rdf_subject.to_s @@ -297,16 +309,19 @@ def base_uri self.class.base_uri end def type - self.get_values(:type) + get_values(:type) end def type=(type) - raise "Type must be an RDF::URI" unless type.kind_of? RDF::URI - self.update(RDF::Statement.new(rdf_subject, RDF.type, type)) + raise(ArgumentError, + "Type must be an RDF::URI. Got: #{type.class}, #{type}") unless + type.is_a? RDF::URI + + update(RDF::Statement.new(rdf_subject, RDF.type, type)) end ## # Looks for labels in various default fields, prioritizing # configured label fields. @@ -321,20 +336,10 @@ end node? ? [] : [rdf_subject.to_s] end ## - # @return [Array<RDF::URI>] a group of properties to use for default labels. - def default_labels - [RDF::SKOS.prefLabel, - RDF::DC.title, - RDF::RDFS.label, - RDF::SKOS.altLabel, - RDF::SKOS.hiddenLabel] - end - - ## # Load data from the #rdf_subject URI. Retrieved data will be # parsed into the Resource's graph from available RDF::Readers # and available from property accessors if if predicates are # registered. # @@ -351,19 +356,19 @@ # @yield gives self to block if this is a node, or an error is raised during # load # @yieldparam [ActiveTriples::RDFSource] resource self # # @return [ActiveTriples::RDFSource] self - def fetch(*args, &block) + def fetch(*args, &_block) begin load(rdf_subject, *args) rescue => e if block_given? yield(self) else - raise "#{self} is a blank node; Cannot fetch a resource without a URI" if - node? + raise "#{self} is a blank node; " \ + 'Cannot fetch a resource without a URI' if node? raise e end end self end @@ -402,16 +407,16 @@ # or a single value. If not an {RDF::Resource}, the values will be # coerced to an {RDF::Literal} or {RDF::Node} by {RDF::Statement} # # @overload set_value(subject, property, values) # Updates the values for the property, using the given term as the subject - # - # @param [RDF::Term] subject the term representing the + # + # @param [RDF::Term] subject the term representing the # @param [RDF::Term, #to_sym] property a symbol with the property name # or an RDF::Term to use as a predicate. # @param [Array<RDF::Resource>, RDF::Resource] values an array of values - # or a single value. If not an {RDF::Resource}, the values will be + # or a single value. If not an {RDF::Resource}, the values will be # coerced to an {RDF::Literal} or {RDF::Node} by {RDF::Statement} # # @return [ActiveTriples::Relation] an array {Relation} containing the # values of the property # @@ -419,20 +424,21 @@ # coerced into an acceptable `RDF::Term`. # # @note This method will delete existing statements with the given # subject and predicate from the graph # - # @see http://www.rubydoc.info/github/ruby-rdf/rdf/RDF/Statement For - # documentation on {RDF::Statement} and the handling of + # @see http://www.rubydoc.info/github/ruby-rdf/rdf/RDF/Statement For + # documentation on {RDF::Statement} and the handling of # non-{RDF::Resource} values. def set_value(*args) # Add support for legacy 3-parameter syntax if args.length > 3 || args.length < 2 - raise ArgumentError, "wrong number of arguments (#{args.length} for 2-3)" + raise ArgumentError, + "wrong number of arguments (#{args.length} for 2-3)" end values = args.pop - get_relation(args).set(values) + get_values(*args).set(values) end ## # Returns an array of values belonging to the property # requested. Elements in the array may RdfResource objects or a @@ -440,60 +446,61 @@ # # Handles two argument patterns. The recommended pattern, which accesses # properties directly on this RDFSource, is: # get_values(property) # - # @overload get_values(property) + # @overload get_values(property) # Gets values on the RDFSource for the given property # @param [String, #to_term] property the property for the values # # @overload get_values(uri, property) # For backwards compatibility, explicitly passing the term used as the # subject {ActiveTriples::Relation#rdf_subject} of the returned relation. # @param [RDF::Term] uri the term to use as the subject # @param [String, #to_term] property the property for the values # - # @return [ActiveTriples::Relation] an array {Relation} containing the + # @return [ActiveTriples::Relation] an array {Relation} containing the # values of the property # - # @todo should this raise an error when the property argument is not an + # @todo should this raise an error when the property argument is not an # {RDF::Term} or a registered property key? def get_values(*args) - get_relation(args) + @relation_cache ||= {} + rel = Relation.new(self, args) + @relation_cache["#{rel.send(:rdf_subject)}/#{rel.property}/#{rel.rel_args}"] ||= rel + @relation_cache["#{rel.send(:rdf_subject)}/#{rel.property}/#{rel.rel_args}"] end ## # Returns an array of values belonging to the property requested. Elements # in the array may RdfResource objects or a valid datatype. - # + # # @param [RDF::Term, :to_s] term_or_property def [](term_or_property) - get_relation([term_or_property]) + get_values(term_or_property) end ## # Adds or updates a property with supplied values. # # @param [RDF::Term, :to_s] term_or_property # @param [Array<RDF::Resource>, RDF::Resource] values an array of values - # or a single value to set the property to. + # or a single value to set the property to. # - # @note This method will delete existing statements with the correct + # @note This method will delete existing statements with the correct # subject and predicate from the graph def []=(term_or_property, value) self[term_or_property].set(value) end ## + # @deprecated for removal in 1.0; use `#get_values` instead. # @see #get_values - # @todo deprecate and remove? this is an alias to `#get_values` def get_relation(args) - reload if (persistence_strategy.respond_to? :loaded?) && !persistence_strategy.loaded? - @relation_cache ||= {} - rel = Relation.new(self, args) - @relation_cache["#{rel.send(:rdf_subject)}/#{rel.property}/#{rel.rel_args}"] ||= rel - @relation_cache["#{rel.send(:rdf_subject)}/#{rel.property}/#{rel.rel_args}"] + warn 'DEPRECATION: `ActiveTriples::RDFSource#get_relation` will be' \ + 'removed in 1.0; use `#get_values` instead.' + get_values(*args) end ## # Set a new rdf_subject for the resource. # @@ -506,17 +513,16 @@ # @raise if the current subject is not a blank node, # and returns false if it can't figure out how to make a URI from # the param. Otherwise it creates a URI for the resource and # rebuilds the graph with the updated URI. def set_subject!(uri_or_str) - raise "Refusing update URI when one is already assigned!" unless + raise 'Refusing to update URI when one is already assigned!' unless node? || rdf_subject == RDF::URI(nil) - - return false if uri_or_str.nil? || - (uri_or_str.to_s.empty? && - !uri_or_str.kind_of?(RDF::URI)) - + + return if uri_or_str.nil? || + (uri_or_str.to_s.empty? && !uri_or_str.is_a?(RDF::URI)) + new_subject = get_uri(uri_or_str) rewrite_statement_uris(rdf_subject, new_subject) @rdf_subject = new_subject end @@ -524,11 +530,11 @@ ## # Indicates if the record is 'new' (has not yet been persisted). # # @return [Boolean] def new_record? - not persisted? + !persisted? end def mark_for_destruction @marked_for_destruction = true end @@ -537,130 +543,77 @@ @marked_for_destruction end private - ## - # This gives the {RDF::Graph} which represents the current state of this - # resource. - # - # @return [RDF::Graph] the underlying graph representation of the - # `RDFSource`. - # - # @see http://www.w3.org/TR/2014/REC-rdf11-concepts-20140225/#change-over-time - # RDF Concepts and Abstract Syntax comment on "RDF source" - def graph - @graph - end + ## + # @return [Array<RDF::URI>] a group of properties to use for default labels. + def default_labels + [RDF::Vocab::SKOS.prefLabel, + RDF::Vocab::DC.title, + RDF::RDFS.label, + RDF::Vocab::SKOS.altLabel, + RDF::Vocab::SKOS.hiddenLabel] + end - ## + ## + # Rewrites the subject and object of each statement containing + # `old_subject` in either position. Used when setting the subject to + # remove the placeholder blank node subjects. + # + # @param [RDF::Term] old_subject + # @param [RDF::Term] new_subject + # @return [void] + def rewrite_statement_uris(old_subject, new_subject) + graph.query(subject: old_subject).each do |st| + graph.delete(st) - # Lists fields registered as properties on the object. - # - # @return [Array<Symbol>] the list of registered properties. - def fields - properties.keys.map(&:to_sym).reject{ |x| x == :type } + st.subject = new_subject + st.object = new_subject if st.object == old_subject + graph.insert(st) end - ## - # Returns the properties registered and their configurations. - # - # @return [ActiveSupport::HashWithIndifferentAccess{String => ActiveTriples::NodeConfig}] - def properties - _active_triples_config - end + graph.query(object: old_subject).each do |st| + graph.delete(st) - ## - # List of RDF predicates registered as properties on the object. - # - # @return [Array<RDF::URI>] - def registered_predicates - properties.values.map { |config| config.predicate } + st.object = new_subject + graph.insert(st) end + end - ## - # List of RDF predicates used in the Resource's triples, but not - # mapped to any property or accessor methods. - # - # @return [Array<RDF::URI>] - def unregistered_predicates - registered_preds = registered_predicates - registered_preds << RDF.type - unregistered_preds = [] - - query(subject: rdf_subject) do |stmt| - unregistered_preds << stmt.predicate unless - registered_preds.include? stmt.predicate - end + ## + # Takes a URI or String and aggressively tries to convert it into + # an RDF term. If a String is given, first tries to interpret it + # as a valid URI, then tries to append it to base_uri. Finally, + # raises an error if no valid term can be built. + # + # The argument must be an RDF::Node, an object that responds to + # #to_uri, a String that represents a valid URI, or a String that + # appends to the Resource's base_uri to create a valid URI. + # + # @TODO: URI.scheme_list is naive and incomplete. Find a better + # way to check for an existing scheme. + # + # @param uri_or_str [RDF::Resource, String] + # + # @return [RDF::Resource] A term + # @raise [RuntimeError] no valid RDF term could be built + def get_uri(uri_or_str) + return uri_or_str.to_term if uri_or_str.respond_to? :to_term - unregistered_preds - end + uri_or_node = RDF::Resource.new(uri_or_str) + return uri_or_node if uri_or_node.valid? - def default_labels - [RDF::Vocab::SKOS.prefLabel, - RDF::Vocab::DC.title, - RDF::RDFS.label, - RDF::Vocab::SKOS.altLabel, - RDF::Vocab::SKOS.hiddenLabel] - end + uri_or_str = uri_or_str.to_s + return RDF::URI.intern(base_uri.to_s) / uri_or_str if + base_uri && !uri_or_str.start_with?(base_uri.to_s) - ## - # Rewrites the subject and object of each statement containing - # `old_subject` in either position. Used when setting the subject to - # remove the placeholder blank node subjects. - # - # @param [RDF::Term] old_subject - # @param [RDF::Term] new_subject - # @return [void] - def rewrite_statement_uris(old_subject, new_subject) - graph.transaction(mutable: true) do |tx| - tx.query(subject: old_subject).each do |st| - tx.delete(st) + raise "could not make a valid RDF::URI from #{uri_or_str}" + end - st.subject = new_subject - st.object = new_subject if st.object == old_subject - tx.insert(st) - end - - tx.query(object: old_subject).each do |st| - tx.delete(st) - - st.object = new_subject - tx.insert(st) - end - end - end - - ## - # Takes a URI or String and aggressively tries to convert it into - # an RDF term. If a String is given, first tries to interpret it - # as a valid URI, then tries to append it to base_uri. Finally, - # raises an error if no valid term can be built. - # - # The argument must be an RDF::Node, an object that responds to - # #to_uri, a String that represents a valid URI, or a String that - # appends to the Resource's base_uri to create a valid URI. - # - # @TODO: URI.scheme_list is naive and incomplete. Find a better - # way to check for an existing scheme. - # - # @param uri_or_str [RDF::Resource, String] - # - # @return [RDF::Resource] A term - # @raise [RuntimeError] no valid RDF term could be built - def get_uri(uri_or_str) - return uri_or_str.to_term if uri_or_str.respond_to? :to_term - return uri_or_str if uri_or_str.is_a? RDF::Node - uri_or_node = RDF::Resource.new(uri_or_str) - return uri_or_node if uri_or_node.valid? - uri_or_str = uri_or_str.to_s - return RDF::URI(base_uri.to_s) / uri_or_str if base_uri && !uri_or_str.start_with?(base_uri.to_s) - raise RuntimeError, "could not make a valid RDF::URI from #{uri_or_str}" - end - - public - + ## + # Class methods for RDFSource, included via ActiveSupport module ClassMethods ## # Adapter for a consistent interface for creating a new Resource # from a URI. Similar functionality should exist in all objects # which can become a Resource. @@ -677,14 +630,12 @@ # Apply a predicate mapping using a given strategy. # # @param [ActiveTriples::Schema, #properties] schema A schema to apply. # @param [#apply!] strategy A strategy for applying. Defaults # to ActiveTriples::ExtensionStrategy - def apply_schema(schema, strategy=ActiveTriples::ExtensionStrategy) - schema.properties.each do |property| - strategy.apply(self, property) - end + def apply_schema(schema, strategy = ActiveTriples::ExtensionStrategy) + schema.properties.each { |property| strategy.apply(self, property) } end ## # Test if the rdf_subject that would be generated using a # specific ID is already in use in the triplestore. @@ -695,11 +646,12 @@ # use in the triplestore; otherwise, false. # NOTE: If the ID is in use in an object not yet # persisted, false will be returned presenting # a window of opportunity for an ID clash. def id_persisted?(test_id) - rdf_subject = self.new(test_id).rdf_subject + rdf_subject = new(test_id).rdf_subject + ActiveTriples::Repositories.has_subject?(rdf_subject) end ## # Test if the rdf_subject that would be generated using a @@ -711,11 +663,12 @@ # use in the triplestore; otherwise, false. # NOTE: If the URI is in use in an object not yet # persisted, false will be returned presenting # a window of opportunity for an ID clash. def uri_persisted?(test_uri) - rdf_subject = test_uri.kind_of?(RDF::URI) ? test_uri : RDF::URI(test_uri) - ActiveTriples::Repositories.has_subject?(rdf_subject) + test_uri = RDF::URI.intern(test_uri) unless test_uri.is_a?(RDF::URI) + + ActiveTriples::Repositories.has_subject?(test_uri) end end end end