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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2e509bff01283a06b873a93ce83938d8b1330534f661d5fe13dc8235f764da7
4
- data.tar.gz: d2fb750754fa8959baca1ff1236a9ecb83b9e908ca749d3fa25b94b8cc2dab17
3
+ metadata.gz: bdf704d77a580357657db281c7fd5021045fcd5c2b7cca9f32f6f468cf33d895
4
+ data.tar.gz: 995fc538958ecf0ae7b1ce67cb81498e4223b3538782cc036a9763eefa1c0e4f
5
5
  SHA512:
6
- metadata.gz: 9f8280a41d8289520a8a2f99a2fed9c0713c075805d134b16e66b02dbb53121d070f2acf3d7458744d1bf4517eaf5c2eacaf8d880ba73ad0167e3334ef82d2d3
7
- data.tar.gz: 8f526df3095aa6e092eb95aee7c52258824ce4dccd8c312ff20660a14cfe1d60251f528b80a0b3ce8c22238f4f96222a525605cf81f899b69338fd50abe87f4b
6
+ metadata.gz: fd373216be05945333cc1ddd6850892151549db1e887cc64ce462ae4f98ee4e86b698e34d591b71f6fd667f5cb65ae8a3634a175a9186f88ee3c97035fcc06af
7
+ data.tar.gz: d62fc13e0f3151f338314d79e7664267812b2de7f5a70ef6e1ba5fbb24ae550a569bf7266858906ee8f7502387aa76bf7687b9f8cf3c73a6a65a0a584213604f
data/.rubocop.yml CHANGED
@@ -44,6 +44,8 @@ Metrics/MethodLength:
44
44
 
45
45
  Metrics/ModuleLength:
46
46
  Max: 100
47
+ Exclude:
48
+ - 'lib/schnecke/schnecke.rb'
47
49
 
48
50
  Minitest/MultipleAssertions:
49
51
  Max: 10
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- schnecke (0.1.0)
4
+ schnecke (0.3.0)
5
5
  activerecord (> 4.2.0)
6
6
  activesupport (> 4.2.0)
7
7
 
data/README.md CHANGED
@@ -22,10 +22,10 @@ Or install it yourself as:
22
22
 
23
23
  ## Usage
24
24
 
25
- Given a class `A` which has the attribute `name` and has a `slug` column defined, we can do the following:
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 A
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 A
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 A
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, `A` and `B` as defined as below, then the slugs will be unique for all slugs for all type `A` objcets and all type `B` 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 A
99
+ class SomeObject
62
100
  include Schnecke
63
101
  slug :name
64
102
  end
65
103
 
66
- class B
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 `A` and once for any object of type `B`. 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.
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 `A`, 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.
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 A
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 A
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 B
192
+ class SomeOtherObject
132
193
  include Schnecke
133
194
  slug :name, require_format: /\A[a-z]+\z/
134
195
 
@@ -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
- validate_source
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!(/[\p{Pc}\p{Ps}\p{Pe}\p{Pi}\p{Pf}\p{Po}]/, '')
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 validate_source
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Schnecke
4
- VERSION = '0.1.0'
4
+ VERSION = '0.3.0'
5
5
  end
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.1.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-01 00:00:00.000000000 Z
11
+ date: 2022-10-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler