gearbox 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/Gemfile +21 -0
- data/Gemfile.lock +138 -0
- data/Guardfile +9 -0
- data/LICENSE.txt +20 -0
- data/README.html +89 -0
- data/README.md +87 -0
- data/Rakefile +46 -0
- data/VERSION +1 -0
- data/lib/examples/audience.rb +24 -0
- data/lib/examples/person.rb +29 -0
- data/lib/examples/reference.rb +38 -0
- data/lib/examples/theme.rb +8 -0
- data/lib/gearbox.rb +40 -0
- data/lib/gearbox/attribute_collection.rb +42 -0
- data/lib/gearbox/mixins/ad_hoc_properties.rb +41 -0
- data/lib/gearbox/mixins/resource.rb +17 -0
- data/lib/gearbox/mixins/semantic_accessors.rb +118 -0
- data/lib/gearbox/rdf_collection.rb +72 -0
- data/lib/gearbox/type.rb +85 -0
- data/lib/gearbox/types.rb +28 -0
- data/lib/gearbox/types/any.rb +24 -0
- data/lib/gearbox/types/boolean.rb +31 -0
- data/lib/gearbox/types/date.rb +27 -0
- data/lib/gearbox/types/decimal.rb +29 -0
- data/lib/gearbox/types/float.rb +28 -0
- data/lib/gearbox/types/integer.rb +27 -0
- data/lib/gearbox/types/native.rb +22 -0
- data/lib/gearbox/types/string.rb +27 -0
- data/lib/gearbox/types/uri.rb +25 -0
- data/spec/examples/audience_spec.rb +28 -0
- data/spec/examples/person_spec.rb +45 -0
- data/spec/examples/reference_spec.rb +43 -0
- data/spec/examples/theme_spec.rb +137 -0
- data/spec/gearbox/attribute_collection_spec.rb +33 -0
- data/spec/gearbox/mixins/ad_hoc_properties_spec.rb +52 -0
- data/spec/gearbox/mixins/resource_spec.rb +32 -0
- data/spec/gearbox/mixins/semantic_accessors_spec.rb +53 -0
- data/spec/gearbox/rdf_collection_spec.rb +52 -0
- data/spec/gearbox_spec.rb +13 -0
- data/spec/spec_helper.rb +16 -0
- metadata +235 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
module Gearbox
|
2
|
+
class Person
|
3
|
+
|
4
|
+
attr_accessor :name
|
5
|
+
attr_accessor :twitter_account
|
6
|
+
attr_accessor :email
|
7
|
+
attr_accessor :website
|
8
|
+
attr_accessor :phone
|
9
|
+
attr_accessor :resource
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
# Person
|
16
|
+
# name
|
17
|
+
# tweet account | email | website | phone
|
18
|
+
|
19
|
+
# it "has a people association" do
|
20
|
+
# subject.person_source = OpenStruct.public_method(:new)
|
21
|
+
# name = "George Q. Cannon"
|
22
|
+
# twitter_account = "gcannon"
|
23
|
+
# person = subject.add_person(:name => name, :twitter_account => twitter_account)
|
24
|
+
# subject.people[0].must_equal person
|
25
|
+
# person.resource.must_equal subject
|
26
|
+
# person.name.must_equal name
|
27
|
+
# person.twitter_account.must_equal twitter_account
|
28
|
+
# end
|
29
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Gearbox
|
2
|
+
class Reference
|
3
|
+
attr_accessor :location
|
4
|
+
attr_accessor :audience
|
5
|
+
|
6
|
+
attr_writer :person_source
|
7
|
+
attr_writer :theme_source
|
8
|
+
|
9
|
+
def add_person(hash={})
|
10
|
+
new_person = person_source.call(hash.merge(:reference => self))
|
11
|
+
people << new_person
|
12
|
+
new_person
|
13
|
+
end
|
14
|
+
|
15
|
+
def people
|
16
|
+
@people ||= []
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_theme(hash={})
|
20
|
+
new_theme = theme_source.call(hash.merge(:reference => self))
|
21
|
+
themes << new_theme
|
22
|
+
new_theme
|
23
|
+
end
|
24
|
+
|
25
|
+
def themes
|
26
|
+
@themes ||= []
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
def person_source
|
31
|
+
@person_source ||= Person.public_method(:new)
|
32
|
+
end
|
33
|
+
|
34
|
+
def theme_source
|
35
|
+
@theme_source ||= Theme.public_method(:new)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/gearbox.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# ================
|
2
|
+
# = Dependencies =
|
3
|
+
# ================
|
4
|
+
require 'uuid'
|
5
|
+
require 'linkeddata'
|
6
|
+
require 'ostruct'
|
7
|
+
|
8
|
+
module Gearbox
|
9
|
+
|
10
|
+
# ========================
|
11
|
+
# = Helper Utility: path =
|
12
|
+
# ========================
|
13
|
+
# @private
|
14
|
+
def path(path)
|
15
|
+
File.expand_path("../gearbox/#{path}", __FILE__)
|
16
|
+
end
|
17
|
+
private :path
|
18
|
+
module_function :path
|
19
|
+
|
20
|
+
# =======================
|
21
|
+
# = Loading the Library =
|
22
|
+
# =======================
|
23
|
+
autoload :AttributeCollection, path('attribute_collection')
|
24
|
+
autoload :RDFCollection, path('rdf_collection')
|
25
|
+
|
26
|
+
autoload :AdHocProperties, path('mixins/ad_hoc_properties')
|
27
|
+
autoload :Resource, path('mixins/resource')
|
28
|
+
autoload :SemanticAccessors, path('mixins/semantic_accessors')
|
29
|
+
|
30
|
+
# ============
|
31
|
+
# = Examples =
|
32
|
+
# ============
|
33
|
+
|
34
|
+
# Will separate these after things get to a solid 0.1 state.
|
35
|
+
autoload :Audience, path('../examples/audience')
|
36
|
+
autoload :Person, path('../examples/person')
|
37
|
+
autoload :Reference, path('../examples/reference')
|
38
|
+
autoload :Theme, path('../examples/theme')
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Gearbox
|
2
|
+
|
3
|
+
##
|
4
|
+
# Collects attributes in a hash-like format. Serializes the attributes in an OpenStruct.
|
5
|
+
##
|
6
|
+
class AttributeCollection
|
7
|
+
|
8
|
+
# Build a new AttributeCollection with an optional Hash.
|
9
|
+
# Note: this class normalizes the keys and creates OpenStructs for values.
|
10
|
+
# @param [Hash] default A hash with normalized keys and OpenStruct values
|
11
|
+
def initialize(default={})
|
12
|
+
raise ArgumentError, "Must provide a hash for the defaults" unless default.is_a?(Hash)
|
13
|
+
@default = default
|
14
|
+
@source = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
# Set one attribute in the collection.
|
18
|
+
# @param [String, Symbol] key
|
19
|
+
# @param [Hash] hash
|
20
|
+
# @return [OpenStruct] The hash, converted into an OpenStruct
|
21
|
+
def []=(key, hash)
|
22
|
+
raise ArgumentError, "Must provide a hash for the value" unless hash.is_a?(Hash)
|
23
|
+
@source[normalize_key(key)] = OpenStruct.new(@default.merge(hash))
|
24
|
+
end
|
25
|
+
|
26
|
+
# Get one attribute from the collection.
|
27
|
+
# @param [String, Symbol] key
|
28
|
+
# @return [OpenStruct, nil] Returns the attribute OpenStruct, if found.
|
29
|
+
def [](key)
|
30
|
+
@source[normalize_key(key)]
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
# Normalizes the key. Converts to a lower case symbol with non-alpha-numerics
|
35
|
+
# replaced by underscores, removing trailing and preceding underscores.
|
36
|
+
# @private
|
37
|
+
def normalize_key(obj)
|
38
|
+
obj.to_s.downcase.gsub(/[^A-Za-z0-9_]+/, '_').gsub(/(_$)|(^_)/, '').to_sym
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Gearbox
|
2
|
+
##
|
3
|
+
# Allows a model instance to add a new property at runtime.
|
4
|
+
# This is a (small) tip of the hat towards the flexibility
|
5
|
+
# offered by using a graph instead of a schema to govern
|
6
|
+
# the data store.
|
7
|
+
##
|
8
|
+
module AdHocProperties
|
9
|
+
|
10
|
+
# Getter and setter for an id property.
|
11
|
+
# Will be adjusted when I decide whether to mixin ActiveModel
|
12
|
+
attr_accessor :id
|
13
|
+
|
14
|
+
# Stored attributes
|
15
|
+
# @return [RDFCollection]
|
16
|
+
def attributes_list
|
17
|
+
@attributes_list ||= RDFCollection.new
|
18
|
+
end
|
19
|
+
|
20
|
+
# Generates or gets a blank node, based on the id.
|
21
|
+
# Will be replaced by subject.
|
22
|
+
# @return [RDF::Node]
|
23
|
+
def bnode
|
24
|
+
return @bnode if @bnode
|
25
|
+
self.id ||= UUID.generate
|
26
|
+
safe_id = "#{self.class.name}_#{id}".gsub(/[^A-Za-z0-9\-_]/, '_')
|
27
|
+
@bnode = RDF::Node(safe_id)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Add a property without defining it on the class.
|
31
|
+
# This will stay, will use the subject, and the regular infrastructure.
|
32
|
+
# @param [Symbol] accessor, the new field being created.
|
33
|
+
# @param [RDF::Statement] predicate, the predicate for the new field.
|
34
|
+
# @param [Any] The value to store
|
35
|
+
def add_property(accessor, predicate, object)
|
36
|
+
new_property = RDF::Statement.new(bnode, predicate, object)
|
37
|
+
attributes_list[accessor] = new_property
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Gearbox
|
2
|
+
##
|
3
|
+
# The main mixin for any model.
|
4
|
+
# TODO: include an example file.
|
5
|
+
##
|
6
|
+
module Resource
|
7
|
+
|
8
|
+
# ============
|
9
|
+
# = Behavior =
|
10
|
+
# ============
|
11
|
+
include AdHocProperties
|
12
|
+
include SemanticAccessors
|
13
|
+
include RDF::Mutable
|
14
|
+
include RDF::Queryable
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module Gearbox
|
2
|
+
|
3
|
+
##
|
4
|
+
# The attributes to add to a model.
|
5
|
+
# TODO: Add example from file.
|
6
|
+
##
|
7
|
+
module SemanticAccessors
|
8
|
+
|
9
|
+
# Treat this as a bundle of class methods and instance methods.
|
10
|
+
# @private
|
11
|
+
def self.included(base)
|
12
|
+
base.extend ClassMethods
|
13
|
+
base.send :include, InstanceMethods
|
14
|
+
end
|
15
|
+
|
16
|
+
##
|
17
|
+
# Class methods for the model.
|
18
|
+
##
|
19
|
+
module ClassMethods
|
20
|
+
|
21
|
+
# Add an attribute or a field to a model. Takes a field name.
|
22
|
+
# Defines both a getter and a setter on the object.
|
23
|
+
# Requires a predicate option. Options are:
|
24
|
+
# * :predicate => RDF::URI
|
25
|
+
# * :reverse => Boolean store as value, predicate, subject
|
26
|
+
# * :index => Boolean maintain a full-text search index on this attribute
|
27
|
+
# @param [String, Symbol] getter_name, the field that is being created.
|
28
|
+
# @param [Hash] options
|
29
|
+
def attribute(getter_name, options={})
|
30
|
+
|
31
|
+
raise ArgumentError, "A predicate must be defined" unless options[:predicate]
|
32
|
+
|
33
|
+
send(attributes_source)[getter_name] = options
|
34
|
+
|
35
|
+
# Define a getter on the object
|
36
|
+
define_method(getter_name) do
|
37
|
+
self.class.yield_attr(getter_name, self)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Define a setter on the object
|
41
|
+
define_method("#{getter_name}=") do |value|
|
42
|
+
self.class.store_attr(getter_name, self, value)
|
43
|
+
# attribute = send(self.class.attributes_source)[getter_name]
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
# Sets the attributes_source, where to store the attributes
|
49
|
+
attr_writer :attributes_source
|
50
|
+
|
51
|
+
# Gets the attributes_source...
|
52
|
+
def attributes_source
|
53
|
+
@attributes_source ||= :attribute_collection
|
54
|
+
end
|
55
|
+
|
56
|
+
def attribute_collection
|
57
|
+
@attribute_collection ||= AttributeCollection.new
|
58
|
+
end
|
59
|
+
|
60
|
+
def yield_attr(getter_name, instance)
|
61
|
+
if statement = instance.rdf_collection[getter_name]
|
62
|
+
# TODO: Deserialize object
|
63
|
+
return statement.object.to_s
|
64
|
+
else
|
65
|
+
attribute_options = send(attributes_source)[getter_name]
|
66
|
+
attribute_options ? attribute_options.default : nil
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def store_attr(getter_name, instance, value)
|
71
|
+
attribute_options = send(attributes_source)[getter_name]
|
72
|
+
# TODO: serialize value
|
73
|
+
statement = RDF::Statement.new(instance.subject, attribute_options.predicate, value)
|
74
|
+
instance.rdf_collection[getter_name] = statement
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
module InstanceMethods
|
80
|
+
# We collect triples inside the models in order to query them, filter them
|
81
|
+
# and handle the bridge between domain models and the graph we're building.
|
82
|
+
def rdf_collection
|
83
|
+
@rdf_collection ||= RDFCollection.new
|
84
|
+
end
|
85
|
+
|
86
|
+
def subject
|
87
|
+
"1"
|
88
|
+
end
|
89
|
+
|
90
|
+
# An initialization strategy for all occasions.
|
91
|
+
def initialize(obj=nil)
|
92
|
+
case obj
|
93
|
+
when Hash
|
94
|
+
merge_hash_values(obj)
|
95
|
+
when RDFCollection
|
96
|
+
merge_rdf_collection(obj)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def merge_hash_values(hash)
|
103
|
+
# TODO: work with associations here...
|
104
|
+
hash.each do |getter_name, value|
|
105
|
+
setter_name = "#{getter_name}="
|
106
|
+
send(setter_name, value) if respond_to?(setter_name)
|
107
|
+
# What to do with the others?
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def merge_rdf_collection(collection)
|
112
|
+
rdf_collection.merge!(collection)
|
113
|
+
end
|
114
|
+
|
115
|
+
end # InstanceMethods
|
116
|
+
|
117
|
+
end # SemanticAccessors
|
118
|
+
end # Gearbox
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Gearbox
|
2
|
+
|
3
|
+
##
|
4
|
+
# Collects model values as RDF. This is a key part of making SPARQL our primary
|
5
|
+
# filtering, finding, and extension language.
|
6
|
+
##
|
7
|
+
class RDFCollection
|
8
|
+
|
9
|
+
# ============
|
10
|
+
# = Behavior =
|
11
|
+
# ============
|
12
|
+
include RDF::Enumerable
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@source = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
# Enumerates on the RDF Statements. Necessary for RDF::Enumerable to
|
19
|
+
# add all of the internal and external iterator goodies available there
|
20
|
+
# (like each_subject and has_subject?).
|
21
|
+
# @param [Block] block Optional block. Creates an external iterator if omitted.
|
22
|
+
# @return [nil, Enumerator] Returns either nil, or an external iterator.
|
23
|
+
def each(&block)
|
24
|
+
if block_given?
|
25
|
+
@source.each(&block)
|
26
|
+
else
|
27
|
+
Enumerator.new(self, :each)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Set RDF::Statements to the underlying collection. Normalizes the keys.
|
32
|
+
# @param [String, Symbol] key
|
33
|
+
# @param [RDF::Statement] obj. RDF::Statement that will be added.
|
34
|
+
def add_statement(key, obj)
|
35
|
+
@source[normalize_key(key)] = obj if obj.is_a?(RDF::Statement)
|
36
|
+
end
|
37
|
+
alias :[]= :add_statement
|
38
|
+
|
39
|
+
# Get RDF::Statement from the underlying collection. Normalizes the key.
|
40
|
+
# @param [String, Symbol] key. Normalized.
|
41
|
+
# @return [RDF::Statement, nil] Found statement, if it exists.
|
42
|
+
def [](key)
|
43
|
+
@source[normalize_key(key)]
|
44
|
+
end
|
45
|
+
|
46
|
+
# Lookup whether the key exists.
|
47
|
+
# @param [String, Symbol] key
|
48
|
+
# @param [Hash, nil] opts. :normalize => false will lookup the key as provided.
|
49
|
+
# @return [Boolean]
|
50
|
+
def has_key?(key, opts={})
|
51
|
+
key = normalize_key(key) if opts.fetch(:normalize, true)
|
52
|
+
@source.has_key?(key)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Merges a hash of RDF::Statements into the underlying collection.
|
56
|
+
# Uses the add_statement to filter the values of the hash.
|
57
|
+
# @param [Hash] hash. Collection of statements.
|
58
|
+
# @return [nil]
|
59
|
+
def merge!(hash)
|
60
|
+
hash.each {|key, obj| add_statement(key, obj)}
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
# Normalizes the key. Converts to a lower case symbol with non-alpha-numerics
|
65
|
+
# replaced by underscores, removing trailing and preceding underscores.
|
66
|
+
# @private
|
67
|
+
def normalize_key(obj)
|
68
|
+
obj.to_s.downcase.gsub(/[^A-Za-z0-9_]+/, '_').gsub(/(_$)|(^_)/, '').to_sym
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
data/lib/gearbox/type.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
module Gearbox
|
2
|
+
|
3
|
+
##
|
4
|
+
# This was taken wholesale from Spira. I think Ben did a great job with that.
|
5
|
+
# I have nothing to add or detract from his good work.
|
6
|
+
#
|
7
|
+
# Gearbox::Type can be included by classes to create new property types for
|
8
|
+
# Gearbox. These types are responsible for serialization a Ruby value into an
|
9
|
+
# `RDF::Value`, and deserialization of an `RDF::Value` into a Ruby value.
|
10
|
+
#
|
11
|
+
# A simple example:
|
12
|
+
#
|
13
|
+
# class Integer
|
14
|
+
#
|
15
|
+
# include Gearbox::Type
|
16
|
+
#
|
17
|
+
# def self.unserialize(value)
|
18
|
+
# value.object
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# def self.serialize(value)
|
22
|
+
# RDF::Literal.new(value)
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# register_alias XSD.integer
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# This example will serialize and deserialize integers. It's included with
|
29
|
+
# Gearbox by default. It allows either of the following forms to declare an
|
30
|
+
# integer property on a Gearbox resource:
|
31
|
+
#
|
32
|
+
# property :age, :predicate => FOAF.age, :type => Integer
|
33
|
+
# property :age, :predicate => FOAF.age, :type => XSD.integer
|
34
|
+
#
|
35
|
+
# `Gearbox::Type`s include the RDF namespace and thus have all of the base RDF
|
36
|
+
# vocabularies available to them without the `RDF::` prefix.
|
37
|
+
#
|
38
|
+
# @see http://rdf.rubyforge.org/RDF/Value.html
|
39
|
+
# @see Gearbox::Resource
|
40
|
+
module Type
|
41
|
+
|
42
|
+
##
|
43
|
+
# Make the DSL available to a child class.
|
44
|
+
#
|
45
|
+
# @private
|
46
|
+
def self.included(child)
|
47
|
+
child.extend(ClassMethods)
|
48
|
+
Gearbox.type_alias(child,child)
|
49
|
+
end
|
50
|
+
|
51
|
+
include RDF
|
52
|
+
|
53
|
+
module ClassMethods
|
54
|
+
|
55
|
+
##
|
56
|
+
# Register an alias that this type can be referred to as, such as an RDF
|
57
|
+
# URI. The alias can be any object, symbol, or constant.
|
58
|
+
#
|
59
|
+
# @param [Any] identifier The new alias in property declarations for this class
|
60
|
+
# @return [Void]
|
61
|
+
def register_alias(any)
|
62
|
+
Gearbox.type_alias(any, self)
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# Serialize a given value to RDF.
|
67
|
+
#
|
68
|
+
# @param [Any] value The Ruby value to be serialized
|
69
|
+
# @return [RDF::Value] The RDF form of this value
|
70
|
+
def serialize(value)
|
71
|
+
value
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# Unserialize a given RDF value to Ruby
|
76
|
+
#
|
77
|
+
# @param [RDF::Value] value The RDF form of this value
|
78
|
+
# @return [Any] The Ruby form of this value
|
79
|
+
def unserialize(value)
|
80
|
+
value
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|