cldwalker-has_machine_tags 0.1.2

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/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
+