<file_sep># encoding: UTF-8
# Copyright © 2013, 2014, Watu

require "rubygems"

# Test coverage
require "simplecov"
require "coveralls"
SimpleCov.start do
  add_filter "/test/"
end
Coveralls.wear! # Comment out this line to have the local coverage generated.

require "minitest/autorun"
require "minitest/reporters"
MiniTest::Reporters.use!

# This class is here only to trick shoulda into attaching itself to MiniTest due to: https://github.com/thoughtbot/shoulda-context/issues/38
module ActiveSupport
  class TestCase < MiniTest::Unit::TestCase
  end
end
require "shoulda"
require "shoulda-context"
require "shoulda-matchers"

require "mocha/setup"

# Database setup
require "active_record"
require "logger"
ActiveRecord::Base.logger = Logger.new(STDERR)
ActiveRecord::Base.logger.level = Logger::WARN
ActiveRecord::Base.configurations = {"sqlite3" => {adapter: "sqlite3", database: ":memory:"}}
ActiveRecord::Base.establish_connection("sqlite3")

# Make the code to be tested easy to load.
$LOAD_PATH.unshift(File.dirname(__FILE__))
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
<file_sep># encoding: UTF-8
# Copyright © 2013, 2014, Watu

module RandomUniqueId
  VERSION = "0.2.1"
end
<file_sep># encoding: UTF-8
# Copyright © 2011, 2012, 2013, 2014, Watu

require_relative "test_helper"

require "random_unique_id"

ActiveRecord::Schema.define(version: 0) do
  create_table :blogs do |t|
    t.string :rid
    t.string :name
  end
  add_index :blogs, :rid, unique: true

  create_table :posts do |t|
    t.string :rid
    t.string :type
    t.integer :blog_id
  end
  add_index :posts, :rid, unique: true
end

class Blog < ActiveRecord::Base
  has_many :posts
  has_random_unique_id
end

class Post < ActiveRecord::Base
  belongs_to :blog
  has_random_unique_id
end

class TextPost < Post
end

class ImagePost < Post
end


class RandomUniqueIdTest < MiniTest::Unit::TestCase
  context "With a record with random id" do
    setup { @text_post = TextPost.create! }

    should "generate a random id" do
      assert @text_post.rid
    end

    should "return random id as param" do
      assert_equal @text_post.rid, @text_post.to_param
    end

    should "resolve random id collision" do
      # Mock RandomUniqueId to return a collision on the first call, and hopefully a non collision on the second, expecting n to grow by one.
      RandomUniqueId.expects(:generate_random_id).with(5).returns(@text_post.rid)
      new_rid = @text_post.rid + "i"
      RandomUniqueId.expects(:generate_random_id).with(6).returns(new_rid)

      new_record = TextPost.create! # No exception should be raised.
      assert_equal new_rid, new_record.rid
    end

    should "resolve random id collision in different classes of the same table (due to STI)" do
      # Mock RandomUniqueId to return a collision on the first call, and hopefully a non collision on the second, expecting n to grow by one.
      RandomUniqueId.expects(:generate_random_id).with(5).returns(@text_post.rid)
      new_rid = @text_post.rid + "i"
      RandomUniqueId.expects(:generate_random_id).with(6).returns(new_rid)

      new_record = ImagePost.create! # No exception should be raised.
      assert_equal new_rid, new_record.rid
    end

    should "have automatic *_rid= and *_rid methods" do
      blog = Blog.create!

      @text_post.blog_rid = blog.rid
      @text_post.save!

      assert_equal blog, @text_post.blog
      assert_equal blog.rid, @text_post.blog_rid
    end

    should "populate a table with rids" do
      # Create a bunch of blogs without rid by manually inserting them into the talbe.
      rid_less_records = 10
      5.times { Blog.create! }
      existing_rids = Blog.all.map(&:rid).compact
      rid_less_records.times { Blog.connection.execute("INSERT INTO blogs (name) VALUES ('Blag')") }
      assert_equal rid_less_records, Blog.where(:rid => nil).count # Just to be sure this test is being effective.

      rids_populated = 0
      Blog.populate_random_unique_ids { |_, rid_just_populated| rids_populated += 1 if rid_just_populated }
      assert_equal rid_less_records, rids_populated
      assert_equal 0, Blog.where(:rid => nil).count
      assert_equal existing_rids.count, Blog.where(:rid => existing_rids).count # Make sure the existing rids where not touched.
    end
  end
end
<file_sep># encoding: UTF-8
# Copyright © 2013, 2014, Watu

source "https://rubygems.org"

gem "bundler", "~> 1.3"
gem "coveralls", require: false
gem "minitest", "~> 4.7.5"
gem "minitest-reporters"
gem "mocha"
gem "rake"
gem "simplecov"
gem "shoulda"
gem "sqlite3"
gem "activesupport", "~> 3.2.0"
gem "activerecord", "~> 3.2.0"
<file_sep># Random Unique ID

