-
Notifications
You must be signed in to change notification settings - Fork 64
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Integrating AvramSlugify directly in. (#786)
- Loading branch information
Showing
7 changed files
with
234 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
class CreateArticles::V20220111192510 < Avram::Migrator::Migration::V1 | ||
def migrate | ||
create table_for(Article) do | ||
primary_key id : Int64 | ||
add_timestamps | ||
add title : String | ||
add sub_heading : String? | ||
add slug : String | ||
end | ||
end | ||
|
||
def rollback | ||
drop table_for(Article) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
require "../spec_helper" | ||
|
||
describe Avram::Slugify do | ||
describe ".set" do | ||
it "does not set anything if slug is already set" do | ||
op = build_op(title: "Writing Specs") | ||
|
||
slugify(op.slug, "Writing Specs") | ||
|
||
op.slug.value.should eq("writing-specs") | ||
end | ||
|
||
it "skips blank slug candidates" do | ||
op = build_op(title: "Software Developer") | ||
|
||
slugify(op.slug, ["", op.title]) | ||
|
||
op.slug.value.should eq("software-developer") | ||
end | ||
|
||
describe "with a single slug candidate" do | ||
it "it sets slug from a single attribute" do | ||
op = build_op(title: "Software Developer") | ||
|
||
slugify(op.slug, op.title) | ||
|
||
op.slug.value.should eq("software-developer") | ||
end | ||
|
||
it "it sets slug from a single string" do | ||
op = build_op | ||
|
||
slugify(op.slug, "Software Developer") | ||
|
||
op.slug.value.should eq("software-developer") | ||
end | ||
|
||
it "it sets slug from a single string" do | ||
op = build_op | ||
|
||
slugify(op.slug, "Software Developer") | ||
|
||
op.slug.value.should eq("software-developer") | ||
end | ||
end | ||
|
||
describe "with an array of slug candidates" do | ||
describe "and there is no other record with the same slug" do | ||
it "sets using a String" do | ||
op = build_op | ||
|
||
slugify(op.slug, ["Software Developer"]) | ||
|
||
op.slug.value.should eq("software-developer") | ||
end | ||
|
||
it "sets using an attribute" do | ||
op = build_op(title: "Software Developer") | ||
|
||
slugify(op.slug, [op.title]) | ||
|
||
op.slug.value.should eq("software-developer") | ||
end | ||
|
||
it "sets when using multiple attributes" do | ||
op = build_op(title: "How Do Magnets Work?", sub_heading: "And Why?") | ||
|
||
slugify(op.slug, [[op.title, op.sub_heading]]) | ||
|
||
op.slug.value.should eq("how-do-magnets-work-and-why") | ||
end | ||
end | ||
|
||
describe "and the first slug candidate is not unique" do | ||
it "chooses the first unique one in the array" do | ||
ArticleFactory.create &.slug("music") | ||
ArticleFactory.create &.slug("programming") | ||
op = build_op(title: "Music", sub_heading: "Programming") | ||
|
||
slugify(op.slug, [op.title, "programming", [op.title, op.sub_heading]]) | ||
|
||
op.slug.value.should eq("music-programming") | ||
end | ||
end | ||
|
||
describe "and all of the slug candidates are used already" do | ||
it "uses the first present candidate and appends a UUID" do | ||
ArticleFactory.create &.slug("pizza") | ||
ArticleFactory.create &.slug("tacos") | ||
op = build_op(title: "Pizza", sub_heading: "Tacos") | ||
|
||
# First string is empty. Added to make sure it is not used with | ||
# the UUID. | ||
slugify(op.slug, ["", op.title, op.sub_heading]) | ||
|
||
op.slug.value.to_s.should start_with("pizza-") | ||
op.slug.value.to_s.split("-", 2).last.size.should eq(UUID.random.to_s.size) | ||
end | ||
end | ||
|
||
describe "all slug candidates are blank" do | ||
it "leaves the slug as nil" do | ||
op = build_op(title: "") | ||
|
||
# First string is empty. Added to make sure it is not used with | ||
# the UUID. | ||
slugify(op.slug, ["", op.title]) | ||
|
||
op.slug.value.should be_nil | ||
end | ||
end | ||
end | ||
|
||
it "uses the query to scope uniqueness check" do | ||
ArticleFactory.create &.slug("the-boss").title("A") | ||
|
||
op = build_op(title: "The Boss") | ||
slugify(op.slug, op.title, ArticleQuery.new.title("B")) | ||
op.slug.value.should eq("the-boss") | ||
|
||
op = build_op(title: "The Boss") | ||
slugify(op.slug, op.title, ArticleQuery.new.title("A")) | ||
op.slug.value.to_s.should start_with("the-boss-") # Has UUID appended | ||
end | ||
end | ||
end | ||
|
||
private def slugify(slug, slug_candidates, query = ArticleQuery.new) | ||
Avram::Slugify.set(slug, slug_candidates, query) | ||
end | ||
|
||
private def build_op(**named_args) | ||
Article::SaveOperation.new(**named_args) | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
class ArticleFactory < BaseFactory | ||
def initialize | ||
title "The Great Title" | ||
slug "the-great-title" | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
class Article < BaseModel | ||
COLUMN_SQL = "articles.id, articles.created_at, articles.updated_at, articles.title, articles.slug" | ||
|
||
table do | ||
column title : String | ||
column sub_heading : String? | ||
column slug : String | ||
end | ||
end | ||
|
||
class ArticleQuery < Article::BaseQuery | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
# Converts a column value to a URL safe String that | ||
# can be used as a parameter for finding records. A | ||
# `slug` is a `String` column you define on your model | ||
# that will be passed through the URL instead of an `id`. | ||
# | ||
# e.g. /articles/1 -> /articles/how-to-slugify | ||
# | ||
# Use this module in your `SaveOperation#before_save`. | ||
# | ||
# ``` | ||
# class Article < BaseModel | ||
# table do | ||
# column title : String | ||
# column slug : String | ||
# end | ||
# end | ||
# | ||
# class SaveArticle < Article::SaveOperation | ||
# before_save do | ||
# Avram::Slugify.set slug, | ||
# using: title, | ||
# query: ArticleQuery.new | ||
# end | ||
# end | ||
# ``` | ||
module Avram::Slugify | ||
extend self | ||
|
||
def set(slug : Avram::Attribute(String), | ||
using slug_candidate : Avram::Attribute(String) | String, | ||
query : Avram::Queryable) : Nil | ||
set(slug, [slug_candidate], query) | ||
end | ||
|
||
def set(slug : Avram::Attribute(String), | ||
using slug_candidates : Array(String | Avram::Attribute(String) | Array(Avram::Attribute(String))), | ||
query : Avram::Queryable) : Nil | ||
if slug.value.blank? | ||
slug_candidates = slug_candidates.map do |candidate| | ||
parameterize(candidate) | ||
end.reject(&.blank?) | ||
|
||
slug_candidates.find { |candidate| query.where(slug.name, candidate).none? } | ||
.tap { |candidate| slug.value = candidate } | ||
end | ||
|
||
if slug.value.blank? && (candidate = slug_candidates.first?) | ||
slug.value = "#{candidate}-#{UUID.random}" | ||
end | ||
end | ||
|
||
private def parameterize(value : String) : String | ||
Cadmium::Transliterator.parameterize(value) | ||
end | ||
|
||
private def parameterize(value : Avram::Attribute(String)) : String | ||
parameterize(value.value.to_s) | ||
end | ||
|
||
private def parameterize(values : Array(Avram::Attribute(String))) : String | ||
values.join("-") { |value| parameterize(value) } | ||
end | ||
end |