make_taggable 1.2.0 → 1.2.1

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: 7988c41c462a75321cf31cb13888a01d87a4dec964b13d2bb8c94876d7291d2a
4
- data.tar.gz: 76c39347c15e44dd9bad00317aa4e27f4ef58aadd638507340a74fe6bd5bbbd3
3
+ metadata.gz: 99cb64f83f0cef74cc21f3fc09a341e62db5a2248b8c88ef5d720de4b9446abb
4
+ data.tar.gz: c3c08be5a20df6100567523555be2394dc32665770f6c65226e0fe91b3be5652
5
5
  SHA512:
6
- metadata.gz: 89496e3b067d1c4707580b30b4ad1a1be0701e8aa03f1e7de30d2c229b4eec96afd82eec4675e89c32cf5e8f0b3ccf28d7afb0ffcf4a7ada5b9ef2169232dcc2
7
- data.tar.gz: bbeef557e819249bc1d13668ec6437401dfcfbe3d995874535c86d5b86747ff46d5e0bb3383905343057c2f773c0658766eed9580f6173789e288571a516f0bb
6
+ metadata.gz: ad8701479ff9565786acebff64af05fe5aaa88eddbcfc660f88778e4ca885a0d26438677754cfa3072dd54e3eef6374ebed4f1e4de6a70fe911824050cb0b19e
7
+ data.tar.gz: 47804d66800ff4df491f3b7037ba5e82c131fbaf4c9eb8442a650ed6ecbf1772483d2ae2237c5437cb8ffefca831d107744a3ffacb2c517cb65a161063bc6d0e
data/CHANGELOG.md CHANGED
@@ -5,6 +5,33 @@ All notable changes to this project are documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
6
6
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.2.1] - 2026-08-23
9
+
10
+ ### Fixed
11
+
12
+ - `tagged_with(..., exclude: true)` raised on any model whose primary key is not `id`. The exclude
13
+ strategy built its `NOT IN` predicate against a hardcoded `id` column, where the other two
14
+ strategies already used the model's primary key.
15
+
16
+ - `tagged_with(..., on: <context>, exclude: true)` ignored the context entirely, gathering taggings
17
+ from every context, so a record tagged in one context was excluded from a query about another.
18
+
19
+ - `tagged_with([], exclude: true)` returned nothing rather than everything. Excluding no tags rules
20
+ nothing out, so the whole scope now stands. This restores the property that `tagged_with(list)`
21
+ and `tagged_with(list, exclude: true)` partition the scope between them for any list.
22
+
23
+ - A tag context whose name cannot become a Ruby method name -- one starting with a digit, say --
24
+ raised `SyntaxError` while the model was loading, from inside Active Record's association
25
+ builder. Since `SyntaxError` is not a `StandardError` it slipped past application rescues.
26
+ Contexts are now checked as they are declared and rejected with an `ArgumentError` naming the
27
+ context. Non-ASCII context names keep working.
28
+
29
+ ### Internal
30
+
31
+ - The suite's schema teardown no longer depends on the order `connection.tables` returns. MySQL
32
+ ignores `DROP TABLE ... CASCADE` for foreign keys, so it only worked because `taggings` happens
33
+ to sort before `tags`.
34
+
8
35
  ## [1.2.0] - 2026-08-23
9
36
 
10
37
  ### Fixed
data/docs/contexts.md CHANGED
@@ -40,6 +40,23 @@ user.skill_list_change # => [["jogging"], ["diving"]]
40
40
  user.will_save_change_to_skill_list?
41
41
  ```
42
42
 
43
+ ### Naming a context
44
+
45
+ Because the context becomes part of every name in the table above, it has to be usable as a Ruby
46
+ method and instance variable name. A context starting with a digit, or containing a hyphen or a
47
+ space, is rejected when the model declares it:
48
+
49
+ ```ruby
50
+ class Book < ActiveRecord::Base
51
+ make_taggable :"1categories"
52
+ end
53
+ # => ArgumentError: :"1categories" cannot be used as a tag context: make_taggable generates
54
+ # methods and instance variables from it, and "1categories_list" is not a valid Ruby name.
55
+ ```
56
+
57
+ Validity is decided by asking Ruby, not by a pattern, so anything that makes a legal method name is
58
+ allowed — non-ASCII context names included.
59
+
43
60
  ## Adding contexts later
44
61
 
45
62
  Calling `make_taggable` again adds contexts rather than replacing them, which is what lets a
data/docs/querying.md CHANGED
@@ -28,12 +28,20 @@ empty relation rather than every record — worth knowing when the tags come fro
28
28
  | `:exclude` | Match records carrying none of the tags |
29
29
  | `:match_all` | Match records carrying only these tags and no others |
30
30
  | `:wild` | Match tags *containing* the given text, i.e. `%sci%` |
31
- | `:on` | Restrict to one context |
31
+ | `:on` | Restrict to one context. Honoured by every option, `:exclude` included |
32
32
  | `:owned_by` | Restrict to tags applied by one tagger |
33
33
  | `:order_by_matching_tag_count` | With `:any`, order by how many tags matched, most first |
34
34
  | `:start_at` | Only tags applied after this time |
35
35
  | `:end_at` | Only tags applied before this time |
36
36
 
37
+ An empty tag list means "nothing matches" for the matching options and "nothing is ruled out" for
38
+ `:exclude`, so the two always partition the scope between them:
39
+
40
+ ```ruby
41
+ Book.tagged_with([]) # => none
42
+ Book.tagged_with([], exclude: true) # => every book
43
+ ```
44
+
37
45
  `:wild` combines with `:any` or `:exclude`:
38
46
 
39
47
  ```ruby
