url_keyed_object 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/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +29 -0
- data/Rakefile +78 -0
- data/VERSION +1 -0
- data/cucumber.yml +2 -0
- data/features/fixtures/.gitignore +1 -0
- data/features/step_definitions/database_steps.rb +7 -0
- data/features/step_definitions/eval_steps.rb +15 -0
- data/features/step_definitions/feedback_steps.rb +8 -0
- data/features/support/env.rb +4 -0
- data/features/support/hooks.rb +24 -0
- data/features/support/recording_logger.rb +28 -0
- data/features/with_active_record-basic.feature +58 -0
- data/lib/url_keyed_object/active_record.rb +41 -0
- data/lib/url_keyed_object.rb +33 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +39 -0
- data/spec/url_keyed_object/active_record_spec.rb +104 -0
- data/spec/url_keyed_object_spec.rb +41 -0
- metadata +107 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Matt Patterson
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
= Url-keyed Object
|
2
|
+
|
3
|
+
Makes it easy to work with ActiveRecord (and other) objects in Rails (and anything else) which want to use an opaque alphanumeric ID in URLs to identify themselves, rather than their database ID.
|
4
|
+
|
5
|
+
/model/1
|
6
|
+
|
7
|
+
versus
|
8
|
+
|
9
|
+
/model/a1c5t
|
10
|
+
|
11
|
+
One exposes your database IDs in your URLs, and the other doesn't. There are cases when that's genuinely useful not to expose IDs directly (changing your Database from MySQL to Something Else, say, or some radical horiztonal partitioning scheme, or easier data migration), but mostly it's a matter of personal taste.
|
12
|
+
|
13
|
+
== Documentation
|
14
|
+
|
15
|
+
I'm trying to make executable documentation, so all the docs are in the form of Cucumber features, under the +features+ dir.
|
16
|
+
|
17
|
+
== Note on Patches/Pull Requests
|
18
|
+
|
19
|
+
* Fork the project.
|
20
|
+
* Make your feature addition or bug fix.
|
21
|
+
* Add tests for it. This is important so I don't break it in a
|
22
|
+
future version unintentionally.
|
23
|
+
* Commit, do not mess with rakefile, version, or history.
|
24
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
25
|
+
* Send me a pull request. Bonus points for topic branches.
|
26
|
+
|
27
|
+
== Copyright
|
28
|
+
|
29
|
+
Copyright (c) 2010 Matt Patterson. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "url_keyed_object"
|
8
|
+
gem.summary = %Q{Making it easy to work with objects with URL keys}
|
9
|
+
gem.description = %Q{Making it easy to work with Rails objects which use a URL key in their URL instead of their database ID.}
|
10
|
+
gem.email = "matt@reprocessed.org"
|
11
|
+
gem.homepage = "http://github.com/fidothe/url_keyed_object"
|
12
|
+
gem.authors = ["Matt Patterson"]
|
13
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
14
|
+
gem.add_development_dependency "cucumber", ">= 0.5"
|
15
|
+
gem.add_development_dependency "activerecord", ">= 2.3"
|
16
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
17
|
+
end
|
18
|
+
Jeweler::GemcutterTasks.new
|
19
|
+
rescue LoadError
|
20
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'spec/rake/spectask'
|
24
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
25
|
+
spec.libs << 'lib' << 'spec'
|
26
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
27
|
+
end
|
28
|
+
|
29
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
30
|
+
spec.libs << 'lib' << 'spec'
|
31
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
32
|
+
spec.rcov = true
|
33
|
+
end
|
34
|
+
|
35
|
+
task :spec => :check_dependencies
|
36
|
+
|
37
|
+
begin
|
38
|
+
require 'cucumber/rake/task'
|
39
|
+
namespace :cucumber do
|
40
|
+
Cucumber::Rake::Task.new(:ok, 'Run features that should pass') do |t|
|
41
|
+
t.fork = true # You may get faster startup if you set this to false
|
42
|
+
t.profile = 'default'
|
43
|
+
end
|
44
|
+
|
45
|
+
Cucumber::Rake::Task.new(:wip, 'Run features that are being worked on') do |t|
|
46
|
+
t.fork = true # You may get faster startup if you set this to false
|
47
|
+
t.profile = 'wip'
|
48
|
+
end
|
49
|
+
|
50
|
+
desc 'Run all features'
|
51
|
+
task :all => [:ok, :wip]
|
52
|
+
end
|
53
|
+
desc 'Alias for cucumber:ok'
|
54
|
+
task :cucumber => 'cucumber:ok'
|
55
|
+
|
56
|
+
task :default => :cucumber
|
57
|
+
|
58
|
+
task :features => :cucumber do
|
59
|
+
STDERR.puts "*** The 'features' task is deprecated. See rake -T cucumber ***"
|
60
|
+
end
|
61
|
+
rescue LoadError
|
62
|
+
desc 'cucumber rake task not available (cucumber not installed)'
|
63
|
+
task :cucumber do
|
64
|
+
abort 'Cucumber rake task is not available. Be sure to install cucumber as a gem or plugin'
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
task :default => :spec
|
69
|
+
|
70
|
+
require 'rake/rdoctask'
|
71
|
+
Rake::RDocTask.new do |rdoc|
|
72
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
73
|
+
|
74
|
+
rdoc.rdoc_dir = 'rdoc'
|
75
|
+
rdoc.title = "url_keyed_object #{version}"
|
76
|
+
rdoc.rdoc_files.include('README*')
|
77
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
78
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/cucumber.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
*.sqlite3
|
@@ -0,0 +1,15 @@
|
|
1
|
+
Given /^this class:$/ do |string|
|
2
|
+
eval string
|
3
|
+
end
|
4
|
+
|
5
|
+
When /^I (?:make|do) .+:$/ do |string|
|
6
|
+
eval string
|
7
|
+
end
|
8
|
+
|
9
|
+
Then /^(@[a-zA-Z_0-9]+)\.([a-zA-Z_0-9?!]+) should match \/(.+)\/$/ do |ivar, method, regex|
|
10
|
+
instance_variable_get(ivar).send(method).should match(regex)
|
11
|
+
end
|
12
|
+
|
13
|
+
Then /^(@[a-zA-Z_0-9]+)\.([a-zA-Z_0-9?!]+) should be (.+)$/ do |ivar, method, value|
|
14
|
+
instance_variable_get(ivar).send(method).should send("be_#{value}")
|
15
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
Then /^an? (warning|error|debug message|fatal error|info message|unknown) should have been logged$/ do |message_kind|
|
2
|
+
severity = {
|
3
|
+
'warning' => Logger::WARN, 'error' => Logger::ERROR,
|
4
|
+
'debug message' => Logger::DEBUG, 'info message' => Logger::INFO,
|
5
|
+
'fatal error' => Logger::FATAL, 'unknown' => Logger::UNKNOWN
|
6
|
+
}[message_kind]
|
7
|
+
@recording_logger.has_message_for_severity?(severity).should be_true
|
8
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
Before("@db") do |scenario|
|
4
|
+
require 'activerecord'
|
5
|
+
@db_dir = ::Dir.mktmpdir
|
6
|
+
begin
|
7
|
+
# Create the SQLite database
|
8
|
+
ActiveRecord::Base.establish_connection({'adapter' => 'sqlite3',
|
9
|
+
'database' => "#{@db_dir}/feature.sqlite3", 'pool' => 5, 'timeout' => 5000
|
10
|
+
})
|
11
|
+
ActiveRecord::Base.connection
|
12
|
+
@recording_logger = RecordingLogger.new($stderr)
|
13
|
+
ActiveRecord::Base.logger = @recording_logger
|
14
|
+
rescue
|
15
|
+
$stderr.puts $!, *($!.backtrace)
|
16
|
+
$stderr.puts "Couldn't create database for #{"#{@db_dir}/feature.sqlite3".inspect}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
After("@db") do |scenario|
|
21
|
+
ActiveRecord::Base.connection.disconnect!
|
22
|
+
FileUtils.remove_entry_secure @db_dir
|
23
|
+
@recording_logger.reset_recorder!
|
24
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
class RecordingLogger < Logger
|
4
|
+
def recorded_messages
|
5
|
+
@recorded_messages ||= {}
|
6
|
+
end
|
7
|
+
|
8
|
+
alias_method :add_original, :add
|
9
|
+
def add(severity, message = nil, progname = nil, &block)
|
10
|
+
record_it(severity, message)
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def reset_recorder!
|
16
|
+
@recorded_messages = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def has_message_for_severity?(severity)
|
20
|
+
(!recorded_messages[severity].nil? && !recorded_messages[severity].empty?)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def record_it(severity, message)
|
26
|
+
(recorded_messages[severity] ||= []) << message
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
@db
|
2
|
+
Feature: Using with ActiveRecord
|
3
|
+
In order to generate and validate URL keys in an ActiveRecord::Base object
|
4
|
+
A Rails developer
|
5
|
+
Wants to include and use UrlKeyedObject
|
6
|
+
|
7
|
+
# The most basic use case is for a 5-character ID, stored in the url_key column.
|
8
|
+
#
|
9
|
+
# Simply including the UrlKeyedObject::ActiveRecord module is enough.
|
10
|
+
# This makes the url_key attribute protected from mass assignment and read-only.
|
11
|
+
Background:
|
12
|
+
Given a database, with this table defined:
|
13
|
+
"""
|
14
|
+
create_table :things do |t|
|
15
|
+
t.string :url_key, :null => false
|
16
|
+
|
17
|
+
t.timestamps
|
18
|
+
end
|
19
|
+
"""
|
20
|
+
And this class:
|
21
|
+
"""
|
22
|
+
class Thing < ActiveRecord::Base
|
23
|
+
include UrlKeyedObject::ActiveRecord
|
24
|
+
end
|
25
|
+
"""
|
26
|
+
|
27
|
+
Scenario: An unsaved model object with UrlKeyedObject included
|
28
|
+
When I make a bare instance:
|
29
|
+
"""
|
30
|
+
@instance = Thing.new
|
31
|
+
"""
|
32
|
+
Then @instance.url_key should be nil
|
33
|
+
|
34
|
+
Scenario: A saved model object with UrlKeyedObject included
|
35
|
+
When I make and save an instance:
|
36
|
+
"""
|
37
|
+
@instance = Thing.new
|
38
|
+
@instance.save!
|
39
|
+
"""
|
40
|
+
Then @instance.url_key should match /[a-z0-9]{5}/
|
41
|
+
|
42
|
+
Scenario: Attempting to mass-assign url_key ought to fail
|
43
|
+
When I make an instance using mass-assignment:
|
44
|
+
"""
|
45
|
+
@instance = Thing.new(:url_key => 'abcde')
|
46
|
+
"""
|
47
|
+
Then @instance.url_key should be nil
|
48
|
+
And a warning should have been logged
|
49
|
+
|
50
|
+
Scenario: Attempting to set url_key ought to fail
|
51
|
+
When I make an instance using mass-assignment:
|
52
|
+
"""
|
53
|
+
@instance = Thing.new
|
54
|
+
@instance.url_key = 'abcde'
|
55
|
+
"""
|
56
|
+
Then @instance.url_key should be nil
|
57
|
+
And a warning should have been logged
|
58
|
+
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module UrlKeyedObject
|
2
|
+
module ActiveRecord
|
3
|
+
# --
|
4
|
+
# the stuff that actually gets included.
|
5
|
+
# comment included for my benefit, in case I forget (quite likely)
|
6
|
+
# ++
|
7
|
+
|
8
|
+
def to_param
|
9
|
+
url_key
|
10
|
+
end
|
11
|
+
|
12
|
+
def url_key=(value)
|
13
|
+
logger.warn('Attempt to set #url_key!')
|
14
|
+
nil
|
15
|
+
end
|
16
|
+
|
17
|
+
# --
|
18
|
+
# Validations
|
19
|
+
# ++
|
20
|
+
|
21
|
+
def self.included(model)
|
22
|
+
model.class_eval do
|
23
|
+
before_create :generate_valid_url_key
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def generate_valid_url_key
|
30
|
+
new_url_key = UrlKeyedObject.generate_unchecked_url_key
|
31
|
+
while !self.valid_url_key?(new_url_key) do
|
32
|
+
new_url_key = UrlKeyedObject.generate_unchecked_url_key
|
33
|
+
end
|
34
|
+
write_attribute(:url_key, new_url_key)
|
35
|
+
end
|
36
|
+
|
37
|
+
def valid_url_key?(url_key)
|
38
|
+
self.class.find_by_url_key(url_key).nil?
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module UrlKeyedObject
|
2
|
+
# --
|
3
|
+
# module-only methods
|
4
|
+
# ++
|
5
|
+
class << self
|
6
|
+
# holds an array so a base-31 column (units / tens / hundreds) equiv can be
|
7
|
+
# encoded as an ASCII character
|
8
|
+
def key_encoding
|
9
|
+
@key_encoding ||= ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
|
10
|
+
"a", "b", "c", "d", "e", "f", "g", "h", "j", "k", "m",
|
11
|
+
"n", "p", "q", "r", "s", "v", "w", "x", "y", "z"]
|
12
|
+
end
|
13
|
+
|
14
|
+
# generates a url key from a random 5-digit base 31 number, without checking its uniqueness
|
15
|
+
def generate_unchecked_url_key
|
16
|
+
(1..5).collect do
|
17
|
+
key_encoding[rand(31)]
|
18
|
+
end.join('')
|
19
|
+
end
|
20
|
+
|
21
|
+
# Validate the well-formedness of a URL key
|
22
|
+
def well_formed_url_key?(url_key)
|
23
|
+
!url_key.match(/^[0-9abcdefghjkmnpqrsvwxyz]{5}$/).nil?
|
24
|
+
end
|
25
|
+
|
26
|
+
# decode a moderately dirty URL key
|
27
|
+
def decode_url_key(url_key)
|
28
|
+
url_key.downcase.tr('o', '0').tr('il', '1')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
require 'url_keyed_object/active_record'
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__))
|
2
|
+
$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
require 'url_keyed_object'
|
4
|
+
require 'spec'
|
5
|
+
require 'spec/autorun'
|
6
|
+
require 'rubygems'
|
7
|
+
gem 'activerecord'
|
8
|
+
|
9
|
+
Spec::Runner.configure do |config|
|
10
|
+
config.mock_with :mocha
|
11
|
+
end
|
12
|
+
|
13
|
+
Spec::Matchers.define :use_method do |method|
|
14
|
+
chain :for_callback do |callback|
|
15
|
+
@callback = callback
|
16
|
+
end
|
17
|
+
|
18
|
+
match do |model_class|
|
19
|
+
model_class.send("#{@callback.to_s}_callback_chain").find(method)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Class
|
24
|
+
def publicize_methods
|
25
|
+
saved_private_instance_methods = self.private_instance_methods
|
26
|
+
saved_protected_instance_methods = self.protected_instance_methods
|
27
|
+
self.class_eval do
|
28
|
+
public *saved_private_instance_methods
|
29
|
+
public *saved_protected_instance_methods
|
30
|
+
end
|
31
|
+
|
32
|
+
yield
|
33
|
+
|
34
|
+
self.class_eval do
|
35
|
+
private *saved_private_instance_methods
|
36
|
+
protected *saved_protected_instance_methods
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'active_record'
|
3
|
+
require 'tmpdir'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'logger'
|
6
|
+
|
7
|
+
class SpecMigration < ActiveRecord::Migration
|
8
|
+
def self.up
|
9
|
+
create_table :things do |t|
|
10
|
+
t.string :url_key, :null => false
|
11
|
+
|
12
|
+
t.timestamps
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe UrlKeyedObject::ActiveRecord do
|
18
|
+
before(:all) do
|
19
|
+
@db_dir = ::Dir.mktmpdir
|
20
|
+
begin
|
21
|
+
# Create the SQLite database
|
22
|
+
ActiveRecord::Base.establish_connection({'adapter' => 'sqlite3',
|
23
|
+
'database' => "#{@db_dir}/spec.sqlite3", 'pool' => 5, 'timeout' => 5000
|
24
|
+
})
|
25
|
+
ActiveRecord::Base.connection
|
26
|
+
rescue
|
27
|
+
$stderr.puts $!, *($!.backtrace)
|
28
|
+
$stderr.puts "Couldn't create database for #{"#{@db_dir}/feature.sqlite3".inspect}"
|
29
|
+
end
|
30
|
+
|
31
|
+
SpecMigration.migrate(:up)
|
32
|
+
|
33
|
+
@url_keyed_class = Class.new(ActiveRecord::Base)
|
34
|
+
@url_keyed_class.class_eval do
|
35
|
+
set_table_name 'things'
|
36
|
+
def self.name
|
37
|
+
'UrlKeyedObjectMock'
|
38
|
+
end
|
39
|
+
|
40
|
+
include UrlKeyedObject::ActiveRecord
|
41
|
+
end
|
42
|
+
|
43
|
+
ActiveRecord::Base.logger = Logger.new(STDERR)
|
44
|
+
end
|
45
|
+
|
46
|
+
after(:all) do
|
47
|
+
ActiveRecord::Base.connection.disconnect!
|
48
|
+
FileUtils.remove_entry_secure @db_dir
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should protect URL keys from mass-assignment" do
|
52
|
+
url_keyed_object = @url_keyed_class.new(:url_key => 'woo')
|
53
|
+
|
54
|
+
url_keyed_object.url_key.should be_nil
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should not allow URL keys to be set" do
|
58
|
+
url_keyed_object = @url_keyed_class.new
|
59
|
+
|
60
|
+
url_keyed_object.url_key = 'dfghj'
|
61
|
+
|
62
|
+
url_keyed_object.url_key.should be_nil
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "ensuring unique URL keys get created " do
|
66
|
+
it "should be able to verify that a URL key is valid" do
|
67
|
+
@url_keyed_class.expects(:find_by_url_key).with('a_url').returns(nil)
|
68
|
+
url_keyed_object = @url_keyed_class.new
|
69
|
+
|
70
|
+
@url_keyed_class.publicize_methods do
|
71
|
+
url_keyed_object.valid_url_key?('a_url').should be_true
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should be able to verify that a URL key is not valid" do
|
76
|
+
@url_keyed_class.expects(:find_by_url_key).with('a_url').returns(@url_keyed_class.new)
|
77
|
+
url_keyed_object = @url_keyed_class.new
|
78
|
+
|
79
|
+
@url_keyed_class.publicize_methods do
|
80
|
+
url_keyed_object.valid_url_key?('a_url').should be_false
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should be able to keep attempting to create a URL key until one is valid" do
|
85
|
+
url_keyed_object = @url_keyed_class.new
|
86
|
+
valid_url_sequence = sequence('valid url sequence')
|
87
|
+
|
88
|
+
UrlKeyedObject.expects(:generate_unchecked_url_key).in_sequence(valid_url_sequence).returns('a1url')
|
89
|
+
url_keyed_object.expects(:valid_url_key?).in_sequence(valid_url_sequence).returns(false)
|
90
|
+
UrlKeyedObject.expects(:generate_unchecked_url_key).in_sequence(valid_url_sequence).returns('a2url')
|
91
|
+
url_keyed_object.expects(:valid_url_key?).in_sequence(valid_url_sequence).returns(true)
|
92
|
+
|
93
|
+
@url_keyed_class.publicize_methods do
|
94
|
+
url_keyed_object.generate_valid_url_key
|
95
|
+
|
96
|
+
url_keyed_object.url_key.should == 'a2url'
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should use a before_create callback to ensure that a URL key is created" do
|
101
|
+
@url_keyed_class.should use_method(:generate_valid_url_key).for_callback(:before_create)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe UrlKeyedObject do
|
4
|
+
it "should be able to generate a 5-character URL key" do
|
5
|
+
UrlKeyedObject.generate_unchecked_url_key.should match(/[0-9abcdefghjkmnpqrsvwxyz]{5}/)
|
6
|
+
end
|
7
|
+
|
8
|
+
describe "decoding a moderately dirty URL key (case insensitivity, ambiguous character replacement)" do
|
9
|
+
it "should be able to cope with O o 0 ambiguity" do
|
10
|
+
UrlKeyedObject.decode_url_key('Oo0ab').should == '000ab'
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should be able to cope with 1 I i L l ambiguity" do
|
14
|
+
UrlKeyedObject.decode_url_key('1IiLl').should == '11111'
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should be able to cope with uppercase" do
|
18
|
+
UrlKeyedObject.decode_url_key('ABCDE').should == 'abcde'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "well-formedness of URL keys" do
|
23
|
+
it "should be able to confirm that a URL key is well-formed" do
|
24
|
+
UrlKeyedObject.well_formed_url_key?('0ab9d').should be_true
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should report that malformed URL keys are NOT well-formed" do
|
28
|
+
['DfZZZ', # upper case
|
29
|
+
'abcdef', # too long
|
30
|
+
'iabcd', # ambiguous character
|
31
|
+
'tabcd', # ambiguous character
|
32
|
+
'uabcd', # ambiguous character
|
33
|
+
'_abcd', # illegal character
|
34
|
+
'ab,cd', # illegal character
|
35
|
+
'ab.cd', # illegal character
|
36
|
+
].each do |malformed_url_key|
|
37
|
+
UrlKeyedObject.well_formed_url_key?(malformed_url_key).should be_false
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
metadata
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: url_keyed_object
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matt Patterson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-01-19 00:00:00 +00:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rspec
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.2.9
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: cucumber
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0.5"
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: activerecord
|
37
|
+
type: :development
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: "2.3"
|
44
|
+
version:
|
45
|
+
description: Making it easy to work with Rails objects which use a URL key in their URL instead of their database ID.
|
46
|
+
email: matt@reprocessed.org
|
47
|
+
executables: []
|
48
|
+
|
49
|
+
extensions: []
|
50
|
+
|
51
|
+
extra_rdoc_files:
|
52
|
+
- LICENSE
|
53
|
+
- README.rdoc
|
54
|
+
files:
|
55
|
+
- .document
|
56
|
+
- .gitignore
|
57
|
+
- LICENSE
|
58
|
+
- README.rdoc
|
59
|
+
- Rakefile
|
60
|
+
- VERSION
|
61
|
+
- cucumber.yml
|
62
|
+
- features/fixtures/.gitignore
|
63
|
+
- features/step_definitions/database_steps.rb
|
64
|
+
- features/step_definitions/eval_steps.rb
|
65
|
+
- features/step_definitions/feedback_steps.rb
|
66
|
+
- features/support/env.rb
|
67
|
+
- features/support/hooks.rb
|
68
|
+
- features/support/recording_logger.rb
|
69
|
+
- features/with_active_record-basic.feature
|
70
|
+
- lib/url_keyed_object.rb
|
71
|
+
- lib/url_keyed_object/active_record.rb
|
72
|
+
- spec/spec.opts
|
73
|
+
- spec/spec_helper.rb
|
74
|
+
- spec/url_keyed_object/active_record_spec.rb
|
75
|
+
- spec/url_keyed_object_spec.rb
|
76
|
+
has_rdoc: true
|
77
|
+
homepage: http://github.com/fidothe/url_keyed_object
|
78
|
+
licenses: []
|
79
|
+
|
80
|
+
post_install_message:
|
81
|
+
rdoc_options:
|
82
|
+
- --charset=UTF-8
|
83
|
+
require_paths:
|
84
|
+
- lib
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: "0"
|
90
|
+
version:
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: "0"
|
96
|
+
version:
|
97
|
+
requirements: []
|
98
|
+
|
99
|
+
rubyforge_project:
|
100
|
+
rubygems_version: 1.3.5
|
101
|
+
signing_key:
|
102
|
+
specification_version: 3
|
103
|
+
summary: Making it easy to work with objects with URL keys
|
104
|
+
test_files:
|
105
|
+
- spec/spec_helper.rb
|
106
|
+
- spec/url_keyed_object/active_record_spec.rb
|
107
|
+
- spec/url_keyed_object_spec.rb
|