gearbox 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|