schnecke 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/Gemfile.lock +1 -1
- data/README.md +75 -14
- data/lib/schnecke/schnecke.rb +59 -15
- data/lib/schnecke/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bdf704d77a580357657db281c7fd5021045fcd5c2b7cca9f32f6f468cf33d895
|
|
4
|
+
data.tar.gz: 995fc538958ecf0ae7b1ce67cb81498e4223b3538782cc036a9763eefa1c0e4f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fd373216be05945333cc1ddd6850892151549db1e887cc64ce462ae4f98ee4e86b698e34d591b71f6fd667f5cb65ae8a3634a175a9186f88ee3c97035fcc06af
|
|
7
|
+
data.tar.gz: d62fc13e0f3151f338314d79e7664267812b2de7f5a70ef6e1ba5fbb24ae550a569bf7266858906ee8f7502387aa76bf7687b9f8cf3c73a6a65a0a584213604f
|
data/.rubocop.yml
CHANGED
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -22,10 +22,10 @@ Or install it yourself as:
|
|
|
22
22
|
|
|
23
23
|
## Usage
|
|
24
24
|
|
|
25
|
-
Given a class `
|
|
25
|
+
Given a class `SomeObject` which has the attribute `name` and has a `slug` column defined, we can do the following:
|
|
26
26
|
|
|
27
27
|
```ruby
|
|
28
|
-
class
|
|
28
|
+
class SomeObject
|
|
29
29
|
include Schnecke
|
|
30
30
|
slug :name
|
|
31
31
|
end
|
|
@@ -34,18 +34,37 @@ end
|
|
|
34
34
|
This will take the value in `name` and automatically set the slug based on it. If the slug needs to be based on multiple attributes of a model, simply pass in the array of attributes as follows:
|
|
35
35
|
|
|
36
36
|
```ruby
|
|
37
|
-
class
|
|
37
|
+
class SomeObject
|
|
38
38
|
include Schnecke
|
|
39
39
|
slug [:first_name, :last_name]
|
|
40
40
|
end
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
Under the hood, this library adds a `before_validate` callback that automatically runs a method called `assign_slug`. You are welcome to call this method explicity if you so desire.
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
obj = SomeObject.new(name: 'Hello World!')
|
|
47
|
+
obj.assign_slug
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
It is important to note that if the attribute used to hold the slug (`slug` by default, see next section) already contains a value, the slug assignment **WILL NOT HAPPEN**. This means, if you manually assign the slug by explicitly setting the slug value yourself, it will not be modified. If you would like the slug to be overwrriten you can explicitly call the `reassign_slug` method.
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
obj = SomeObject.new(name: 'Hello World!', slug: 'hi')
|
|
54
|
+
|
|
55
|
+
# This will do nothing as the slug was already set
|
|
56
|
+
obj.assign_slug
|
|
57
|
+
|
|
58
|
+
# This will cause the slug to be assigned
|
|
59
|
+
obj.reassign_slug
|
|
60
|
+
```
|
|
61
|
+
|
|
43
62
|
### Slug column
|
|
44
63
|
|
|
45
64
|
By default it is assumed that the generated slug will be assigned to the `slug` attribute of the model. If one needs to place the slug in a different columm, this can be done by defining the `column` attribute:
|
|
46
65
|
|
|
47
66
|
```ruby
|
|
48
|
-
class
|
|
67
|
+
class SomeObject
|
|
49
68
|
include Schnecke
|
|
50
69
|
slug :name, column: :some_other_column
|
|
51
70
|
end
|
|
@@ -53,36 +72,57 @@ end
|
|
|
53
72
|
|
|
54
73
|
The above will place the generated slug in `some_other_column`.
|
|
55
74
|
|
|
75
|
+
### Setting the maximum length of a slug
|
|
76
|
+
|
|
77
|
+
By default the maxium length of a slug is 32 characters *NOT INCLUDING* any potential sequence numbers added to make it unique (see the "Handling non-unique slugs" section). You can either change the maximum or remove it entirely as follows
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
class SomeObject
|
|
82
|
+
include Schnecke
|
|
83
|
+
slug :name, limit_length: 15
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
class SomeObject
|
|
89
|
+
include Schnecke
|
|
90
|
+
slug :name, limit_length: false
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
56
94
|
### Slug Uniquness
|
|
57
95
|
|
|
58
|
-
By default slugs are unique to the object that defines the slug. For example if we have the 2 objects, `
|
|
96
|
+
By default slugs are unique to the object that defines the slug. For example if we have the 2 objects, `SomeObject` and `SomeOtherObject` as defined as below, then the slugs will be unique for all slugs for all type `SomeObject` objcets and all type `SomeOtherObject` objects.
|
|
59
97
|
|
|
60
98
|
```ruby
|
|
61
|
-
class
|
|
99
|
+
class SomeObject
|
|
62
100
|
include Schnecke
|
|
63
101
|
slug :name
|
|
64
102
|
end
|
|
65
103
|
|
|
66
|
-
class
|
|
104
|
+
class SomeOtherObject
|
|
67
105
|
include Schnecke
|
|
68
106
|
slug :name
|
|
69
107
|
end
|
|
70
108
|
```
|
|
71
109
|
|
|
72
|
-
This means that the slug `foo` can exists 2 times; once for any object of type `
|
|
110
|
+
This means that the slug `foo` can exists 2 times; once for any object of type `SomeObject` and once for any object of type `SomeOtherObject`. Currently there is no way to create globally unique slugs. If this is something that is required, then something like [`friendly_id`](https://github.com/norman/friendly_id) might be more appropriate for your use case.
|
|
73
111
|
|
|
74
112
|
### Handling non-unique slugs
|
|
75
113
|
|
|
76
114
|
If a duplicate slug is to be created, a number is automatically appended to the end of the slug. For example, if there is a slug `foo`, the second one would become `foo-2`, the third `foo-3`, and so forth.
|
|
77
115
|
|
|
116
|
+
It is important to note that the maximum length of a slug does not include the addition of the sequence identifier at the end. By default the maximum length of a slug is 32 characters, but if a sequence number is added, it will be 34 characters when we append the `-2`, `-3`, etc. This was done on purpose so that the base slug always remains constant and does not get truncated.
|
|
117
|
+
|
|
78
118
|
### Defining a custom uniqueness scope
|
|
79
119
|
|
|
80
|
-
There are times when we want slugs not be unique for all objects of type `
|
|
120
|
+
There are times when we want slugs not be unique for all objects of type `SomeObject`, but rather for a smaller scope. For example, let's say we have a system with multiple `Accounts`, each containing `Record`s. If we want the slug for the `Record` to be unique only within the scope of an `account` we can do by providing the uniqueness scope when setting up the slug.
|
|
81
121
|
|
|
82
122
|
```ruby
|
|
83
123
|
class Record
|
|
84
124
|
include Schnecke
|
|
85
|
-
slug :name, uniqueness: { scope: :account}
|
|
125
|
+
slug :name, uniqueness: { scope: :account }
|
|
86
126
|
|
|
87
127
|
belongs_to :account
|
|
88
128
|
end
|
|
@@ -93,19 +133,40 @@ When we do this, this will let us have the same slug 'foo' for multiple `record`
|
|
|
93
133
|
```ruby
|
|
94
134
|
class Tag
|
|
95
135
|
include Schnecke
|
|
96
|
-
slug :name, uniqueness: { scope: [:account, :record]}
|
|
136
|
+
slug :name, uniqueness: { scope: [:account, :record] }
|
|
97
137
|
|
|
98
138
|
belongs_to :account
|
|
99
139
|
belongs_to :record
|
|
100
140
|
end
|
|
101
141
|
```
|
|
102
142
|
|
|
143
|
+
### Callbacks
|
|
144
|
+
|
|
145
|
+
Two callbacks, `before_assign_slug` and `after_assign_slug`, are provided so that you can run arbitrary code before and after the slug assignment process. Both of these callbacks will always run regardless of whether or not a slug is to be assigned. The only time `after_assign_slug` is not run is if there is an exception raised during the assignment process.
|
|
146
|
+
|
|
147
|
+
Note, since `reassign_slug` is just a forced assignment of a slug, both callbacks will run as well.
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
class SomeObject
|
|
151
|
+
include Schnecke
|
|
152
|
+
slug :name
|
|
153
|
+
|
|
154
|
+
def before_assign_slug(opts={})
|
|
155
|
+
puts 'Hello world! I get run before the slug assignment process'
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def after_assign_slug(opts={})
|
|
159
|
+
puts 'Goodbye world! I get run after the slug assignment process'
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
```
|
|
163
|
+
|
|
103
164
|
### Advanced Usage
|
|
104
165
|
|
|
105
166
|
If you need to change how the slug is generated, how duplicates are handled, etc., you can overwrite the methods in your class. For example to change how slugs are generated you can overwrite the `slugify` method.
|
|
106
167
|
|
|
107
168
|
```ruby
|
|
108
|
-
class
|
|
169
|
+
class SomeObject
|
|
109
170
|
include Schnecke
|
|
110
171
|
slug :name
|
|
111
172
|
|
|
@@ -119,7 +180,7 @@ end
|
|
|
119
180
|
Note, by default the library will validate to ensure that the slug only contains lowercase alpphanumeric letters and '-' or '_'. If your new method changes the alloweable set of characters you can either disable this validation, or pass in your own validation pattern.
|
|
120
181
|
|
|
121
182
|
```ruby
|
|
122
|
-
class
|
|
183
|
+
class SomeObject
|
|
123
184
|
include Schnecke
|
|
124
185
|
slug :name, require_format: false
|
|
125
186
|
|
|
@@ -128,7 +189,7 @@ class A
|
|
|
128
189
|
end
|
|
129
190
|
end
|
|
130
191
|
|
|
131
|
-
class
|
|
192
|
+
class SomeOtherObject
|
|
132
193
|
include Schnecke
|
|
133
194
|
slug :name, require_format: /\A[a-z]+\z/
|
|
134
195
|
|
data/lib/schnecke/schnecke.rb
CHANGED
|
@@ -10,9 +10,11 @@ module Schnecke
|
|
|
10
10
|
|
|
11
11
|
DEFAULT_SLUG_COLUMN = :slug
|
|
12
12
|
DEFAULT_SLUG_SEPARATOR = '-'
|
|
13
|
+
DEFAULT_MAX_LENGTH = 32
|
|
13
14
|
DEFAULT_REQUIRED_FORMAT = /\A[a-z0-9\-_]+\z/
|
|
14
15
|
|
|
15
16
|
class_methods do
|
|
17
|
+
# rubocop:disable Metrics/AbcSize
|
|
16
18
|
def slug(source, opts = {})
|
|
17
19
|
class_attribute :schnecke_config
|
|
18
20
|
|
|
@@ -21,6 +23,7 @@ module Schnecke
|
|
|
21
23
|
slug_source: source,
|
|
22
24
|
slug_column: opts.fetch(:column, DEFAULT_SLUG_COLUMN),
|
|
23
25
|
slug_separator: opts.fetch(:separator, DEFAULT_SLUG_SEPARATOR),
|
|
26
|
+
limit_length: opts.fetch(:limit_length, DEFAULT_MAX_LENGTH),
|
|
24
27
|
required: opts.fetch(:required, true),
|
|
25
28
|
generate_on_blank: opts.fetch(:generate_on_blank, true),
|
|
26
29
|
require_format: opts.fetch(:require_format, DEFAULT_REQUIRED_FORMAT),
|
|
@@ -50,6 +53,7 @@ module Schnecke
|
|
|
50
53
|
include InstanceMethods
|
|
51
54
|
end
|
|
52
55
|
end
|
|
56
|
+
# rubocop:enable Metrics/AbcSize
|
|
53
57
|
|
|
54
58
|
# Instance methods to include
|
|
55
59
|
module InstanceMethods
|
|
@@ -60,7 +64,47 @@ module Schnecke
|
|
|
60
64
|
# Note, a slug will not be assigned if one already exists. If one needs to
|
|
61
65
|
# force the assignment of a slug, pass `force: true`
|
|
62
66
|
def assign_slug(opts = {})
|
|
63
|
-
|
|
67
|
+
before_assign_slug(opts)
|
|
68
|
+
perform_slug_assign(opts)
|
|
69
|
+
after_assign_slug(opts)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Reassign the slug
|
|
73
|
+
#
|
|
74
|
+
# Unlike assign_slug, this will cause a slug to be created even if one
|
|
75
|
+
# already exists.
|
|
76
|
+
def reassign_slug(opts = {})
|
|
77
|
+
opts[:force] = true
|
|
78
|
+
assign_slug(opts)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Callback that is handled before the slug assignment process.
|
|
82
|
+
#
|
|
83
|
+
# When this is called, no validations, or decisions about whether or not
|
|
84
|
+
# a slug should be created have been made. As such, this will always run
|
|
85
|
+
# regardless of whether or not the slug assignment process proceeds or not.
|
|
86
|
+
def before_assign_slug(opts = {})
|
|
87
|
+
# Left blank, but can be implemented by user
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Callback that is handled after the slug is assigned
|
|
91
|
+
#
|
|
92
|
+
# Unless an error is raised during the slug assignment process, this method
|
|
93
|
+
# will always be called regardless of whether or not the slug was assigned
|
|
94
|
+
def after_assign_slug(opts = {})
|
|
95
|
+
# Left blank, but can be implemented by user
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
protected
|
|
99
|
+
|
|
100
|
+
# Assign the slug
|
|
101
|
+
#
|
|
102
|
+
# This is automatically called before model validation.
|
|
103
|
+
#
|
|
104
|
+
# Note, a slug will not be assigned if one already exists. If one needs to
|
|
105
|
+
# force the assignment of a slug, pass `force: true`
|
|
106
|
+
def perform_slug_assign(opts = {})
|
|
107
|
+
validate_slug_source
|
|
64
108
|
validate_slug_column
|
|
65
109
|
|
|
66
110
|
return if !should_create_slug? && !opts[:force]
|
|
@@ -74,6 +118,9 @@ module Schnecke
|
|
|
74
118
|
candidate_slug = slugify_blank
|
|
75
119
|
end
|
|
76
120
|
|
|
121
|
+
# Make sure it is not too long
|
|
122
|
+
candidate_slug = truncate_slug(candidate_slug)
|
|
123
|
+
|
|
77
124
|
# If there is a duplicate, create a unique one
|
|
78
125
|
if slug_exists?(candidate_slug)
|
|
79
126
|
candidate_slug = slugify_duplicate(candidate_slug)
|
|
@@ -82,16 +129,6 @@ module Schnecke
|
|
|
82
129
|
self[schnecke_config[:slug_column]] = candidate_slug
|
|
83
130
|
end
|
|
84
131
|
|
|
85
|
-
# Reassign the slug
|
|
86
|
-
#
|
|
87
|
-
# Unlike assign_slug, this will cause a slug to be created even if one
|
|
88
|
-
# already exists.
|
|
89
|
-
def reassign_slug
|
|
90
|
-
assign_slug(force: true)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
protected
|
|
94
|
-
|
|
95
132
|
# Slugify a string
|
|
96
133
|
#
|
|
97
134
|
# This will take a string and convert it to a slug by removing punctuation
|
|
@@ -99,9 +136,9 @@ module Schnecke
|
|
|
99
136
|
#
|
|
100
137
|
# This can be overriden if a different slug generation method is needed
|
|
101
138
|
def slugify(str)
|
|
102
|
-
return if str.blank?
|
|
139
|
+
return str if str.blank?
|
|
103
140
|
|
|
104
|
-
str.gsub
|
|
141
|
+
str = str.gsub(/[\p{Pc}\p{Ps}\p{Pe}\p{Pi}\p{Pf}\p{Po}]/, '')
|
|
105
142
|
str.parameterize
|
|
106
143
|
end
|
|
107
144
|
|
|
@@ -124,7 +161,7 @@ module Schnecke
|
|
|
124
161
|
#
|
|
125
162
|
# This can be overriden if a different behavior is desired
|
|
126
163
|
def slugify_duplicate(slug)
|
|
127
|
-
return if slug.blank?
|
|
164
|
+
return slug if slug.blank?
|
|
128
165
|
|
|
129
166
|
seq = 2
|
|
130
167
|
new_slug = slug_concat([slug, seq])
|
|
@@ -149,7 +186,7 @@ module Schnecke
|
|
|
149
186
|
|
|
150
187
|
private
|
|
151
188
|
|
|
152
|
-
def
|
|
189
|
+
def validate_slug_source
|
|
153
190
|
source = arrayify(schnecke_config[:slug_source])
|
|
154
191
|
source.each do |attr|
|
|
155
192
|
unless respond_to?(attr)
|
|
@@ -178,6 +215,13 @@ module Schnecke
|
|
|
178
215
|
parts.join(schnecke_config[:slug_separator])
|
|
179
216
|
end
|
|
180
217
|
|
|
218
|
+
def truncate_slug(slug)
|
|
219
|
+
return slug if slug.blank?
|
|
220
|
+
return slug if schnecke_config[:limit_length].blank?
|
|
221
|
+
|
|
222
|
+
slug[0, schnecke_config[:limit_length]]
|
|
223
|
+
end
|
|
224
|
+
|
|
181
225
|
def slug_exists?(slug)
|
|
182
226
|
slug_scope.exists?(schnecke_config[:slug_column] => slug)
|
|
183
227
|
end
|
data/lib/schnecke/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: schnecke
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick R. Schmid
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2022-10-
|
|
11
|
+
date: 2022-10-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|