[Activerdf] RoR and ActiveRDF integration, plus some extensions

Jean-Francois Cloutier jean.f.cloutier at gmail.com
Sat Jul 15 16:02:52 IST 2006


Hi,

I made some extensions and modifications to ActiveRDF about a month ago in
an attempt to

1- better integrate it with Rails: testing (with Rake and RDF fixtures),
callbacks (similar to ActiveRecord) and validation

 2- add capabilities I needed :generating classes from schema into a module,
inheritance based on the schema class hierarchy and ability to add
(inherited) functionality to the generated classes.

However much I'd like to work with an RDF store on my current project, it
has become clear that I'll have to go with MySQL and ActiveRecord until
production-grade support for RDF is fully integrated into Rails. I am
hopeful that the work initiated by Hansson (Rails' creator) on
ActiveResource (google it), by extending it to support RDF resources, will
lead to fully integrated RDF support in Rails.

The following code is barely documented and minimally tested. Since I am
putting this work aside indefinitely, I thought I'd share it in its current
state. I hope someone will find it useful.

WARNING: The code is experimental and is not up to date with the latest
development release. Also I renamed a few things that were specific to my
project and I have not tested those changes. Don't expect this code to work
for you as is.

Best,
Jean-Francois

README.txt
-------------------

I made the following enhancements to activerdf

    - Optimized Ruby class generation from RDFS schema classes (activerdf
does not recreate the RDFS inheritance structure)
    - Added the option to create these classes in a dedicated module to
avoid name conflicts
    - Implemented the functional equivalent of inheritance between generated
classes via mixins
    - Added validation callbacks and all other ActiveRecord-style callbacks
on creation, saving and updating
    - Fixed the find method so that the instances retrieved are of the
correct, schema-generated classes

The RDF database used for the protoype thus far is YARS. YARS only accepts
Notation3 as input and as a query language. I had to write a simple shell
script to convert and massage an XML-RDFS file into a valid N3 encoding. (I
use Protege to build the schema and model and then save it as XML.-RDFS).

The Ruby source code:

    - activerdf_fixes.rb are fixes to the development build as of June 07
    - activerdf_extensions.rb are the actual extensions to activerdf
    - mixins.rb is sample code that is injected into generated classes when
faking inheritance


The extensions I introduced make a number of assumptions:

    - The YARS connection is established with class model generation turned
on and classes created within a "model module", say ACME (for example an
RDFS class called <http://domain.com/ACME/Foo> would yield the class
ACME::Foo, a subclass of IdentifiedResource)

    - When a model class is created, a sub-module of the model module is
mixed into the class to add class methods if module <model module>::<class
name>ClassMixin (e.g. ACME::FooClassMixin) is found.

    - When an instance of a model class is created, the instance is extended
by modules <model module>::<class name>Mixin where <class name> is the local
name of the instance's class (e.g. Foo) or the name of a class that ought to
be its superclass according to the RDFS schema (e.g.SuperFoo). This is how
RDFS-schema-directed inheritance is "faked" in the generated Ruby class
model. (Doing it right would require a significant rewrite of activerdf
which is actually planned by its developers)

    - the URIs of individuals are expected to follow the following pattern
<class URI>#<id> (e.g. http://domain.com/ACME/Foo#myfoo)

Shell script to convert RDFS-XML into NTriples
-------------------------------------------------------------------
#! /bin/sh

# parameters = directory (no trailing /) and file prefix (e.g. acme)

# assumes that the schema is saved as <prefix>_schema.rdfs (e.g.
acme_schema.rdfs)
# assumes that the model (individuals) is saved as <prefix>_model.rdfs
# requires the CWM python library

cd ~/tools/cwm
echo "Converting schema"
python cwm.py -rdf $1/$2_schema.rdfs -ntriples >  $1/$2_schema.pre
sed -e s/\"\"\"/\"/g $1/$2_schema.pre > $1/$2_schema.nt
rm $1/$2_schema.pre
echo "Converting model"
python cwm.py -rdf $1/$2_model.rdfs -ntriples >  $1/$2_model.pre
sed -e s/\"\"\"/\"/g $1/$2_model.pre > $1/$2_model.nt
rm $1/$2_model.pre


