gearbox 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/.document +5 -0
  2. data/Gemfile +21 -0
  3. data/Gemfile.lock +138 -0
  4. data/Guardfile +9 -0
  5. data/LICENSE.txt +20 -0
  6. data/README.html +89 -0
  7. data/README.md +87 -0
  8. data/Rakefile +46 -0
  9. data/VERSION +1 -0
  10. data/lib/examples/audience.rb +24 -0
  11. data/lib/examples/person.rb +29 -0
  12. data/lib/examples/reference.rb +38 -0
  13. data/lib/examples/theme.rb +8 -0
  14. data/lib/gearbox.rb +40 -0
  15. data/lib/gearbox/attribute_collection.rb +42 -0
  16. data/lib/gearbox/mixins/ad_hoc_properties.rb +41 -0
  17. data/lib/gearbox/mixins/resource.rb +17 -0
  18. data/lib/gearbox/mixins/semantic_accessors.rb +118 -0
  19. data/lib/gearbox/rdf_collection.rb +72 -0
  20. data/lib/gearbox/type.rb +85 -0
  21. data/lib/gearbox/types.rb +28 -0
  22. data/lib/gearbox/types/any.rb +24 -0
  23. data/lib/gearbox/types/boolean.rb +31 -0
  24. data/lib/gearbox/types/date.rb +27 -0
  25. data/lib/gearbox/types/decimal.rb +29 -0
  26. data/lib/gearbox/types/float.rb +28 -0
  27. data/lib/gearbox/types/integer.rb +27 -0
  28. data/lib/gearbox/types/native.rb +22 -0
  29. data/lib/gearbox/types/string.rb +27 -0
  30. data/lib/gearbox/types/uri.rb +25 -0
  31. data/spec/examples/audience_spec.rb +28 -0
  32. data/spec/examples/person_spec.rb +45 -0
  33. data/spec/examples/reference_spec.rb +43 -0
  34. data/spec/examples/theme_spec.rb +137 -0
  35. data/spec/gearbox/attribute_collection_spec.rb +33 -0
  36. data/spec/gearbox/mixins/ad_hoc_properties_spec.rb +52 -0
  37. data/spec/gearbox/mixins/resource_spec.rb +32 -0
  38. data/spec/gearbox/mixins/semantic_accessors_spec.rb +53 -0
  39. data/spec/gearbox/rdf_collection_spec.rb +52 -0
  40. data/spec/gearbox_spec.rb +13 -0
  41. data/spec/spec_helper.rb +16 -0
  42. 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
@@ -0,0 +1,8 @@
1
+ module Gearbox
2
+ class Theme
3
+
4
+ attr_accessor :name
5
+ attr_accessor :tally
6
+
7
+ end
8
+ end
@@ -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
@@ -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