[![Build Status](https://travis-ci.org/watu/random_unique_id.png?branch=master)](https://travis-ci.org/watu/random_unique_id)
[![Coverage Status](https://coveralls.io/repos/watu/random_unique_id/badge.png?branch=master)](https://coveralls.io/r/watu/random_unique_id?branch=master)
[![Code Climate](https://codeclimate.com/github/watu/random_unique_id.png)](https://codeclimate.com/github/watu/random_unique_id)
[![Inline docs](http://inch-ci.org/github/watu/random_unique_id.png)](http://inch-ci.org/github/watu/random_unique_id)
[![Gem Version](https://badge.fury.io/rb/random_unique_id.png)](http://badge.fury.io/rb/random_unique_id)

This gem will generate a random unique id for your active record records that you can use instead of their actual ID for
all external interactions with users. The goal is for you to be able to hide how many records you have, for business
purposes, but also to make IDs non-predictable.

This gem is built to work with Ruby 1.9, 2.0, 2.1 as well as with Rails 3.2 and 4.0. All of these cases are
[continuously tested for](https://travis-ci.org/watu/random_unique_id.png?branch=master).

## Installation

Add this line to your application's Gemfile:

    gem "random_unique_id"

And then execute:

    $ bundle

Or install it yourself as:

    $ gem install random_unique_id

## Usage

The usage is very simple. For each record where you want to have a random id generated, add the following line to the
class:

    has_random_unique_id

For example:

    class Post < ActiveRecord::Base
      has_random_unique_id
    end

You need to also add a column, called `rid` of type string/varchar. It is recommended that you also add a unique index
on that column, for example:

    def up
      add_column :posts, :rid, :string
      add_index :posts, :rid, :unique
    end

The method `to_param` will be overridden to return the `rid` instead of the `id`. The method `belongs_to` gets extended
to define `_rid` methods similar to the `_id` method, like: `blog_rid` and `blog_rid=`. If you don't want to define
those pass `define_rid_method` as false, for example:

    class Post
      belongs_to :blog, define_rid_method: false
    end

Classes that have rids also get a method called `populate_random_unique_ids` to help you populate the rid of existing
records. For example:

    def up
      add_column :posts, :rid, :string
      add_index :posts, :rid, :unique
      say_with_time "Post.populate_random_unique_ids" do
        Post.reset_column_information
        Post.populate_random_unique_ids { print "."}
      end
    end

## Changelog

### Version 0.2.0
- Added method populate_random_unique_ids.
- Improved documentation
- Started testing with Ruby 2.1.

### Version 0.1.0
- Initial release of the code extracted from [Watu](http://github.com/watu).

## Contributing

1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am "Add some feature"`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request
<file_sep># encoding: UTF-8
# Copyright © 2011, 2012, 2013, 2014, Watu

require "random_unique_id/version"
require "securerandom"
require "active_support"
require "active_record"

module RandomUniqueId
  extend ActiveSupport::Concern

  module ClassMethods
    # Mark a model as containing a random unique id. A field called rid of type string is required. It's recommended
    # that it's indexed and unique. For example, you could add it to a migration like this:
    #   def up
    #     add_column :posts, :rid, :string
    #     add_index :posts, :rid, :unique
    #   end
    #
    # and then to the model like this:
    #   class Post
    #     has_random_unique_id
    #     # ... other stuff
    #   end
    def has_random_unique_id
      validates :rid, presence: true, uniqueness: true
      before_validation :generate_random_unique_id, if: Proc.new { |r| r.rid.blank? }
      define_method(:to_param) { rid }
    end

    def belongs_to(*attrs)
      define_rid_method = attrs[1].try(:delete, :rid)
      super.tap do
        if define_rid_method != false
          relationship_name = attrs[0]
          rel = reflections[relationship_name]

          return if rel.options[:polymorphic] # If we don't know the class, we cannot find the record by rid.

          class_name = rel.options[:class_name] || relationship_name.to_s.classify
          related_class = class_name.constantize
          define_rid_accessors(related_class, relationship_name) if related_class.attribute_names.include? "rid"
        end
      end
    end

    # Populate all the blank rids in a table. This is useful when adding rids to a table that already has data in it.
    # For example:
    #   def up
    #     add_column :posts, :rid, :string
    #     add_index :posts, :rid, :unique
    #     say_with_time "Post.populate_random_unique_ids" do
    #       Post.reset_column_information
    #       Post.populate_random_unique_ids { print "."}
    #     end
    #   end
    #
    # This method uses update_column to avoid running validations and callbacks. It will not change existing rids, so
    # it's safe to call several times and a failure (even without a transaction) is not catastrophic.
    def populate_random_unique_ids
      find_each do |record|
        rid_just_populated = false
        if record.rid.blank?
          record.generate_random_unique_id
          record.update_column(:rid, record.rid)
          rid_just_populated = true
        end
        yield(record, rid_just_populated) if block_given?
      end
    end

    private

    def define_rid_accessors(related_class, relationship_name)
      define_method("#{relationship_name}_rid") do
        self.send(relationship_name).try(:rid)
      end

      define_method("#{relationship_name}_rid=") do |rid|
        record = related_class.find_by_rid(rid)
        self.send("#{relationship_name}=", record)
        record
      end
    end
  end

  def generate_random_unique_id(n=5, field="rid")
    # Find the topmost class before ActiveRecord::Base so that when we do queries, we don't end up with type=Whatever in the where clause.
    klass = self.class
    self.class.ancestors.each do |k|
      if k == ActiveRecord::Base
        break # we reached the bottom of this barrel
      end
      if k.is_a? Class
        klass = k
      end
    end

    begin
      self.send("#{field}=", RandomUniqueId.generate_random_id(n))
      n += 1
    end while klass.unscoped.where(field => self.send(field)).exists?
  end

  def self.generate_random_id(n=10)
    # IMPORTANT: don't ever generate dashes or underscores in the RIDs as they are likely to end up in the UI in Rails
    # and they'll be converted to something else by jquery ujs or something like that.
    generated_rid = ""
    while generated_rid.length < n
      generated_rid = (generated_rid + SecureRandom.urlsafe_base64(n * 3).downcase.gsub(/[^a-z0-9]/, ""))[0..(n-1)]
    end
    return generated_rid
  end
end

ActiveRecord::Base.send(:include, RandomUniqueId)