<railsAppDir>/config/environment.rb
----------------------------------------------------

# Be sure to restart your web server when you modify this file.

# Uncomment below to force Rails into production mode when
# you don't control web/app server and can't set it the proper way
# ENV['RAILS_ENV'] ||= 'production'

# Specifies gem version of Rails to use when vendor/rails is not present
RAILS_GEM_VERSION = '1.1.2'

# Bootstrap the Rails environment, frameworks, and default configuration
require File.join(File.dirname(__FILE__), 'boot')

Rails::Initializer.run do |config|
  # Settings in config/environments/* take precedence those specified here

  # Skip frameworks you're not going to use
  config.frameworks -= [ :action_web_service, :action_mailer, :active_record
]

  # Add additional load paths for your own custom dirs
  # config.load_paths += %W( #{RAILS_ROOT}/extras )
    config.load_paths += %W( "#{RAILS_ROOT}/lib/activerdf" )

  # Force all environments to use the same logger level
  # (by default production uses :info, the others :debug)
  # config.log_level = :debug

  # Use the database for sessions instead of the file system
  # (create the session table with 'rake db:sessions:create')
  # config.action_controller.session_store = :active_record_store

  # Use SQL instead of Active Record's schema dumper when creating the test
database.
  # This is necessary if your schema can't be completely dumped by the
schema dumper,
  # like if you have constraints or database-specific column types
  # config.active_record.schema_format = :sql

  # Activate observers that should always be running
  # config.active_record.observers = :cacher, :garbage_collector

  # Make Active Record use UTC-base instead of local time
  # config.active_record.default_timezone = :utc

  # See Rails::Configuration for more options

    # ActiveRDF config
    require 'activerdf/active_rdf'
    require 'activerdf_fixes'
    require 'activerdf_extensions'
    require 'mixins'
end

# Add new inflection rules using the following format
# (all these examples are active by default):
# Inflector.inflections do |inflect|
#   inflect.plural /^(ox)$/i, '\1en'
#   inflect.singular /^(ox)en/i, '\1'
#   inflect.irregular 'person', 'people'
#   inflect.uncountable %w( fish sheep )
# end

# Include your application configuration below




<railsAppDir>/config/development.rb
----------------------------------------------------

# Settings specified here will take precedence over those in
config/environment.rb

# In the development environment your application's code is reloaded on
# every request.  This slows down response time but is perfect for
development
# since you don't have to restart the webserver when you make code changes.
config.cache_classes = false

# Log error messages when you accidentally call methods on nil.
config.whiny_nils = true

# Enable the breakpoint server that script/breakpointer connects to
config.breakpoint_server = true

# Show full error reports and disable caching
config.action_controller.consider_all_requests_local = true
config.action_controller.perform_caching             = false
config.action_view.cache_template_extensions         = false
config.action_view.debug_rjs                         = true

# Don't care if the mailer can't send
config.action_mailer.raise_delivery_errors = false