@@ -129,7 +129,8 @@ module MakeTaggable::Taggable
129
129
  # @option options [Symbol, String] :on only tags applied in this context
130
130
  # @option options [Time, Date] :start_at only tags applied after this time
131
131
  # @option options [Time, Date] :end_at only tags applied before this time
132
- # @return [ActiveRecord::Relation] empty when no tags are given
132
+ # @return [ActiveRecord::Relation] empty when no tags are given, except under `:exclude`,
133
+ # where excluding no tags leaves the whole scope standing
133
134
  #
134
135
  # @example Every tag
135
136
  # User.tagged_with(["awesome", "cool"])
@@ -144,7 +145,9 @@ module MakeTaggable::Taggable
144
145
  tag_list = MakeTaggable.default_parser.new(tags).parse
145
146
  options = options.dup
146
147
 
147
- return none if tag_list.empty?
148
+ # Asking for no tags matches nothing, but *excluding* no tags rules
149
+ # nothing out, so the whole scope stands.
150
+ return options[:exclude].present? ? all : none if tag_list.empty?
148
151
 
149
152
  ::MakeTaggable::Taggable::TaggedWithQuery.build(self, MakeTaggable::Tag, MakeTaggable::Tagging, tag_list, options)
150
153
  end
@@ -20,15 +20,21 @@ module MakeTaggable::Taggable::TaggedWithQuery
20
20
  private
21
21
 
22
22
  def tags_not_in_list
23
- taggable_arel_table[:id].not_in(
23
+ on_condition = tagging_arel_table[:tag_id].eq(tag_arel_table[:id])
24
+ .and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name))
25
+ .and(tags_match_type)
26
+
27
+ # Without this the subquery gathers taggings from every context, so a
28
+ # record tagged in one context is excluded from a query about another.
29
+ if options[:on].present?
30
+ on_condition = on_condition.and(tagging_arel_table[:context].eq(options[:on]))
31
+ end
32
+
33
+ taggable_arel_table[taggable_model.primary_key].not_in(
24
34
  tagging_arel_table
25
35
  .project(tagging_arel_table[:taggable_id])
26
36
  .join(tag_arel_table)
27
- .on(
28
- tagging_arel_table[:tag_id].eq(tag_arel_table[:id])
29
- .and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name))
30
- .and(tags_match_type)
31
- )
37
+ .on(on_condition)
32
38
  )
33
39
 
34
40
  # FIXME: missing time scope, this is also missing in the original implementation
@@ -69,6 +69,7 @@ module MakeTaggable
69
69
  #
70
70
  def taggable_on(preserve_tag_order, *tag_types)
71
71
  tag_types = tag_types.to_a.flatten.compact.map(&:to_sym)
72
+ tag_types.each { |tag_type| validate_tag_context!(tag_type) }
72
73
 
73
74
  if taggable?
74
75
  self.tag_types = (self.tag_types + tag_types).uniq
@@ -97,5 +98,31 @@ module MakeTaggable
97
98
  include Ownership
98
99
  include Related
99
100
  end
101
+
102
+ # Rejects a context that cannot become the methods and instance variables the
103
+ # library generates from it.
104
+ #
105
+ # A context is interpolated straight into generated source -- `#{context}_list`,
106
+ # `#{context}_taggings`, `@#{context}_list`. A name that is not a valid identifier
107
+ # produces source Ruby cannot parse, and the resulting SyntaxError descends from
108
+ # ScriptError rather than StandardError, so it slips past an application's own
109
+ # rescue and takes the boot down pointing at Active Record's association builder
110
+ # rather than at the offending declaration.
111
+ #
112
+ # Validity is decided by asking Ruby rather than by pattern, so a context is
113
+ # accepted on exactly the terms the generated code needs -- non-ASCII names
114
+ # included, since those make perfectly good method names.
115
+ #
116
+ # @param tag_type [Symbol] the context being declared
117
+ # @return [void]
118
+ # @raise [ArgumentError] when the context cannot become an identifier
119
+ def validate_tag_context!(tag_type)
120
+ Object.new.instance_variable_defined?(:"@#{tag_type}_list")
121
+ rescue NameError
122
+ raise ArgumentError,
123
+ "#{tag_type.inspect} cannot be used as a tag context: " \
124
+ "make_taggable generates methods and instance variables from it, and " \
125
+ "\"#{tag_type}_list\" is not a valid Ruby name."
126
+ end
100
127
  end
101
128
  end
@@ -6,5 +6,5 @@ module MakeTaggable
6
6
  #
7
7
  # @return [String]
8
8
  #
9
- VERSION = "1.2.0"
9
+ VERSION = "1.2.1"
10
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: make_taggable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Kennedy