Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Created a hash decoder for hashed ids within Rails params. #52

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@
/spec/reports/
/tmp/
/.ruby-version
/.idea
/.tm_properties
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# Changelog


## 1.2.3 (2019-10-25)
- Added ability to decode hashids within Rails inbound parameter hashes.

## 1.2.2 (2018-07-29)
### Fixed
- Handle exception raised when using a letter-only alphabet and attempting to
decode an integer ID from [@Drakula2k](https://github.com/Drakula2k) ([#54](https://github.com/jcypret/hashid-rails/pull/54)).

Expand Down
2 changes: 1 addition & 1 deletion hashid-rails.gemspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

lib = File.expand_path("../lib", __FILE__)
lib = File.expand_path("lib", __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require "hashid/rails/version"

Expand Down
2 changes: 1 addition & 1 deletion lib/hashid/rails.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

require "hashid/rails/version"
require "hashid/rails/configuration"
require "hashid/rails/decoder"
require "hashids"
require "active_record"

Expand Down
93 changes: 93 additions & 0 deletions lib/hashid/rails/decoder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# frozen_string_literal: true

require "hashid/rails/parameter_patterns"

module Hashid
module Rails
class Decoder
class << self
include ParameterPatterns

def decode_params(parent_class, params)
_decode_params(parent_class, params)
end

private

def _decode_params(parent_class, params, child_name = nil)
dup = HashWithIndifferentAccess.new(params.dup)
dup.update(dup) do |k, v|
if requires_decoding?(k)
decode(parent_class, k, v, child_name)
else
v
end
end
end

def requires_decoding?(key)
id?(key) ||
association?(key) ||
associations?(key) ||
nested_association?(key) ||
nested_associations?(key)
end

# rubocop:disable Metrics/MethodLength
def decode(parent_class, key, value, child_name)
if id?(key)
decode_id(parent_class, value)
elsif association?(key)
decode_association(parent_class, key, value)
elsif associations?(key)
decode_associations(parent_class, key, value)
elsif nested_association?(key)
decode_nested_association(parent_class, value, child_name)
elsif nested_associations?(key)
decode_nested_associations(parent_class, key, value)
end
end
# rubocop:enable Metrics/MethodLength

def decode_id(parent_class, hashid)
parent_class.decode_id(hashid, fallback: true)
rescue NoMethodError
hashid
end

def decode_child_id(parent_class, hashid, child_name)
association = parent_class.reflect_on_association(child_name)
return hashid if association.nil?
decode_id(association.klass, hashid)
end

def decode_child_ids(parent_class, hashids, child_name)
hashids.map do |hashid|
decode_child_id(parent_class, hashid, child_name)
end
end

def decode_association(parent_class, key, value)
child_name = key.to_s.gsub("_id", "")
decode_child_id(parent_class, value, child_name)
end

def decode_associations(parent_class, key, value)
child_name = key.to_s.gsub("_ids", "s")
decode_child_ids(parent_class, value, child_name)
end

def decode_nested_association(parent_class, value, child_name)
association = parent_class.reflect_on_association(child_name)
child_class = association.klass
_decode_params(child_class, value)
end

def decode_nested_associations(parent_class, key, value)
child_name = key.to_s.gsub("_attributes", "")
_decode_params(parent_class, value, child_name)
end
end
end
end
end
27 changes: 27 additions & 0 deletions lib/hashid/rails/parameter_patterns.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module Hashid
module Rails
module ParameterPatterns
def id?(key)
(/^id$/ =~ key).present?
end

def association?(key)
(/_id$/ =~ key).present?
end

def associations?(key)
(/_ids$/ =~ key).present?
end

def nested_association?(key)
(/^[0-9]+$/ =~ key).present?
end

def nested_associations?(key)
(/_attributes$/ =~ key).present?
end
end
end
end
101 changes: 101 additions & 0 deletions spec/hashid/rails/decoder_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# frozen_string_literal: true

require "spec_helper"

describe Hashid::Rails::Decoder do
describe "#decode_params" do
let(:post) { Post.new(id: 1) }
let(:image) { Image.new(id: 1) }
let(:author) { Author.new(id: 1) }
let(:another_author) { Author.new(id: 2) }
let(:topic_one) { Topic.new(id: 1) }
let(:topic_two) { Topic.new(id: 2) }
let(:comment_one) { Comment.new(id: 1) }
let(:comment_two) { Comment.new(id: 2) }
let(:sponsor_id) { "acme-rockets-01" }
let(:post_params) do
{
id: post.to_param,
title: "Title"
}
end
let(:author_params) do
{
author_id: author.to_param
}
end
let(:topic_params) do
{
topic_ids: [
topic_one.to_param,
topic_two.to_param
]
}
end
let(:comments_params) do
{
comments_attributes: {
"0" => {
id: comment_one.to_param
},
"1" => {
id: comment_two.to_param
}
}
}
end
let(:image_params) do
{
image_id: image.to_param
}
end
let(:sponsor_params) do
{
sponsor_id: sponsor_id
}
end

it "decodes id and nothing else" do
actual = Hashid::Rails::Decoder.decode_params(Post, post_params)
expect(actual[:id]).to eq(post.id)
expect(actual[:title]).to eq("Title")
end

it "decodes belongs_to association" do
params = post_params.merge(author_params)
actual = Hashid::Rails::Decoder.decode_params(Post, params)
expect(actual[:author_id]).to eq(author.id)
end

it "decodes has_many association" do
params = post_params.merge(comments_params)
actual = Hashid::Rails::Decoder.decode_params(Post, params)
expect(actual[:comments_attributes]["0"][:id]).to eq(comment_one.id)
expect(actual[:comments_attributes]["1"][:id]).to eq(comment_two.id)
end

it "decodes has_and_belongs_to_many association" do
params = post_params.merge(topic_params)
actual = Hashid::Rails::Decoder.decode_params(Post, params)
expect(actual[:topic_ids][0]).to eq(topic_one.id)
expect(actual[:topic_ids][1]).to eq(topic_two.id)
end

it "passes over un-hashed ids" do
actual = Hashid::Rails::Decoder.decode_params(Post, id: post.id)
expect(actual[:id]).to eq(post.id)
end

it "handles when hashid not enabled" do
params = post_params.merge(image_params)
actual = Hashid::Rails::Decoder.decode_params(Post, params)
expect(actual[:image_id]).to eq(image.id.to_s)
end

it "handles params that look like associations but aren't" do
params = post_params.merge(sponsor_params)
actual = Hashid::Rails::Decoder.decode_params(Post, params)
expect(actual[:sponsor_id]).to eq(sponsor_id)
end
end
end
5 changes: 4 additions & 1 deletion spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@

require "byebug"

$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
$LOAD_PATH.unshift File.expand_path("lib", __dir__)
require "hashid/rails"

ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
require_relative "support/schema"
require_relative "support/fake_model"
require_relative "support/author"
require_relative "support/post"
require_relative "support/image"
require_relative "support/comment"
require_relative "support/topic"
7 changes: 7 additions & 0 deletions spec/support/author.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class Author < ActiveRecord::Base
include Hashid::Rails

has_many :posts
end
5 changes: 5 additions & 0 deletions spec/support/image.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

class Image < ActiveRecord::Base
belongs_to :post
end
3 changes: 3 additions & 0 deletions spec/support/post.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@
class Post < ActiveRecord::Base
include Hashid::Rails

has_one :image
belongs_to :author
has_many :comments
has_and_belongs_to_many :topics
end
15 changes: 14 additions & 1 deletion spec/support/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,21 @@
t.string :name
end

create_table :posts, force: true
create_table :authors, force: true
create_table :posts, force: true do |t|
t.belongs_to :author, index: true
t.string :title
t.integer :sponsor_id
end
create_table :images, force: true do |t|
t.belongs_to :post, index: true
end
create_table :comments, force: true do |t|
t.belongs_to :post, index: true
end
create_table :topics, force: true
create_table :posts_topics, id: false, force: true do |t|
t.integer :post_id
t.integer :topic_id
end
end
7 changes: 7 additions & 0 deletions spec/support/topic.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class Topic < ActiveRecord::Base
include Hashid::Rails

has_and_belongs_to_many :posts, join_table: :posts_topics
end