#ActiveRDF
#Load YARS configuration from yaml file
yaml = YAML::load_file File.join(File.dirname(config.environment_path),'
activerdf.yml')
yaml = yaml['development']
NodeFactory.connection yaml.symbolize_keys

<railsAppDir>/config/test.rb
---------------------------------------

# Settings specified here will take precedence over those in
config/environment.rb

# The test environment is used exclusively to run your application's
# test suite.  You never need to work with it otherwise.  Remember that
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs.  Don't rely on the data there!
config.cache_classes = true

# Log error messages when you accidentally call methods on nil.
config.whiny_nils = true

# Show full error reports and disable caching
config.action_controller.consider_all_requests_local = true
config.action_controller.perform_caching             = false

# Tell ActionMailer not to deliver emails to the real world.
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test

#ActiveRDF
#Load YARS configuration from yaml file
yaml = YAML::load_file File.join(File.dirname(config.environment_path),'
activerdf.yml')
yaml = yaml['test']
NodeFactory.connection yaml.symbolize_keys
# Dirty patch to bug in ActiveRDF...
# $logger = Logger.new '/tmp/activerdf.log', 'daily'
# $logger.level = Logger::DEBUG

<railsAppDir>/config/activerdf.yml
-------------------------------------------------

development:
  cache_server: !ruby/symbol memory
  adapter: !ruby/symbol yars
  context: 'yars'
  host: 'localhost'
  construct_class_model: true
  logger: '/tmp/activerdf.log'
  log_level: 0
  class_model_module: 'ACME'

test:
  cache_server: !ruby/symbol memory
  adapter: !ruby/symbol yars
  context: 'yars-test'
  host: 'localhost'
  construct_class_model: true
  logger: '/tmp/activerdf.log'
  log_level: 0
  class_model_module: 'ACME'




<railsAppDir>/app/activerdf_extensions.rb
--------------------------------------------------------------

###########################
# Extensions to ActiveRDF #
###########################

################### Extending NodeFactory ####################

# Adding a callback to model construction
module NodeFactoryMixin

    def construct_class_model_with_callback(module_name)
        construct_class_model_without_callback(module_name)
        class_model_constructed_callback(module_name)
    end

end

NodeFactory.extend(NodeFactoryMixin)

class NodeFactory

    # Adds class-level and instance-level mixins if defined to classes
generated from RDF schema
    # Only works if classes generated within a named module
    def self.class_model_constructed_callback(module_name)
        $logger.info "Callback: Model constructed in module #{module_name}"
        if module_name
            module_regexp = Regexp.new("^#{module_name}::")
            IdentifiedResource.subclasses.each do |klass_name|
                if klass_name =~ module_regexp
                    if eval("defined? #{klass_name}ClassMixin")
                        $logger.info "*** extending #{klass_name} with
#{klass_name}ClassMixin"
                        # p "*** extending #{klass_name} with
#{klass_name}ClassMixin"

eval("#{klass_name}.extend(#{klass_name}ClassMixin)")
                    else
                        # p "did NOT extend class #{klass_name} with
#{klass_name}ClassMixin"
                    end
                    superklass_names =
find_superclasses(eval(klass_name).class_URI).collect {|s|
"#{module_name}::#{s}"}
                    IdentifiedResource.set_superclass_names(klass_name,
superklass_names)
                end
            end
        end
    end

    # Efficiently finds all superclasses of a schema class
    def self.find_superclasses(uri)
        superclass_names = []
        qe = QueryEngine.new
        qe.add_binding_variables(:o)
        qe.add_condition(uri, NamespaceFactory.get(:rdfs,:subClassOf), :o)
        # Execute the query
        qe.execute.each do |o|
            unless superclass_names.include? o
                superclass_names << o.local_part
                superclass_names += find_superclasses(o)
            end
        end
      superclass_names.uniq.reverse # top-down
    end

    class << self
        alias_method :construct_class_model_without_callback,
:construct_class_model
        alias_method :construct_class_model,
:construct_class_model_with_callback
    end

end


################### IdentifiedResource extensions ####################

# IdentifiedResource does not create a class hierarchy of identified
resources that reflects the schema class hierarchy.
# This mixin module fixes that by creating a parallel hierarchy of modules
that are mixed into the generated classes.
# This module also  implements a callback framework similar to that of
ActiveRecord.
# There's also the beginning of a validation framework similar to
ActiveRecord.

# IMPORTANT: Assumes the URI pattern #{class_URI}#id for individuals

module IdentifiedResourceClassMixin

    def create_with_callbacks(uri)
        pre_create(uri)
        # Assumes the URI pattern #{class_URI}#id for individuals
        klass = get_class_from_uri(uri)
        individual = klass.create_without_callbacks(uri)
        individual.post_create
        individual
    end

end

# Add class methods
IdentifiedResource.extend(IdentifiedResourceClassMixin)

class IdentifiedResource

    @@superclass_names = {}
    @@class_uris = {}

    def self.get_class_uris()
        @@class_uris
    end

    def self.get_class_from_uri(uri)
        if uri =~ /^(.+)#.+$/
            class_uri = $1
            @@class_uris[class_uri] || IdentifiedResource
        else
            IdentifiedResource
        end
    end

    def self.set_superclass_names(klass_name, super_names)
        @@superclass_names[klass_name] = super_names
    end

    def self.get_superclass_names(klass_name)
        @@superclass_names[klass_name] || []
    end

    def self.remember_class_uri(klass)
        @@class_uris[klass.class_URI.uri] = klass
    end

    def self.pre_create(uri)
        before_create(uri)
    end

    # callback
    def self.before_create(uri)
        $logger.info "Before creating #{self.name} with URI #{uri}"
    end

    attr :errors
    attr :created

    # alias class methods
    class << self
            alias_method :create_without_callbacks, :create
            alias_method :create, :create_with_callbacks
    end

    def save_with_callbacks()
        pre_save
        save_without_callbacks
        post_save
    end

    alias_method :save_without_callbacks, :save
    alias_method :save, :save_with_callbacks

    def post_create()
      # $logger.info "was created with #{uri}"
        # Extend instance with all available mixins associated with RDFS
class and RDFS superclasses of instance
        # $logger.info IdentifiedResource.get_superclass_names(
self.class.to_s)
        (IdentifiedResource.get_superclass_names(self.class.to_s) <<
self.class.to_s).each do |sn|
            if eval("defined? #{sn}Mixin")
                eval("self.extend #{sn}Mixin")
                $logger.info "Extended instance of #{self.class} with
#{sn}Mixin"
            end
        end
        @created = true
        @errors = Hash.new
        after_create
    end

    def pre_save()
        before_validation
        validate
        after_validation
        if @created
            before_validation_on_create
            validate_on_create
            after_validation_on_create
        else
            before_validation_on_update
            validate_on_update
            after_validation_on_update
        end
        before_save
    end

    def post_save()
        @created = false
        after_save
    end

    ## Default noop callbacks

    def after_create()
        $logger.info "After creating #{self}"
    end

    def before_save()
        $logger.info "Before saving #{self}"
    end

    def after_save()
        $logger.info "After saving #{self}"
    end

    def validate()
        $logger.info "Validating #{self}"
    end

    def validate_on_create()
        $logger.info "Validating #{self} on create"
    end

    def validate_on_update()
        $logger.info "Validating #{self} on create"
    end

    def before_validation()
        $logger.info "Before validation of #{self}"
    end

    def after_validation()
        $logger.info "After validation of #{self}"
    end

    def before_validation_on_create()
        $logger.info "Before validation on create of #{self}"
    end

    def after_validation_on_create()
        $logger.info "After validation on create of #{self}"
    end

    def before_validation_on_update()
        $logger.info "Before validation on update of #{self}"
    end

    def after_validation_on_update()
        $logger.info "After validation on update of #{self}"
    end

end



<railsAppDir>/app/activerdf_fixes.rb
---------------------------------------------------

######################
# Fixes to ActiveRDF #
######################

module AttributesContainer

    # save all property values. we use self.predicates hash to know the
original
    # URIs of all predicates (we lost those when converting to attributes)
    def save_attributes()

        # If _attributes is nil, we need to load it from the DB
        if _attributes.nil?
            initialize_attributes
        end

        self.class.predicates.each do |attr_localname, attr_fullname|
            object = @_attributes[attr_localname]

            # to save the triple we need the full URI of the predicate
            predicate = attr_fullname

            # if an attribute is an array, we save all constituents
sequentially,
            # e.g. person.publications = ['article1', 'article2']

            # First, we remove all triples related to (subject, predicate)
only if
            # the attribute values have changed
            if not object.nil? and object[1]
                # [FIX] Ignore exception on removal
                begin
                    NodeFactory.connection.remove(self, predicate, nil)
                rescue
                    $logger.error "Remove #{predicate} failed: #{$!}"
                end
            end

            # then save the new value, if the value have changed and the
value is not nil
            if not object.nil? and object[1] and not object[0].nil? and
object[0].is_a?(Array)
                object[0].each do |realvalue|
                    NodeFactory.connection.add(self, predicate, realvalue)
                end
            elsif not object.nil? and object[1] and not object[0].nil?
                NodeFactory.connection.add(self, predicate, object[0])
            end
        end
    end

end

class NodeFactory

    # Initialise cache and connection to data source.  If no parameter
given, we
    # return the previous instantiated connection.
    #
    # Available parameters, possible values and default values
    # * +:adapter+ => :yars, :redland, :sparql, :jena (default :yars)
    # * +:construct_class_model+ => boolean (default true)
    # * +:class_model_module+ => string (default nil)
    # * +:construct_schema+ => boolean (default false)
    # * +:cache_server+ => 'host-url' or :memory (default :memory)
    # * +:proxy+ => 'proxy-url' or ProxyClass (default nil)
    # * +:logger+ => File (default tmpdir/activerdf.log)
    # * +:logger_level+ => Logger::Level (default Logger::FATAL)
    #
    # Hash of parameters for adapter yars:
    # * +:host+ => Host of the yars server
    # * +:port+ => y default 8080
    # * +:context+ => 'context-url', by default the root context
    # Hash of parameters for adapter redland:
    # * +:location+ => 'file-path' or :memory (default /tmp/test-store)
    def self.connection(params = {})

    # if no parameters given, and connection already established earlier,
return that connection
        if params.empty?
            return @@current_connection unless  @@current_connection.nil?
        end

    # use default parameters for the unspecified parameters
    params = default_parameters.merge(params)

    # setup the logger
        # [FIX] added second parameter
    $logger = Logger.new params[:logger] , 'daily'
    $logger.level = params[:logger_level]

        # Initialize cache system
    init_cache(params[:cache_server] || params[:host])

        # Initialize DB adapter
        connection = init_adapter(params)

        # Save the parameter
        @@default_host_parameters = params

        return connection
    end

    private

    # constructs the class model from a RDF dataset
    def self.construct_class_model(module_name)
        qe = QueryEngine.new
        qe.add_binding_variables :s
        qe.add_condition(:s, NamespaceFactory.get(:rdf,:type),
NamespaceFactory.get(:rdfs,:Class))
        all_types = qe.execute
        $logger.info "found #{all_types.size} types in #{connection.context
}"

        # [EXTENSION] Added support for generating classes from schema into
