neat_ids 1.0.1 → 1.1.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: 47363230b8058ed8405561165226af620e1f75ffcbf63003359572cc083cb0c2
4
- data.tar.gz: 7cab3e1ff65aec2f9ba9f7eae92546a11d78eb0fb471bbde69e2377cf6aca915
3
+ metadata.gz: 71d2b22c9ed3296f5b52c0fcb193caee551837cb351c5c7200e985da420df33c
4
+ data.tar.gz: e0df512bd2bd22df8f44d0a83acc863a37bb9d3affa3ab05bb5806087b6718cf
5
5
  SHA512:
6
- metadata.gz: 37ab0caa54c2f8a80ee6776083eab688f61a323b3b4289d38610ac0e3e2949b0a49a311e049143e11a2ca91db58b358fc8036fb5f740b50474f8fa49ad15fbb5
7
- data.tar.gz: 8065f32d9d5b8b354834ef9beb75d59f2f69c0ca54284ff83add2f0c9a4e8ef7ad0af81919f5ccdeb96fb0c3eb76dcc5b14a1e947652c197ce864864d1ebbd3b
6
+ metadata.gz: 23715fa9a6cd461ed7970f775903632ba35afa785682499029b21b0c3ca0cbdecdcfb1e7caecb848a87eefd601af8df308a9d10be527ca31ecb690fc58249a34
7
+ data.tar.gz: f658ad44a57b92be00d2c76c822839874f362c07fff00ef7615e943c63fbcc6104209aa64b8bc0f26460d0e76cf55dc79aef93eb1bca3adcd9eb8557f2a49a8f
data/Rakefile CHANGED
@@ -3,8 +3,6 @@ require "bundler/setup"
3
3
  APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
4
  load "rails/tasks/engine.rake"
5
5
 
6
- load "rails/tasks/statistics.rake"
7
-
8
6
  require "bundler/gem_tasks"
9
7
 
10
8
  require "rake/testtask"
@@ -1,13 +1,14 @@
1
1
  module NeatIds
2
2
  class NeatId
3
- attr_reader :sqids, :prefix
3
+ attr_reader :sqids, :prefix, :salt
4
4
 
5
5
  TOKEN = 123
6
6
 
7
- def initialize(model, prefix, min_length: NeatIds.minimum_length, alphabet: NeatIds.alphabet, delimiter: NeatIds.delimiter, **options)
7
+ def initialize(model, prefix, min_length: NeatIds.minimum_length, alphabet: NeatIds.alphabet, delimiter: NeatIds.delimiter, salt: NeatIds.salt, **options)
8
8
  @prefix = prefix.to_s
9
9
  @delimiter = delimiter.to_s
10
- @sqids = Sqids.new( min_length: min_length, alphabet: alphabet)
10
+ @salt = "#{model.table_name}#{salt}"
11
+ @sqids = Sqids.new(min_length: min_length, alphabet: shuffle_alphabet(alphabet))
11
12
  end
12
13
 
13
14
  def encode(id)
@@ -38,6 +39,24 @@ module NeatIds
38
39
 
39
40
  private
40
41
 
42
+ def shuffle_alphabet(alphabet)
43
+ chars = alphabet.chars
44
+ i = 0
45
+ p = 0
46
+ len = chars.length
47
+
48
+ salt_length = @salt.length
49
+ while i < len
50
+ v = salt_length.zero? ? 0 : @salt.getbyte(i % salt_length)
51
+ p += v
52
+ j = (v + p + i) % len
53
+ chars[i], chars[j] = chars[j], chars[i]
54
+ i += 1
55
+ end
56
+
57
+ chars.join
58
+ end
59
+
41
60
  def valid?(decoded_hashid)
42
61
  decoded_hashid.size >= 2 && decoded_hashid.first == TOKEN
43
62
  end
@@ -47,13 +66,13 @@ module NeatIds
47
66
  end
48
67
 
49
68
  def uuid_to_ints(uuid)
50
- hex = uuid.delete('-')
69
+ hex = uuid.delete("-")
51
70
  [0, 8, 16, 24].map { |i| hex[i, 8].to_i(16) }
52
71
  end
53
72
 
54
73
  def ints_to_uuid(ints)
55
- hex = ints.map { |n| n.to_s(16).rjust(8, '0') }.join
56
- "#{hex[0,8]}-#{hex[8,4]}-#{hex[12,4]}-#{hex[16,4]}-#{hex[20,12]}"
74
+ hex = ints.map { |n| n.to_s(16).rjust(8, "0") }.join
75
+ "#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
57
76
  end
58
77
  end
59
78
  end
@@ -1,3 +1,3 @@
1
1
  module NeatIds
2
- VERSION = "1.0.1"
2
+ VERSION = "1.1.0"
3
3
  end
data/lib/neat_ids.rb CHANGED
@@ -3,6 +3,19 @@ require "neat_ids/engine"
3
3
 
4
4
  require "sqids"
5
5
 
