lib/active_triples/rdf_source.rb in active-triples-0.8.1 vs lib/active_triples/rdf_source.rb in active-triples-0.8.2

- old
+ new

@@ -1,7 +1,9 @@ +# frozen_string_literal: true require 'active_model' require 'active_support/core_ext/hash' +require 'active_support/core_ext/array/wrap' module ActiveTriples ## # Defines a concern for managing {RDF::Graph} driven Resources as discrete, # stateful graphs using ActiveModel-style objects. @@ -32,22 +34,21 @@ # example of the RDF source concept as defined in the LDP specification # # An `RDFSource` is an {RDF::Term}---it can be used as a subject, predicate, # object, or context in an {RDF::Statement}. # - # @todo complete RDF::Value/RDF::Term/RDF::Resource interfaces + # @todo complete RDF::Value/RDF::Term/RDF::Resource interfaces # # @see ActiveModel # @see RDF::Resource # @see RDF::Queryable module RDFSource extend ActiveSupport::Concern include NestedAttributes include Persistable include Properties - include Reflection include RDF::Value include RDF::Queryable include ActiveModel::Validations include ActiveModel::Conversion include ActiveModel::Serialization @@ -72,11 +73,11 @@ end define_model_callbacks :persist end - delegate :each, :load!, :count, :has_statement?, :to => :graph + 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 @@ -100,11 +101,11 @@ set_subject!(resource_uri) if resource_uri reload # Append type to graph if necessary. - Array(self.class.type).each do |type| + Array.wrap(self.class.type).each do |type| unless self.get_values(:type).include?(type) self.get_values(:type) << type end end end @@ -122,62 +123,77 @@ def ==(other) other == to_term end ## - # Delegate parent to the persistence strategy if possible + # Gives a hash containing both the registered and unregistered attributes of + # the resource. Unregistered attributes are given with full URIs. + # + # @example + # class WithProperties + # include ActiveTriples::RDFSource + # property :title, predicate: RDF::Vocab::DC.title + # property :creator, predicate: RDF::Vocab::DC.creator, + # class_name: 'Agent' + # end # - # @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 - end + # class Agent; include ActiveTriples::RDFSource; end + # + # resource = WithProperties.new + # + # resource.attributes + # # => {"id"=>"g47123700054720", "title"=>[], "creator"=>[]} + # + # resource.creator.build + # resource.title << ['Comet in Moominland', 'Christmas in Moominvalley'] - ## - # @todo deprecate/remove - # @see #parent - def parent=(parent) - persistence_strategy.respond_to?(:parent=) ? (persistence_strategy.parent = parent) : nil - end - + # resource.attributes + # # => {"id"=>"g47123700054720", + # # "title"=>["Comet in Moominland", "Christmas in Moominvalley"], + # # "creator"=>[#<Agent:0x2adbd76f1a5c(#<Agent:0x0055b7aede34b8>)>]} + # + # resource << [resource, RDF::Vocab::DC.relation, 'Helsinki'] + # # => {"id"=>"g47123700054720", + # # "title"=>["Comet in Moominland", "Christmas in Moominvalley"], + # # "creator"=>[#<Agent:0x2adbd76f1a5c(#<Agent:0x0055b7aede34b8>)>], + # # "http://purl.org/dc/terms/relation"=>["Helsinki"]}]} + # + # @return [Hash<String, Array<Object>>] + # + # @todo: should this, `#attributes=`, and `#serializable_hash` be moved out + # into a dedicated `Serializer` object? def attributes attrs = {} - attrs['id'] = id if id + attrs['id'] = id fields.map { |f| attrs[f.to_s] = get_values(f) } unregistered_predicates.map { |uri| attrs[uri.to_s] = get_values(uri) } attrs end - def serializable_hash(options = nil) - attrs = (fields.map { |f| f.to_s }) << 'id' - hash = super(:only => attrs) - unregistered_predicates.map { |uri| hash[uri.to_s] = get_values(uri) } - hash - end - - ## - # @return [Class] gives `self#class` - def reflections - self.class - end - def attributes=(values) raise ArgumentError, "values must be a Hash, you provided #{values.class}" unless values.kind_of? Hash values = values.with_indifferent_access id = values.delete(:id) - set_subject!(id) if id && node? + set_subject!(id) if node? values.each do |key, value| - if reflections.reflect_on_property(key) + if reflections.has_property?(key) set_value(rdf_subject, key, value) elsif nested_attributes_options.keys.map { |k| "#{k}_attributes" }.include?(key) send("#{key}=".to_sym, value) else 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) + unregistered_predicates.map { |uri| hash[uri.to_s] = get_values(uri) } + hash + end + ## # Returns a serialized string representation of self. # Extends the base implementation builds a JSON-LD context if the # specified format is :jsonld and a context is provided by # #jsonld_context @@ -193,32 +209,63 @@ end super end ## - # Gives the representation of this + # Delegate parent to the persistence strategy if possible # + # @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 + end + + ## + # @todo deprecate/remove + # @see #parent + def parent=(parent) + persistence_strategy.respond_to?(:parent=) ? (persistence_strategy.parent = parent) : nil + end + + ## + # Gives the representation of this RDFSource as an RDF::Term + # # @return [RDF::URI, RDF::Node] the URI that identifies this `RDFSource`; # or a bnode identifier # # @see RDF::Term#to_term def rdf_subject @rdf_subject ||= RDF::Node.new end alias_method :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 + # 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 + # 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 end ## # @return [RDF::URI] the uri def to_uri - uri? ? rdf_subject : NullURI.new + rdf_subject if uri? end ## # @return [String] # @@ -249,100 +296,199 @@ def base_uri self.class.base_uri end def type - self.get_values(:type).to_a + self.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)) end ## # Looks for labels in various default fields, prioritizing # configured label fields. + # + # @see #default_labels def rdf_label - labels = Array(self.class.rdf_label) + labels = Array.wrap(self.class.rdf_label) labels += default_labels labels.each do |label| values = get_values(label) return values unless values.empty? 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. # + # @example # osu = new('http://dbpedia.org/resource/Oregon_State_University') # osu.fetch # osu.rdf_label.first # # => "Oregon State University" # - # @return [ActiveTriples::Entity] self - def fetch - load(rdf_subject) + # @example with default action block + # my_source = new('http://example.org/dead_url') + # my_source.fetch { |obj| obj.status = 'dead link' } + # + # @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) + 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 e + end + end self end ## - # Adds or updates a property with supplied values. + # Adds or updates a property by creating triples for each of the supplied + # values. # - # Handles two argument patterns. The recommended pattern is: - # set_value(property, values) + # The `property` argument may be either a symbol representing a registered + # property name, or an RDF::Term to use as the predicate. # - # For backwards compatibility, there is support for explicitly - # passing the rdf_subject to be used in the statement: - # set_value(uri, property, values) + # @example setting with a property name + # class Thing + # include ActiveTriples::RDFSource + # property :creator, predicate: RDF::DC.creator + # end # - # @note This method will delete existing statements with the correct subject and predicate from the graph + # t = Thing.new + # t.set_value(:creator, 'Tove Jansson') # => ['Tove Jansson'] + # + # + # @example setting with a predicate + # t = Thing.new + # t.set_value(RDF::DC.creator, 'Tove Jansson') # => ['Tove Jansson'] + # + # + # The recommended pattern, which sets properties directly on this + # RDFSource, is: `set_value(property, values)` + # + # @overload set_value(property, values) + # Updates the values for the property, using this RDFSource as the subject + # + # @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 + # 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, #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 + # coerced to an {RDF::Literal} or {RDF::Node} by {RDF::Statement} + # + # @return [ActiveTriples::Relation] an array {Relation} containing the + # values of the property + # + # @raise [ActiveTriples::Relation::ValueError] when the given value can't be + # 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 + # 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)" end values = args.pop get_relation(args).set(values) end ## - # Adds or updates a property with supplied values. - # - # @note This method will delete existing statements with the correct subject and predicate from the graph - def []=(uri_or_term_property, value) - self[uri_or_term_property].set(value) - end - - ## # Returns an array of values belonging to the property # requested. Elements in the array may RdfResource objects or a # valid datatype. # - # Handles two argument patterns. The recommended pattern is: + # Handles two argument patterns. The recommended pattern, which accesses + # properties directly on this RDFSource, is: # get_values(property) # - # For backwards compatibility, there is support for explicitly - # passing the rdf_subject to be used in th statement: - # get_values(uri, 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 + # values of the property + # + # @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) end ## - # Returns an array of values belonging to the property - # requested. Elements in the array may RdfResource objects or a - # valid datatype. - def [](uri_or_term_property) - get_relation([uri_or_term_property]) + # 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]) 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. + # + # @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 + + ## + # @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}"] end @@ -376,16 +522,10 @@ self << RDF::Statement.new(statement.subject, statement.predicate, rdf_subject) end end end - def destroy_child(child) - statements.each do |statement| - delete_statement(statement) if statement.subject == child.rdf_subject || statement.object == child.rdf_subject - end - end - ## # Indicates if the record is 'new' (has not yet been persisted). # # @return [Boolean] def new_record? @@ -400,15 +540,25 @@ @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 ## + # 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 } @@ -440,15 +590,15 @@ preds << RDF.type predicates.select { |p| !preds.include? p } end def default_labels - [RDF::SKOS.prefLabel, - RDF::DC.title, + [RDF::Vocab::SKOS.prefLabel, + RDF::Vocab::DC.title, RDF::RDFS.label, - RDF::SKOS.altLabel, - RDF::SKOS.hiddenLabel] + RDF::Vocab::SKOS.altLabel, + RDF::Vocab::SKOS.hiddenLabel] 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 @@ -492,10 +642,10 @@ new(uri, *args) end ## # 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|