named module
        if module_name
            if eval "defined? #{module_name}"
                mod = eval "#{module_name}"
                # $logger.warn "Module #{module_name} is already defined"
            else
                mod = Object.module_eval "#{module_name} = Module.new"
                # $logger.info "Defining module #{module_name}"
            end
        else
            mod = Object
        end

        klasses = all_types.collect do |type|
            construct_class(mod, type, qe) if type.kind_of?(Resource)
        end
        ##for type in all_types do
        ##    unless type.kind_of?(Resource)
        ##        raise(NodeFactoryError, "received literal #{type} instead
as resource type")
        ##    end
        ##    klasses << construct_class(type, qe)
        ##end

        klasses.uniq.each do |klass|
            # and loading all attributes into the class
            get_class_attributes_from_data klass, qe
        end
    end

    # constructs a class from an RDF resource (using its local_name as class
name)
    # [EXTENSION] Support for generating classes from schema into named
module
    def self.construct_class(mod, type, qe)
        ## TODO: setup context inside class

        class_name  = type.to_class_name
        unless mod.const_defined? class_name.to_sym
            klass = mod.module_eval("#{class_name} =
Class.newIdentifiedResource")
            klass.set_class_uri type.uri

            # Remember mapping class -to -uri
            IdentifiedResource.remember_class_uri(klass)

            $logger.info "created class #{class_name}"
            klass
        else
            mod.const_get(class_name)
        end
    end

end

# Optimized finding predicates

class Resource

    @@predicates_cache = Hash.new
    @@thing_predicates = nil

    def self.find_thing_predicates()
        unless @@thing_predicates
            predicates = Hash.new
            preds = Resource.find({ NamespaceFactory.get(:rdfs,:domain) =>
NamespaceFactory.get(:owl,:Thing) })
             preds.each do |predicate|
                attribute = predicate.local_part
                predicates[attribute] = predicate.uri
                # $logger.debug "added OWL Thing predicate #{attribute}"
            end unless preds.nil?
            @@thing_predicates = predicates
        end
        @@thing_predicates
    end


    # Find all predicates for a resource from the schema definition
    # returns a hash containing from localname to full predicate URI (e.g.
    # 'firstName' => 'http://foaf.org/firstName')
    def self.find_predicates(class_uri)
        raise(ActiveRdfError, "In #{__FILE__}:#{__LINE__}, class uri is
nil.") if class_uri.nil?
        raise(ActiveRdfError, "In #{__FILE__}:#{__LINE__}, class uri is not
a Resource, it's a #{class_uri.class}.") unless
class_uri.kind_of?(IdentifiedResource)

        unless @@predicates_cache[class_uri]

            predicates = Hash.new

            preds = Resource.find({ NamespaceFactory.get(:rdfs,:domain) =>
class_uri })

             preds.each do |predicate|
                attribute = predicate.local_part
                predicates[attribute] = predicate.uri
                $logger.debug "found predicate #{attribute}"
            end unless preds.nil?

            predicates.update(find_thing_predicates)

            # Generate the query string
            qe = QueryEngine.new
            qe.add_binding_variables(:o)
            qe.add_condition(class_uri,
NamespaceFactory.get(:rdfs,:subClassOf),
:o)

            $logger.debug "Before call Find superpredicate for #{class_uri}"

            # Execute the query
            qe.execute.each do |o|
                $logger.debug "Find superpredicate #{o.to_s} for
#{class_uri}"
                superclass = o
                superpredicates = find_predicates(superclass)
                # $logger.debug "Found superpredicate : " +
superpredicates.inspect
                predicates.update(superpredicates)
            end
            $logger.debug "Find predicate result : " + predicates.inspect
            @@predicates_cache[class_uri] = predicates
        end # unless
        @@predicates_cache[class_uri]
    end
end




<railsAppDir>/app/mixins.rb
-----------------------------------------

####################################
# Resource class extension modules #
####################################

# Assuming that NodFactory generated classes from schema into ACME module
module ACME

    # Assuming a schema class named UniqueIDResource

    # Class mixin
    module UniqueIDResourceClassMixin

        def make_guid
            "#{Object::Time.now.to_f}#{rand}"
        end

    end

    # Instance mixin
    module UniqueIDResourceMixin

        def after_create
            super
            self.has_guid = ACME::UniqueIDResource.make_guid
            self.created_on = Object::Time.now.utc.strftime
('%Y-%m-%dT%H:%M:%S')
        end

    end

    # Assuming a schema class named TestResource as subclass of
UniqueIDResource

    module TestResourceMixin

        def after_create
            super
            $logger.info "After creating #{self.has_guid} in TestResource"
        end

    end

    module UserMixin

        def is_admin?
            is_admin == 'true'
        end

    end


end




<railsAppDir>/test/test_helper.rb
-----------------------------------------------

ENV["RAILS_ENV"] = "test"
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")

# From Rails Recipe, Rails without a database
# Instead of require test_help.rb which redefines setup and teardown for
databases, we cherry-pick.
require 'application'
require 'test/unit'
require 'action_controller/test_process'
require 'breakpoint'

class Test::Unit::TestCase

    # RDF fixtures

    @@rdf_fixtures = Hash.new

    alias __initialize__ initialize

    # Assign the fixtures to an instance variable of the same name
    def initialize(test)
        __initialize__(test)
        @@rdf_fixtures.each do |fixture, value|
                eval("@#{fixture} = {}")
                self.class.send(:attr_writer, fixture.to_sym)
                self.send("#{fixture}=", value)
        end
    end

    # Load the RDF fixtures in file <name>.yml in ./test/fixtures/rdf
    def self.rdf_fixtures(name)
        filename = File.expand_path(File.join(File.dirname(__FILE__),
"fixtures/rdf/#{name}.yml"))
        yaml = YAML::load_file filename
        @@rdf_fixtures[name] = yaml2rdf(yaml)
    end

    private

    # Process YAML file containing RDF resource specifications
    #    The YAML file defining the RDF fixtures has a simple format.
    #Example:
    #    ISNA:
    #        - 'http://mindalliance.com/isna'
    #        - User:
  #    admin:
  #      find_one:
  #        has_uid: 'admin'
  #    participant:
  #      create:
  #        has_uid: 'jd'
  #        has_password: 'secret'

    # In this example,

  #  * ISNA is the module name for the schema-generated classes
  #  * http... is the prefix for the model
  #  * User is the name of a class
  #  * admin and participant are the keys for the resources, allowing
@users[:admin] etc.
  #  * find_one and create are the methods by which the resources are
instantiated (retrieved or created).
  #        o find_all is also supported and retrieves into an Array all that
match the criteria.
  #  * has_uid, has_password etc. are resource properties to bet set on
creation or used as conditions for retrieval depending on the methods
specified.
    #Note that the RDF fixtures file can define multiple modules and, in
each one, multiple classes with their instances. The names of the instances
must be unique.
    def self.yaml2rdf(yaml)
        hash = Hash.new
        begin
            yaml.each do |module_name, module_content|
                uri = module_content[0]
                class_sets = module_content[1]
                class_sets.each do |class_name, individuals|
                    individuals.each do |name, individual|
                        action = individual.keys[0]
                        case action
                        when 'create'
                            hash[name] = create_rdf_individual(module_name,
uri, class_name, individual[action])
                        when 'find_one'
                            inds = find_rdf_individual(module_name, uri,
class_name, individual[action])
                            if inds.size != 1
                                raise "More than one individual found"
                            else
                                hash[name] = inds[0]
                            end
                        when 'find_all'
                            hash[name] = find_rdf_individual(module_name,
uri, class_name, individual[action])
                        else
                            raise "Invalid action #{action}"
                        end
                    end
                end
            end
        rescue
            raise "Invalid fixture: #{$!}"
        end
        hash.symbolize_keys!
    end

    # Create rdf resource and initialize properties in params
    def self.create_rdf_individual(module_name, uri, class_name, params)
        klass = eval("#{module_name}::#{class_name}")
        ind = klass.create("#{uri}/#{class_name}##{rand}")
        if params
            params.each do |predicate, value|
                method_name = predicate + '='
                ind.send(method_name.intern, value)
            end
        end
        ind
    end

    # Find all resources that meet conditions in params
    def self.find_rdf_individual(module_name, uri, class_name, params)
        klass = eval("#{module_name}::#{class_name}")
        conditions = Hash.new
        if params
            params.each do |predicate, value|
                conditions[predicate.intern] = Literal.create(value)
            end
        end
        klass.find(conditions)
    end

end




<railsAppDir>/test/unit/user_test.rb
-----------------------------------------------------

# Assumes a schema class User from which a class User was generated in
module ACME

require File.dirname(__FILE__) + '/../test_helper'
class UserTest < Test::Unit::TestCase

    rdf_fixtures :users

    def setup
        @admin = @users[:admin]
        @participant = @users[:participant]
        @all = @users[:all]
    end

# Tests that single fixture was retrieved (find_one)
    def test_found_one
        assert_kind_of ACME::User, @admin
    end

# Test that fixture was properly created
    def test_created
        assert_kind_of ACME::User, @participant
        assert_equal @participant.has_uid, 'jd'
        assert_equal @participant.has_password, 'secret'
    end

# Test that all fixtures were retrieved (find_all)
    def test_all_found
        assert_kind_of Array, @all
        @all.each do |ind|
            assert_kind_of ACME::User, ind
        end
    end

    # Tests that User mixin was properly applied
    def test_admin
        assert @admin.is_admin?
        assert !@participant.is_admin?
    end
end




<railsAppDir>/test/unit/test/fixtures/rdf/users.yml
-----------------------------------------------------------------------

# Don't forget: no tabs
# In this example,

#    * ACME is the module name for the schema-generated classes
#    * http... is the prefix for the model
#    * User is the name of a class
#    * admin and participant are the keys for the resources, allowing
@users[:admin] etc.
#    * find_one and create are the methods by which individual resources are
instantiated (retrieved or created).
#          o find_all is also supported and retrieves into an Array all that
match the criteria.
#    * has_uid, has_password etc. are resource properties to bet set on
creation or used as conditions
#      for retrieval depending on the methods specified.

# Note that the RDF fixtures file can define multiple modules and, in each
one,
# multiple classes with their instances. The names of the instances must be
unique.

ACME:
 - 'http://domain.com/acme'
 - User:
      # assumes that an administrative User 'admin' exists
      admin:
        find_one:
          has_uid: 'admin'
      participant:
        create:
          has_uid: 'jd'
          has_password: 'secret'
      # no conditions
      all:
        find_all:
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.deri.org/pipermail/activerdf/attachments/20060715/238d443c/attachment-0001.htm


More information about the Activerdf mailing list