6
+ # The upstream sqids gem derives its max encodable number from `0.size`
7
+ # (the machine word size). On 32-bit platforms (e.g. arm32) that yields
8
+ # 2**30 - 1 = 1073741823, which is too small for the 32-bit integer chunks
9
+ # used to encode UUIDs and breaks encoding on machines like the Raspberry Pi 2.
10
+ # `max_value` only acts as an upper-bound guard in `Sqids#encode`; it does not
11
+ # affect the encoding/decoding algorithm, so pinning it to a fixed constant is
12
+ # safe and produces identical IDs on every platform.
13
+ class Sqids
14
+ def self.max_value
15
+ 9007199254740991 # 2**53 - 1, per the Sqids spec
16
+ end
17
+ end
18
+
6
19
  module NeatIds
7
20
  class Error < StandardError; end
8
21
 
@@ -11,6 +24,7 @@ module NeatIds
11
24
  mattr_accessor :delimiter, default: "_"
12
25
  mattr_accessor :alphabet, default: "abcdefghijklmnopqrstuvwxyz1234567890"
13
26
  mattr_accessor :minimum_length, default: 24
27
+ mattr_accessor :salt, default: ""
14
28
 
15
29
  mattr_accessor :models, default: {}
16
30
 
@@ -21,6 +35,16 @@ module NeatIds
21
35
  raise Error, "Unable to find model with prefix `#{prefix}`. Available prefixes are: #{models.keys.join(", ")}"
22
36
  end
23
37
 
38
+ # Returns a decoded ID from a neat_id
39
+ def self.decode_neat_id(neat_id)
40
+ return neat_id if neat_id.nil? || neat_id.is_a?(Integer)
41
+ return neat_id unless models.keys.any? { |key| neat_id.to_s.start_with?("#{key}#{delimiter}") }
42
+
43
+ model_name, _ = split_id(neat_id)
44
+
45
+ models.fetch(model_name).decode_neat_id(neat_id)
46
+ end
47
+
24
48
  # Splits a prefixed ID into its prefix and ID
25
49
  def self.split_id(neat_id, delimiter = NeatIds.delimiter)
26
50
  prefix, _, id = neat_id.to_s.rpartition(delimiter)
@@ -62,6 +86,13 @@ module NeatIds
62
86
  module Attribute
63
87
  extend ActiveSupport::Concern
64
88
 
89
+ # Methods added to relations and has_many associations
90
+ module RelationMethods
91
+ def neat_ids
92
+ klass.neat_ids(pluck(:id))
93
+ end
94
+ end
95
+
65
96
  class_methods do
66
97
  def find_by_neat_id(id)
67
98
  find_by(id: _neat_id.decode(id))
@@ -86,6 +117,16 @@ module NeatIds
86
117
  def decode_neat_ids(ids)
87
118
  ids.map { |id| decode_neat_id(id) }
88
119
  end
120
+
121
+ def relation
122
+ super.tap { |r| r.extend RelationMethods }
123
+ end
124
+
125
+ def has_many(*args, &block)
126
+ options = args.extract_options!
127
+ options[:extend] = Array(options[:extend]).push(RelationMethods)
128
+ super(*args, **options, &block)
129
+ end
89
130
  end
90
131
 
91
132
  def neat_id
@@ -112,12 +153,35 @@ module NeatIds
112
153
  end
113
154
 
114
155
  def relation
115
- super.tap { |r| r.extend ClassMethods }
156
+ super.tap { |r| r.extend ClassMethods, Attribute::RelationMethods }
157
+ end
158
+
159
+ def belongs_to(*args, **options, &block)
160
+ association = super
161
+
162
+ name = args.first
163
+ reflection = association[name] || association[name.to_s]
164
+
165
+ return association if reflection.polymorphic?
166
+ return association if reflection.klass._neat_id.blank?
167
+
168
+ generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
169
+ def #{name}_neat_id
170
+ #{reflection.klass}._neat_id.encode(#{reflection.foreign_key})
171
+ end
172
+
173
+ def #{name}_neat_id=(neat_id)
174
+ decoded_id = #{reflection.klass}._neat_id.decode(neat_id, fallback: #{reflection.klass}._neat_id_fallback)
175
+ send("#{reflection.foreign_key}=", decoded_id)
176
+ end
177
+ CODE
178
+
179
+ association
116
180
  end
117
181
 
118
182
  def has_many(*args, &block)
119
183
  options = args.extract_options!
120
- options[:extend] = Array(options[:extend]).push(ClassMethods)
184
+ options[:extend] = Array(options[:extend]).push(ClassMethods, Attribute::RelationMethods)
121
185
  super(*args, **options, &block)
122
186
  end
123
187
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: neat_ids
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brad Thompson
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-07-05 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rails
@@ -59,7 +58,6 @@ metadata:
59
58
  homepage_uri: https://github.com/braddoeswebdev/neat_ids
60
59
  source_code_uri: https://github.com/braddoeswebdev/neat_ids
61
60
  changelog_uri: https://github.com/braddoeswebdev/neat_ids
62
- post_install_message:
63
61
  rdoc_options: []
64
62
  require_paths:
65
63
  - lib
@@ -74,8 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
74
72
  - !ruby/object:Gem::Version
75
73
  version: '0'
76
74
  requirements: []
77
- rubygems_version: 3.5.6
78
- signing_key:
75
+ rubygems_version: 4.0.14
79
76
  specification_version: 4
80
77
  summary: Neat IDs generates IDs with friendly prefixes for your models
81
78
  test_files: []