Skip to content

Instantly share code, notes, and snippets.

@mhenrixon
Last active August 6, 2024 07:00
Show Gist options
  • Save mhenrixon/e2b86bfe4112bf79f676ca5b86ea7e35 to your computer and use it in GitHub Desktop.
Save mhenrixon/e2b86bfe4112bf79f676ca5b86ea7e35 to your computer and use it in GitHub Desktop.
# frozen_string_literal: true
module Taggable
extend ActiveSupport::Concern
TYPE_MATCHER = { string: "varchar", text: "text", integer: "integer", citext: "citext" }.freeze
class_methods do
def has_tags(tag_name) #rubocop:disable Metrics/BlockLength, Metrics/AbcSize, Metrics/MethodLength, Naming/PredicateName
tag_array_type_fetcher = -> { TYPE_MATCHER[columns_hash[tag_name.to_s].type] }
scope :"with_any_#{tag_name}", lambda { |tags|
where("#{table_name}.#{tag_name} && ARRAY[?]::#{tag_array_type_fetcher.call}[]",
parse_tags(tags),
)
}
scope :"with_all_#{tag_name}", lambda { |tags|
where("#{table_name}.#{tag_name} @> ARRAY[?]::#{tag_array_type_fetcher.call}[]",
parse_tags(tags),
)
}
scope :"without_any_#{tag_name}", lambda { |tags|
where.not(
"#{table_name}.#{tag_name} && ARRAY[?]::#{tag_array_type_fetcher.call}[]", parse_tags(tags)
)
}
scope :"without_all_#{tag_name}", lambda { |tags|
where.not(
"#{table_name}.#{tag_name} @> ARRAY[?]::#{tag_array_type_fetcher.call}[]", parse_tags(tags)
)
}
define_method :"#{tag_name}=" do |obj|
super(parse_tags(obj))
end
define_singleton_method :"all_#{tag_name}" do |_options = {}, &block|
subquery_scope = unscoped.select("unnest(#{table_name}.#{tag_name}) as tag").distinct
subquery_scope = subquery_scope.instance_eval(&block) if block
unscope(where: :type).from(subquery_scope).pluck(:tag)
end
define_singleton_method :"#{tag_name}_cloud" do |_options = {}, &block|
subquery_scope = unscoped.select("unnest(#{table_name}.#{tag_name}) as tag")
subquery_scope = subquery_scope.instance_eval(&block) if block
unscope(where: :type).from(subquery_scope).group(:tag).order(:tag).count(:tag)
end
define_singleton_method :parse_tags do |obj|
case obj
when String
obj.split(/\s*,\s*/).compact_blank.uniq
when Array
obj.flatten.map(&:to_s).compact_blank.uniq
else
obj
end
end
class_eval do
define_method :parse_tags do |obj|
self.class.parse_tags(obj)
end
end
end
end
end
# frozen_string_literal: true
# spec/concerns/taggable_spec.rb
require "rails_helper"
RSpec.describe Taggable do
let(:author) { create(:super_admin) }
let(:article) { create(:article, author:) }
describe "scopes" do
before do
create(:article, author:, tags: %w[ruby rails])
create(:article, author:, tags: %w[ruby javascript])
create(:article, author:, tags: %w[python django])
end
it "defines with_any_tags scope" do
expect(Article.with_any_tags(%w[ruby python]).count).to eq(3)
end
it "defines with_all_tags scope" do
expect(Article.with_all_tags(%w[ruby rails]).count).to eq(1)
end
it "defines without_any_tags scope" do
expect(Article.without_any_tags(["ruby"]).count).to eq(1)
end
it "defines without_all_tags scope" do
expect(Article.without_all_tags(%w[ruby rails]).count).to eq(2)
end
end
describe "#tags=" do
it "parses string input" do
article.tags = "ruby, rails, web"
expect(article.tags).to eq(%w[ruby rails web])
end
it "handles array input" do
article.tags = %w[ruby rails web]
expect(article.tags).to eq(%w[ruby rails web])
end
it "removes duplicates" do
article.tags = "ruby, rails, ruby, web"
expect(article.tags).to eq(%w[ruby rails web])
end
it "removes blank entries" do
article.tags = "ruby, , rails, ,web"
expect(article.tags).to eq(%w[ruby rails web])
end
end
describe ".all_tags" do
before do
create(:article, author:, tags: %w[ruby rails])
create(:article, author:, tags: %w[ruby javascript])
create(:article, author:, tags: %w[python django])
end
it "returns all unique tags" do
expect(Article.all_tags).to match_array(%w[ruby rails javascript python django])
end
end
describe ".tags_cloud" do
before do
create(:article, author:, tags: %w[ruby rails])
create(:article, author:, tags: %w[ruby javascript])
create(:article, author:, tags: %w[python django])
end
it "returns tag counts" do
expected_cloud = {
"ruby" => 2,
"rails" => 1,
"javascript" => 1,
"python" => 1,
"django" => 1,
}
expect(Article.tags_cloud).to eq(expected_cloud)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment