cldwalker-has_machine_tags 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ The MIT LICENSE
2
+
3
+ Copyright (c) 2009 Gabriel Horner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,134 @@
1
+ == Description
2
+
3
+ This plugin implements Flickr's machine tags as explained
4
+ here[http://www.flickr.com/groups/api/discuss/72157594497877875]
5
+ while still maintaining standard tagging behavior.
6
+
7
+ Basically, a machine tag has a namespace, a predicate and a value in the format
8
+ [namespace]:[predicate]=[value]
9
+
10
+ This allows for more precise tagging as tags can have unlimited contexts provided
11
+ by combinations of namespaces and predicates. These unlimited contexts also make
12
+ machine tags ripe for modeling relationships between objects. Read the Tag class
13
+ documentation for a more thorough explanation.
14
+
15
+ == Install
16
+
17
+ Install as a gem
18
+
19
+ bash> gem install cldwalker-has_machine_tags -s http://gems.github.com
20
+
21
+ Or as a plugin
22
+
23
+ bash> script/plugin install git://github.com/cldwalker/has_machine_tags.git
24
+
25
+ Migrate your database from Rails root:
26
+
27
+ bash> script/generate has_machine_tags_migration
28
+ bash> rake db:migrate
29
+
30
+ == Usage
31
+
32
+ Setup a model to use has_machine_tags
33
+
34
+ class Url < ActiveRecord::Base
35
+ has_machine_tags
36
+ end
37
+
38
+ Let's create some urls with machine tags!
39
+
40
+ url = Url.create(:name=>"http://github.com/cldwalker/has_machine_tags",
41
+ :tag_list=>'gem:type=tagging,flickr')
42
+
43
+ url2 = Url.create(:name=>"http://github.com/giraffesoft/is_taggable",
44
+ :tag_list=>'gem:type=tagging, gem:user=giraffesoft')
45
+
46
+ url3 = Url.create(:name=>"http://github.com/datamapper/data_mapper/tree/master",
47
+ :tag_list=>'gem:type=orm')
48
+
49
+ url.tag_list # => ["gem:type=tagging", "flickr"]
50
+
51
+ url.tags # => [<Tag name:"gem:type=tagging">, <Tag name:"flickr">]
52
+
53
+ Let's query them:
54
+
55
+ # Query urls tagged as a gem having type tagging
56
+ Url.tagged_with 'gem:type=tagging' # => [url, url2] from above
57
+
58
+ # Non-machine tags work of course
59
+ Url.tagged_with 'flickr' # => [url] from above
60
+
61
+ # tagged_with() is a named_scope so do your sweet chaining
62
+ Url.tagged_with('flickr').yet_another_finder(:sweet).paginate(:per_page=>30)
63
+
64
+ Nothing interesting so far. We could've done the same with normal tagging.
65
+
66
+ But when we start with wildcard machine tag syntax, machine tags become more valuable:
67
+
68
+ # Query urls tagged as gems (namespace = 'gem')
69
+ Url.tagged_with 'gem:' # => [url, url2, url3] from above
70
+
71
+ # Query urls tagged as having a user, regardless of namespace and value (predicate = 'user')
72
+ Url.tagged_with 'user=' # => [url2] from above
73
+
74
+ # Query urls tagged as gems having a user ( namespace ='gem' AND predicate = 'user')
75
+ Url.tagged_with 'gem:user' # => [url2] from above
76
+
77
+ # Query urls tagged as having a tagging value, regardless of namespace and predicate (value = 'tagging')
78
+ Url.tagged_with '=tagging' # => [url, url2] from above
79
+
80
+ More details on machine tag syntax can be found in the Tag class.
81
+
82
+ === More Usage
83
+
84
+ The wildcard machine tag syntax can also be used to fetch tags:
85
+
86
+ # Tags that are gems
87
+ Tag.machine_tags 'gem:' # => [<Tag name:"gem:type=tagging">, <Tag name:"gem:user=giraffesoft">]
88
+
89
+ # Tags that have a user predicate
90
+ Tag.machine_tags 'user=' # => [<Tag name:"gem:user=giraffesoft">]
91
+
92
+ Of course you can do the standard tag_list manipulation:
93
+
94
+ url.tag_list = "comma, delimited"
95
+ url.save
96
+ url.tag_list # =>['comma', 'delimited']
97
+
98
+ url.tag_list = ['or', 'an', 'array']
99
+ url.save
100
+ url.tag_list # =>['or', 'an' 'array']
101
+
102
+ #Add a tag
103
+ url.tag_list << 'another_tag'
104
+ url.save
105
+ url.tag_list # => ["gem:type=tagging", "flickr", "another_tag']
106
+
107
+ #Delete a tag
108
+ url.tag_list.delete('another_tag')
109
+ url.save
110
+ url.tag_list # => ["gem:type=tagging", "flickr"]
111
+
112
+
113
+ == Caveats
114
+
115
+ This is an experiment in progress so the api is subject to change.
116
+
117
+ Since machine tags require special characters to implement its goodness,
118
+ these characters are off limit unless used in the machine tag context:
119
+
120
+ '.', ':' , '*' , '=' , ','
121
+
122
+ == Todo
123
+
124
+ * More tests!
125
+ * Console methods for showing relations between namespaces, predicates + values
126
+ * Add support for DataMapper to be more ORM-agnostic
127
+ * Play friendly with other tagging plugins as needed.
128
+
129
+ == Credits
130
+
131
+ Thanks goes to Flickr for implementing this clever tagging model.
132
+ Thanks also goes to the {acts-as-taggable-on plugin}[http://github.com/mbleigh/acts-as-taggable-on/tree/master]
133
+ for their finder code and the {is_taggable plugin}[http://github.com/giraffesoft/is_taggable/tree/master]
134
+ for demonstrating sane testing for a Rails plugin.
@@ -0,0 +1,7 @@
1
+ class HasMachineTagsMigrationGenerator < Rails::Generator::Base
2
+ def manifest
3
+ record do |m|
4
+ m.migration_template 'migration.rb', 'db/migrate', :migration_file_name => "has_machine_tags_migration"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,26 @@
1
+ class HasMachineTagsMigration < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :tags do |t|
4
+ t.string :name
5
+ t.string :namespace
6
+ t.string :predicate
7
+ t.string :value
8
+ t.datetime :created_at
9
+ end
10
+
11
+ create_table :taggings do |t|
12
+ t.integer :tag_id
13
+ t.integer :taggable_id
14
+ t.string :taggable_type
15
+ t.datetime :created_at
16
+ end
17
+
18
+ add_index :taggings, :tag_id
19
+ add_index :taggings, [:taggable_id, :taggable_type]
20
+ end
21
+
22
+ def self.down
23
+ drop_table :taggings
24
+ drop_table :tags
25
+ end
26
+ end
@@ -0,0 +1,137 @@
1
+ # == Machine Tags
2
+ # Machine tags are in the format:
3
+ # [namespace]:[predicate]=[value]
4
+ #
5
+ # As explained here[http://www.flickr.com/groups/api/discuss/72157594497877875],
6
+ # a namespace and predicate must start with a letter a-z while its remaining characters can be any lowercase alphanumeric character
7
+ # and underscore. A value can contain any characters that normal tags use.
8
+ #
9
+ # == Wildcard Machine Tags
10
+ # Wildcard machine tag syntax is used with Tag.machine_tags() and {tagged_with() or find_tagged_with()}[link:classes/HasMachineTags/SingletonMethods.html] of tagged objects.
11
+ # This syntax allows one to fetch items that fall under a group of tags, as specified by namespace, predicate, value or
12
+ # a combination of these ways. While this plugin supports {Flickr's wildcard format}[http://code.flickr.com/blog/2008/07/18/wildcard-machine-tag-urls/],
13
+ # it also supports its own slightly shorter format.
14
+ #
15
+ # === Examples
16
+ #
17
+ # For a tag 'user:name=john', the following wildcards would match it:
18
+ #
19
+ # * Wild namespace (any tag with namespace user)
20
+ # Tag.machine_tags 'user:' # Our way
21
+ # Tag.machine_tags 'user:*=' # Flickr way
22
+ #
23
+ # * Wild predicate (any tag with predicate name)
24
+ # Tag.machine_tags 'name=' # Our way
25
+ # Tag.machine_tags '*:name=' # Flickr way
26
+ #
27
+ # * Wild predicate (any tag with value john)
28
+ # Tag.machine_tags '=john' # Our way
29
+ # Tag.machine_tags '*:*=john' # Flickr way
30
+ #
31
+ # * Wild namespace and predicate (any tag with namespace user and predicate name)
32
+ # Tag.machine_tags 'user:name' # Our way
33
+ # Tag.machine_tags 'user:name=' # Flickr way
34
+ #
35
+ # * Wild predicate and value (any tag with predicate name and value john)
36
+ # Tag.machine_tags 'name=john' # Our way
37
+ # Tag.machine_tags '*:name=john' # Flickr way
38
+ #
39
+ # * Wild namespace and value (any tag with namespace user and value john)
40
+ # Tag.machine_tags 'user.john' # Our way
41
+ # Tag.machine_tags 'user:*=john' # Flickr way
42
+ #
43
+ # == Food For Thought
44
+ # So what's so great about being able to give a tag a namespace and a predicate?
45
+ # * It allows for more fine-grained tag queries by giving multiple contexts:
46
+ #
47
+ # Say instead of having tagged with 'user:name=john' we had tagged with the traditional separate
48
+ # tags: user, name and john. How would we know that we had meant an item to be tagged
49
+ # as a user ie with namespace user? We wouldn't know. Any query for 'user' would return
50
+ # all user-tagged items <b>without context</b>. With machine tags, we can have 'user' refer
51
+ # to a particular combination of namespace, predicate and value.
52
+ #
53
+ # * It keeps tag-spaces cleaner because there are more contexts:
54
+ #
55
+ # With traditional separate tags, tags just have a global context. So
56
+ # if different users decide to give different meaning to the same tag, the tag starts to become
57
+ # polluted and loses its usefulness. With the limitless contexts provided by machine tags,
58
+ # a machine tag is less likely to pollute other tags.
59
+ #
60
+ # * It allows tagging to serve as a medium for defining relationships between objects:
61
+ #
62
+ # Since a machine tag tracks three attributes (namespace, predicate and value), it's possible to develop relationships
63
+ # between the attributes. This means namespaces can have many predicates and predicates can have many values.
64
+ # Since this closely resembles object modeling, we can start to use tagging to form relationships between tagged items and other objects.
65
+
66
+ class Tag < ActiveRecord::Base
67
+ has_many :taggings
68
+ validates_presence_of :name
69
+ validates_uniqueness_of :name
70
+ before_save :update_name_related_columns
71
+
72
+ NAMESPACE_REGEX = "[a-z](?:[a-z0-9_]+)"
73
+ PREDICATE_REGEX = "[a-z](?:[a-z0-9_]+)"
74
+ VALUE_REGEX = '.+'
75
+
76
+ #disallow machine tags special characters and tag list delimiter OR allow machine tag format
77
+ validates_format_of :name, :with=>/\A(([^\*\=\:\.,]+)|(#{NAMESPACE_REGEX}\:#{PREDICATE_REGEX}\=#{VALUE_REGEX}))\Z/
78
+
79
+ named_scope :namespace_counts, :select=>'*, namespace as counter, count(namespace) as count', :group=>"namespace HAVING count(namespace)>=1"
80
+ named_scope :predicate_counts, :select=>'*, predicate as counter, count(predicate) as count', :group=>"predicate HAVING count(predicate)>=1"
81
+ named_scope :value_counts, :select=>'*, value as counter, count(value) as count', :group=>"value HAVING count(value)>=1"
82
+ named_scope :namespace, lambda {|namespace| {:conditions=>{:namespace=>namespace}} }
83
+ named_scope :predicate, lambda {|predicate| {:conditions=>{:predicate=>predicate}} }
84
+ named_scope :value, lambda {|value| {:conditions=>{:value=>value}} }
85
+
86
+ # To be used with the *counts methods.
87
+ # For example:
88
+ # stat(:namespace_counts)
89
+ # This prints out pairs of a namespaces and their counts in the tags table.
90
+ def self.stat(type)
91
+ send(type).map {|e| [e.counter, e.count] }
92
+ end
93
+
94
+ # Takes a wildcard machine tag and returns matching tags.
95
+ def self.machine_tags(name)
96
+ conditions = if (match = match_wildcard_machine_tag(name))
97
+ match.map {|k,v|
98
+ sanitize_sql(["#{k} = ?", v])
99
+ }.join(" AND ")
100
+ else
101
+ sanitize_sql(["name = ?", name])
102
+ end
103
+ find(:all, :conditions=>conditions)
104
+ end
105
+
106
+ # Valid wildcards with their equivalent shortcuts
107
+ # namespace:*=* -> namespace:
108
+ # *:predicate=* -> predicate=
109
+ # *:*=value -> :value
110
+ def self.match_wildcard_machine_tag(name) #:nodoc:
111
+ if name =~ /^(#{NAMESPACE_REGEX}|\*)\:(#{PREDICATE_REGEX}|\*)\=(#{VALUE_REGEX}|\*)?$/
112
+ result = [[:namespace, $1], [:predicate, $2], [:value, $3]].select {|k,v| ![nil,'*'].include?(v) }
113
+ result.size == 3 ? nil : result
114
+ #duo shortcuts
115
+ elsif name =~ /^(#{NAMESPACE_REGEX}\:#{PREDICATE_REGEX})|(#{PREDICATE_REGEX}\=#{VALUE_REGEX})|(#{NAMESPACE_REGEX}\.#{VALUE_REGEX})$/
116
+ $1 ? [:namespace, :predicate].zip($1.split(":")) : ($2 ? [:predicate, :value].zip($2.split("=")) :
117
+ [:namespace, :value].zip($3.split('.')) )
118
+ #single shortcuts
119
+ elsif name =~ /^((#{NAMESPACE_REGEX})(?:\:)|(#{PREDICATE_REGEX})(?:\=)|(?:\=)(#{VALUE_REGEX}))$/
120
+ $2 ? [[:namespace, $2]] : ($3 ? [[:predicate, $3]] : [[:value, $4]])
121
+ else
122
+ nil
123
+ end
124
+ end
125
+
126
+ def extract_from_name(tag_name) #:nodoc:
127
+ (tag_name =~ /^(#{NAMESPACE_REGEX})\:(#{PREDICATE_REGEX})\=(#{VALUE_REGEX})$/) ? [$1, $2, $3] : nil
128
+ end
129
+
130
+ private
131
+
132
+ def update_name_related_columns
133
+ if self.changed.include?('name') && (arr = extract_from_name(self.name))
134
+ self[:namespace], self[:predicate], self[:value] = arr
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,15 @@
1
+ module HasMachineTags
2
+ class TagList < Array #:nodoc:
3
+ cattr_accessor :delimiter
4
+ self.delimiter = ','
5
+
6
+ def initialize(string_or_array)
7
+ array = string_or_array.is_a?(Array) ? string_or_array : string_or_array.split(/\s*#{delimiter}\s*/)
8
+ concat array
9
+ end
10
+
11
+ def to_s
12
+ join("#{delimiter} ")
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ class Tagging < ActiveRecord::Base
2
+ belongs_to :tag
3
+ end
@@ -0,0 +1,125 @@
1
+ current_dir = File.dirname(__FILE__)
2
+ $:.unshift(current_dir) unless $:.include?(current_dir) || $:.include?(File.expand_path(current_dir))
3
+ require 'has_machine_tags/tag_list'
4
+
5
+ module HasMachineTags
6
+ def self.included(base) #:nodoc:
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods #:nodoc:
11
+ def has_machine_tags
12
+ self.class_eval do
13
+ has_many :taggings, :as=>:taggable, :dependent=>:destroy
14
+ has_many :tags, :through=>:taggings
15
+ after_save :save_tags
16
+
17
+ include HasMachineTags::InstanceMethods
18
+ extend HasMachineTags::SingletonMethods
19
+ if respond_to?(:named_scope)
20
+ named_scope :tagged_with, lambda{ |tags, options|
21
+ find_options_for_find_tagged_with(tags, options)
22
+ }
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ module SingletonMethods
29
+ # Takes a string of delimited tags or an array of tags.
30
+ # Note that each tag is interpreted as a possible wildcard machine tag.
31
+ #
32
+ # Options:
33
+ # :exclude - Find models that are not tagged with the given tags
34
+ # :match_all - Find models that match all of the given tags, not just one, default: true
35
+ # :conditions - A piece of SQL conditions to add to the query
36
+ #
37
+ # Example:
38
+ # Url.tagged_with 'something' # => fetches urls tagged with 'something'
39
+ # Url.tagged_with 'gem:' # => fetches urls tagged with tags that have namespace gem
40
+ # Url.tagged_with 'gem:, something' # => fetches urls that are tagged with 'something'
41
+ # and tags that have namespace gem
42
+ #
43
+ # Note: This method really only needs to be used for Rails < 2.1 .
44
+ # Rails 2.1 and greater should use tagged_with(), which acts the same but with
45
+ # the benefits of named_scope.
46
+ #
47
+ def find_tagged_with(*args)
48
+ options = find_options_for_find_tagged_with(*args)
49
+ options.blank? ? [] : find(:all,options)
50
+ end
51
+
52
+ def find_options_for_find_tagged_with(tags, options = {}) #:nodoc:
53
+ options.reverse_merge!(:match_all=>true)
54
+ tags = TagList.new(tags)
55
+ return {} if tags.empty?
56
+
57
+ conditions = []
58
+ conditions << sanitize_sql(options.delete(:conditions)) if options[:conditions]
59
+
60
+ taggings_alias, tags_alias = "#{table_name}_taggings", "#{table_name}_tags"
61
+
62
+ if options.delete(:exclude)
63
+ tags_conditions = tags.map { |t| sanitize_sql(["#{Tag.table_name}.name = ?", t]) }.join(" OR ")
64
+ conditions << sanitize_sql(["#{table_name}.id NOT IN (SELECT #{Tagging.table_name}.taggable_id FROM #{Tagging.table_name} LEFT OUTER JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id WHERE (#{tags_conditions}) AND #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})", tags])
65
+ else
66
+ conditions << tags.map {|t|
67
+ if match = Tag.match_wildcard_machine_tag(t)
68
+ string = match.map {|k,v|
69
+ sanitize_sql(["#{tags_alias}.#{k} = ?", v])
70
+ }.join(" AND ")
71
+ "(#{string})"
72
+ else
73
+ sanitize_sql(["#{tags_alias}.name = ?", t])
74
+ end
75
+ }.join(" OR ")
76
+
77
+ if options.delete(:match_all)
78
+ group = "#{taggings_alias}.taggable_id HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
79
+ end
80
+ end
81
+
82
+ { :select => "DISTINCT #{table_name}.*",
83
+ :joins => "LEFT OUTER JOIN #{Tagging.table_name} #{taggings_alias} ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key} AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)} " +
84
+ "LEFT OUTER JOIN #{Tag.table_name} #{tags_alias} ON #{tags_alias}.id = #{taggings_alias}.tag_id",
85
+ :conditions => conditions.join(" AND "),
86
+ :group => group
87
+ }.update(options)
88
+ end
89
+ end
90
+
91
+ module InstanceMethods
92
+ # Set tag list with an array of tags or comma delimited string of tags
93
+ def tag_list=(list)
94
+ @tag_list = TagList.new(list)
95
+ end
96
+
97
+ # Fetches latest tag list for an object
98
+ def tag_list
99
+ @tag_list ||= self.tags.map(&:name)
100
+ end
101
+
102
+ protected
103
+ # :stopdoc:
104
+ def save_tags
105
+ self.class.transaction do
106
+ delete_unused_tags
107
+ add_new_tags
108
+ end
109
+ end
110
+
111
+ def delete_unused_tags
112
+ unused_tags = tags.select {|e| !tag_list.include?(e.name) }
113
+ tags.delete(*unused_tags)
114
+ end
115
+
116
+ def add_new_tags
117
+ new_tags = tag_list - (self.tags || []).map(&:name)
118
+ new_tags.each do |t|
119
+ self.tags << Tag.find_or_initialize_by_name(t)
120
+ end
121
+ end
122
+ #:startdoc:
123
+ end
124
+
125
+ end
@@ -0,0 +1,56 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class HasMachineTagsTest < Test::Unit::TestCase
4
+ context "TagList" do
5
+ before(:each) { @taggable = TaggableModel.new }
6
+
7
+ test "sets tag list with array" do
8
+ arr = ['some', 'tag:name=blah']
9
+ @taggable.tag_list = arr
10
+ @taggable.tag_list.should == arr
11
+ end
12
+
13
+ test "sets tag list with delimited string" do
14
+ arr = ['more', 'tag:type=clever']
15
+ @taggable.tag_list = arr.join(", ")
16
+ @taggable.tag_list.should == arr
17
+ end
18
+
19
+ test "sets tag list with messy delimited string" do
20
+ arr = ['more', 'tag:type=dumb', 'really']
21
+ @taggable.tag_list = "more,tag:type=dumb, really"
22
+ @taggable.tag_list.should == arr
23
+ @taggable.tag_list.to_s.should == arr.join(", ")
24
+ end
25
+ end
26
+
27
+ context "HasMachineTags" do
28
+ before(:each) { @taggable = TaggableModel.new }
29
+
30
+ test "creates all tags" do
31
+ tags = ['some', 'tag:name=blah']
32
+ @taggable.tag_list = tags
33
+ @taggable.save!
34
+ @taggable.tags.map(&:name).should == tags
35
+ end
36
+
37
+ test "only creates new tags" do
38
+ @taggable.tag_list = "bling"
39
+ @taggable.save!
40
+ tag_count = Tag.count
41
+ @taggable.tag_list = "bling, bling2"
42
+ @taggable.save!
43
+ @taggable.taggings.size.should == 2
44
+ Tag.count.should == tag_count + 1
45
+ end
46
+
47
+ test "deletes unused tags" do
48
+ @taggable.tag_list == 'bling, bling3'
49
+ @taggable.save!
50
+ @taggable.tag_list = "bling4"
51
+ @taggable.save!
52
+ @taggable.taggings.size.should == 1
53
+ @taggable.tags.map(&:name).should == ['bling4']
54
+ end
55
+ end
56
+ end
data/test/schema.rb ADDED
@@ -0,0 +1,20 @@
1
+ ActiveRecord::Schema.define(:version => 0) do
2
+ create_table :taggable_models do |t|
3
+ t.string :title
4
+ end
5
+
6
+ create_table :tags do |t|
7
+ t.string :name
8
+ t.string :namespace
9
+ t.string :predicate
10
+ t.string :value
11
+ t.datetime :created_at
12
+ end
13
+
14
+ create_table :taggings do |t|
15
+ t.integer :tag_id
16
+ t.string :taggable_type
17
+ t.integer :taggable_id
18
+ t.datetime :created_at
19
+ end
20
+ end
data/test/tag_test.rb ADDED
@@ -0,0 +1,109 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class HasMachineTags::TagTest < Test::Unit::TestCase
4
+ test "create with normal tag name only touches name" do
5
+ obj = Tag.create(:name=>'blah1')
6
+ [:name, :namespace, :predicate, :value].map {|e| obj.send(e)}.should == ['blah1', nil, nil, nil]
7
+ end
8
+
9
+ test "create with machine tag name sets all name fields" do
10
+ obj = Tag.create(:name=>'gem:name=machine')
11
+ [:name, :namespace, :predicate, :value].map {|e| obj.send(e)}.should == ['gem:name=machine', 'gem', 'name', 'machine']
12
+ end
13
+
14
+ context "update" do
15
+ before(:each) { @obj = Tag.new }
16
+
17
+ test "with normal tag name only touches name" do
18
+ @obj.update_attributes :name=> 'bling'
19
+ [:name, :namespace, :predicate, :value].map {|e| @obj.send(e)}.should == ['bling', nil, nil, nil]
20
+ end
21
+
22
+ test "with machine tag name sets all name fields" do
23
+ @obj.update_attributes :name=>'gem:prop=value'
24
+ [:name, :namespace, :predicate, :value].map {|e| @obj.send(e)}.should == ['gem:prop=value', 'gem', 'prop', 'value']
25
+ end
26
+
27
+ test "with no name sets no name fields" do
28
+ @obj.update_attributes :name=>'blah2'
29
+ @obj.update_attributes :predicate=>'changed'
30
+ @obj.name.should == 'blah2'
31
+ end
32
+ end
33
+
34
+ context "match_wildcard_machine_tag" do
35
+ test "matches namespace with asterisk" do
36
+ Tag.match_wildcard_machine_tag('name:*=').should == [[:namespace,'name']]
37
+ end
38
+
39
+ test "matches namespace without asterisk" do
40
+ Tag.match_wildcard_machine_tag('name:').should == [[:namespace,'name']]
41
+ end
42
+
43
+ test "matches predicate with asterisk" do
44
+ Tag.match_wildcard_machine_tag('*:pred=').should == [[:predicate,'pred']]
45
+ end
46
+
47
+ test "matches predicate without asterisk" do
48
+ Tag.match_wildcard_machine_tag('pred=').should == [[:predicate,'pred']]
49
+ end
50
+
51
+ test "matches value with asterisk" do
52
+ Tag.match_wildcard_machine_tag('*:*=val').should == [[:value, 'val']]
53
+ end
54
+
55
+ test "matches value without asterisk" do
56
+ Tag.match_wildcard_machine_tag('=val').should == [[:value, 'val']]
57
+ end
58
+
59
+ test "matches namespace and predicate without asterisk" do
60
+ Tag.match_wildcard_machine_tag('name:pred').should == [[:namespace, 'name'], [:predicate, 'pred']]
61
+ end
62
+
63
+ test "matches namespace and predicate with asterisk" do
64
+ Tag.match_wildcard_machine_tag('name:pred=').should == [[:namespace, 'name'], [:predicate, 'pred']]
65
+ end
66
+
67
+ test "matches predicate and value without asterisk" do
68
+ Tag.match_wildcard_machine_tag('pred=val').should == [[:predicate, 'pred'], [:value, 'val']]
69
+ end
70
+
71
+ test "matches predicate and value with asterisk" do
72
+ Tag.match_wildcard_machine_tag('*:pred=val').should == [[:predicate, 'pred'], [:value, 'val']]
73
+ end
74
+
75
+ test "matches namespace and value without asterisk" do
76
+ Tag.match_wildcard_machine_tag('name.val').should == [[:namespace, 'name'], [:value, 'val']]
77
+ end
78
+
79
+ test "matches namespace and value with asterisk" do
80
+ Tag.match_wildcard_machine_tag('name:*=val').should == [[:namespace, 'name'], [:value, 'val']]
81
+ end
82
+
83
+ test "doesn't match total wildcard" do
84
+ Tag.match_wildcard_machine_tag('*:*=').should == []
85
+ end
86
+
87
+ test "doesn't match machine tag" do
88
+ Tag.match_wildcard_machine_tag('name:pred=val').should == nil
89
+ end
90
+
91
+ test "doesn't match normal tag" do
92
+ Tag.match_wildcard_machine_tag('name').should == nil
93
+ end
94
+ end
95
+
96
+ test "validates name when no invalid characters" do
97
+ Tag.new(:name=>'valid!name_really?').valid?.should be(true)
98
+ end
99
+
100
+ test "validates name when machine tag format" do
101
+ Tag.new(:name=>'name:pred=value').valid?.should be(true)
102
+ end
103
+
104
+ test "invalidates name when invalid characters present" do
105
+ %w{some.tag another:tag so=invalid yet,another whoop*}.each do |e|
106
+ Tag.new(:name=>e).valid?.should be(false)
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,25 @@
1
+ require 'rubygems'
2
+ require 'activerecord'
3
+ require 'test/unit'
4
+ require 'context' #gem install jeremymcanally-context -s http://gems.github.com
5
+ require 'matchy' #gem install jeremymcanally-matchy -s http://gems.github.com
6
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
7
+ require File.join(File.dirname(__FILE__), '..', 'init')
8
+
9
+ #Setup logger
10
+ require 'logger'
11
+ ActiveRecord::Base.logger = Logger.new(STDERR)
12
+ ActiveRecord::Base.logger.level = Logger::WARN
13
+
14
+ #Setup db
15
+ ActiveRecord::Base.configurations = {'sqlite3' => {:adapter => 'sqlite3', :database => ':memory:'}}
16
+ ActiveRecord::Base.establish_connection('sqlite3')
17
+
18
+ #Define schema
19
+ require File.join(File.dirname(__FILE__), 'schema')
20
+ class TaggableModel < ActiveRecord::Base
21
+ has_machine_tags
22
+ end
23
+
24
+ class Test::Unit::TestCase
25
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cldwalker-has_machine_tags
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Gabriel Horner
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-02-12 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: A rails tagging plugin implementing flickr's machine tags + maybe more (semantic tags)
17
+ email: gabriel.horner@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.rdoc
24
+ - LICENSE.txt
25
+ files:
26
+ - README.rdoc
27
+ - LICENSE.txt
28
+ - generators/has_machine_tags_migration
29
+ - generators/has_machine_tags_migration/has_machine_tags_migration_generator.rb
30
+ - generators/has_machine_tags_migration/templates
31
+ - generators/has_machine_tags_migration/templates/migration.rb
32
+ - lib/has_machine_tags
33
+ - lib/has_machine_tags/tag.rb
34
+ - lib/has_machine_tags/tag_list.rb
35
+ - lib/has_machine_tags/tagging.rb
36
+ - lib/has_machine_tags.rb
37
+ - test/has_machine_tags_test.rb
38
+ - test/schema.rb
39
+ - test/tag_test.rb
40
+ - test/test_helper.rb
41
+ has_rdoc: true
42
+ homepage: http://github.com/cldwalker/has_machine_tags
43
+ post_install_message:
44
+ rdoc_options: []
45
+
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ version:
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: "0"
59
+ version:
60
+ requirements: []
61
+
62
+ rubyforge_project:
63
+ rubygems_version: 1.2.0
64
+ signing_key:
65
+ specification_version: 2
66
+ summary: A rails tagging plugin implementing flickr's machine tags + maybe more (semantic tags)
67
+ test_files: []
68
+