uxid 0.2.3 → 1.0.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.
data/lib/uxid.rb CHANGED
@@ -1,68 +1,162 @@
1
- # frozen-string-literal: true
2
-
3
- if RUBY_VERSION >= "2.5"
4
- require "securerandom"
5
- else
6
- require "sysrandom/securerandom"
7
- end
8
-
9
- require "uxid/model"
10
- require "uxid/encoder"
11
- require "uxid/version"
12
-
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "uxid/version"
4
+ require_relative "uxid/error"
5
+ require_relative "uxid/options"
6
+ require_relative "uxid/encoder"
7
+ require_relative "uxid/decoded"
8
+ require_relative "uxid/decoder"
9
+ require_relative "uxid/monotonic"
10
+
11
+ # Prefixed, K-sortable identifiers, wire-compatible with the Elixir
12
+ # reference implementation of uxid.
13
13
  module UXID
14
- CROCKFORD_ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
15
- DELIMITER = "_"
16
-
17
- SIZE_WARNING_MESSAGE = <<-EOM
18
- WARN: The UXID :size param is for Symbol T-Shirt sizes only.
19
- Please use the :size param with Symbols (:xs, :s, :m, :l, :xl, or :xsmall, :small, :medium, :large, :xlarge),
20
- or use the :rand_size param for specific size of random bytes.
21
- EOM
22
-
23
- class Error < StandardError; end
24
-
25
- def self.generate attrs = {}
26
- model = new attrs
27
- model.encode
14
+ # Generates an id: an optional prefix and delimiter, then a body in
15
+ # Crockford Base32, laid out as uxid-ex's wire-format spec defines it. The
16
+ # body is time-based (a time field and a random field) unless :from is
17
+ # given, when it is deterministic: the marker character (z, or Z in an
18
+ # upper-case id) and then a SHA-256 hash of the prefix, the from string
19
+ # and the size, so the same arguments always give the same id. A
20
+ # deterministic id sorts after every time-based id with its prefix and
21
+ # case.
22
+ #
23
+ # Every option is a keyword argument; an option left out takes its default,
24
+ # and an unknown one raises ArgumentError. The options whose default is nil
25
+ # take nil; the others refuse it.
26
+ #
27
+ # @param options [Hash] the keyword arguments below
28
+ # @option options [String, nil] :prefix (nil) the prefix, emitted as given;
29
+ # nil and an empty string are both no prefix, and the id is then the bare
30
+ # body
31
+ # @option options [Symbol, String] :size (:xl) :xs, :s, :m, :l or :xl, or
32
+ # their long names :xsmall, :small, :medium, :large and :xlarge, as
33
+ # Symbols or Strings
34
+ # @option options [Integer, nil] :rand_size (nil) the random field's width
35
+ # in bytes, 0 to 10, in place of the size's; a body whose width is not the
36
+ # size table's has a length a decoder refuses
37
+ # @option options [Boolean] :compact_time (false) true for compact time (the
38
+ # low 40 bits of the time in 8 characters), false for standard time (48
39
+ # bits in 10)
40
+ # @option options [Symbol, String] :case (:lower) :lower or :upper, the case
41
+ # of every body character; the prefix keeps its own
42
+ # @option options [String] :delimiter ("_") the non-empty string between
43
+ # prefix and body
44
+ # @option options [Integer, Time, nil] :time (nil) the time, as milliseconds
45
+ # since the Unix epoch (0 to 2**48 - 1) or a Time (read to the
46
+ # millisecond); nil reads the system clock
47
+ # @option options [String, nil] :rand (nil) the random field's bytes, used
48
+ # verbatim. For reproducible ids such as test vectors only, never as a
49
+ # source of randomness: the id is exactly as guessable as these bytes.
50
+ # Its byte size must be the call's random width
51
+ # @option options [#call, nil] :byte_source (nil) where the random field's
52
+ # bytes come from: any object whose call(count) returns a String of count
53
+ # bytes; nil is SecureRandom.random_bytes. Not called when :rand is given
54
+ # @option options [String, nil] :from (nil) the string a deterministic body
55
+ # is the hash of, read as its bytes: valid text in an encoding other
56
+ # than UTF-8 is transcoded to UTF-8 first; a binary (ASCII-8BIT) String,
57
+ # one not valid in its own encoding, and one Ruby cannot transcode are
58
+ # hashed as their own bytes, so a raw-byte key such as a 16-byte UUID
59
+ # works. The prefix is hashed the same way. The body has the size's
60
+ # standard length; :time, :compact_time, :rand_size and :byte_source do
61
+ # not apply to it and are not read
62
+ # @return [String] the id
63
+ # @raise [UXID::Error] a refusal the wire format names, with its
64
+ # {UXID::Error#code}: :rand_size_too_large for a :rand_size above 10,
65
+ # :rand_width_mismatch for a :rand whose byte size is not the random
66
+ # width, :compact_time_out_of_range for a compact time whose first
67
+ # character would be the deterministic marker, :rand_with_from for a
68
+ # :rand given with :from
69
+ # @raise [ArgumentError] any other option outside the ones above
70
+ #
71
+ # @example
72
+ # UXID.generate(prefix: "usr") # => "usr_01..." (26 body characters)
73
+ # UXID.generate(prefix: "cus", size: :s) # => "cus_01..." (14 body characters)
74
+ # UXID.generate(prefix: "usr", size: :xs, time: 1_700_000_000_000)
75
+ # # => "usr_01hf7yat00"
76
+ # UXID.generate(prefix: "cus", from: "acme-corp", size: :s)
77
+ # # => "cus_zt9vnyfd85bff0"
78
+ def self.generate(**options)
79
+ Encoder.generate(Options.resolve(options))
28
80
  end
29
81
 
30
- def self.new attrs = {}
31
- model = ::UXID::Model.new
32
-
33
- model.time = attrs[:time] || Time.now
34
- model.prefix = attrs[:prefix] || ""
35
- model.size = attrs[:size]
36
-
37
- if attrs[:rand_size]
38
- model.rand_size = attrs[:rand_size]
39
- else
40
- case model.size
41
- when Integer
42
- puts SIZE_WARNING_MESSAGE
43
- model.rand_size = model.size
44
-
45
- when String
46
- puts SIZE_WARNING_MESSAGE
47
- model.rand_size = 10
48
-
49
- when :xs, :xsmall
50
- model.rand_size = 0
51
-
52
- when :s, :small
53
- model.rand_size = 2
54
-
55
- when :m, :medium
56
- model.rand_size = 5
57
-
58
- when :l, :large
59
- model.rand_size = 7
82
+ # Decodes an id: its prefix, its size and time form, whether it is
83
+ # deterministic, and its time, as uxid-ex's wire-format spec reads them.
84
+ # The random field is not read back.
85
+ #
86
+ # The body is everything after the last delimiter and the prefix
87
+ # everything before it; an id with no delimiter is a body with no prefix.
88
+ # The body length in bytes gives the size and the time form. The lengths
89
+ # 10 and 18 are read as standard xs and standard m: a compact xs or compact
90
+ # m id decodes to its size but not its time. A body starting with z or Z
91
+ # is deterministic and has no time. A compact body carries the low 40 bits
92
+ # of its time; the rest come from the decoder's clock, and a result more
93
+ # than one day ahead of the clock is taken from the 2**40 ms window before.
94
+ #
95
+ # Malformed input never raises: a String the format refuses gives an
96
+ # Invalid whose reason is the first rule it fails, in this order: :empty
97
+ # (an empty body: an empty string, or one ending in the delimiter),
98
+ # :invalid_length (a body length in bytes not in the length table),
99
+ # :invalid_character (a body byte outside the alphabet in either case).
100
+ # A value that is not a String has no alphabet characters and gives
101
+ # :invalid_character. A String in any encoding, binary or not valid in its
102
+ # own encoding included, is read as its bytes.
103
+ #
104
+ # @param id [Object] the id to read
105
+ # @param delimiter [String] ("_") the non-empty string between prefix and
106
+ # body; it must be the one the id was generated with
107
+ # @param now [Integer, Time, nil] (nil) the decoder's clock, as
108
+ # milliseconds since the Unix epoch or a Time, read only for a compact
109
+ # time; nil reads the system clock
110
+ # @return [UXID::Decoded, UXID::Invalid] both answer valid?, true for a
111
+ # Decoded and false for an Invalid
112
+ # @raise [ArgumentError] for a delimiter that is not a non-empty String,
113
+ # or a now that is not an Integer, a Time or nil
114
+ #
115
+ # @example
116
+ # UXID.decode("usr_01hf7yat00")
117
+ # # => #<data UXID::Decoded prefix="usr", size=:xs, compact=false, deterministic=false, time=1700000000000>
118
+ # UXID.decode("cus_zt9vnyfd85bff0").deterministic? # => true
119
+ # UXID.decode("usr_01hf7yat0").reason # => :invalid_length
120
+ def self.decode(id, delimiter: "_", now: nil)
121
+ Decoder.decode(id, delimiter, now)
122
+ end
60
123
 
61
- else
62
- model.rand_size = 10
63
- end
64
- end
124
+ # Whether an id decodes (see UXID.decode) and, when :prefix is given, has
125
+ # exactly that prefix. Never raises on the id: anything UXID.decode
126
+ # refuses, a value that is not a String included, is false.
127
+ #
128
+ # @param id [Object] the id to check
129
+ # @param prefix [String, nil] (nil) the prefix the id must have; nil and
130
+ # the empty string accept any prefix or none
131
+ # @param delimiter [String] ("_") the non-empty string between prefix and
132
+ # body
133
+ # @return [Boolean]
134
+ # @raise [ArgumentError] for a delimiter that is not a non-empty String
135
+ #
136
+ # @example
137
+ # UXID.valid?("cus_zt9vnyfd85bff0") # => true
138
+ # UXID.valid?("cus_zt9vnyfd85bff0", prefix: "usr") # => false
139
+ # UXID.valid?("usr_01hu7yat00") # => false
140
+ def self.valid?(id, prefix: nil, delimiter: "_")
141
+ decoded = Decoder.decode(id, delimiter, nil)
142
+ decoded.valid? && (prefix.nil? || prefix.empty? || decoded.prefix == prefix)
143
+ end
65
144
 
66
- model
145
+ # Whether an id decodes (see UXID.decode) to a deterministic body, one
146
+ # generated with :from. Never raises on the id: anything UXID.decode
147
+ # refuses, a value that is not a String included, is false.
148
+ #
149
+ # @param id [Object] the id to check
150
+ # @param delimiter [String] ("_") the non-empty string between prefix and
151
+ # body
152
+ # @return [Boolean]
153
+ # @raise [ArgumentError] for a delimiter that is not a non-empty String
154
+ #
155
+ # @example
156
+ # UXID.deterministic?("cus_zt9vnyfd85bff0") # => true
157
+ # UXID.deterministic?("usr_01hf7yat00") # => false
158
+ def self.deterministic?(id, delimiter: "_")
159
+ decoded = Decoder.decode(id, delimiter, nil)
160
+ decoded.valid? && decoded.deterministic?
67
161
  end
68
162
  end
metadata CHANGED
@@ -1,53 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: uxid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - JohnnyT
8
- autorequire:
9
- bindir: exe
8
+ bindir: bin
10
9
  cert_chain: []
11
- date: 2020-11-29 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies: []
13
- description: 'Generates IDs like: cus_01EPEY1JMK and txn_01EPEY2P06TR1RTV07 (similar
14
- to Stripe identifiers).'
12
+ description: Generates and decodes prefixed, K-sortable identifiers a person can read,
13
+ copy and route back to their resource. Random, deterministic and monotonic generation,
14
+ decoding and validity checks, checked against the conformance corpus of the Elixir
15
+ reference implementation. No runtime dependencies.
15
16
  email:
16
17
  - ubergeek3141@gmail.com
17
18
  executables: []
18
19
  extensions: []
19
20
  extra_rdoc_files: []
20
21
  files:
21
- - ".github/workflows/build.yml"
22
- - ".gitignore"
23
- - ".rubocop.yml"
24
- - ".simplecov"
25
22
  - CHANGELOG.md
26
- - Dangerfile
27
- - Gemfile
28
- - Gemfile.lock
29
23
  - LICENSE
30
24
  - README.md
31
- - Rakefile
32
- - bin/console
33
- - bin/setup
34
- - lib/tasks/bundler.rake
35
- - lib/tasks/check.rake
36
- - lib/tasks/linting.rake
37
- - lib/tasks/quality.rake
38
- - lib/tasks/test.rake
39
25
  - lib/uxid.rb
26
+ - lib/uxid/decoded.rb
27
+ - lib/uxid/decoder.rb
40
28
  - lib/uxid/encoder.rb
41
- - lib/uxid/model.rb
29
+ - lib/uxid/error.rb
30
+ - lib/uxid/monotonic.rb
31
+ - lib/uxid/options.rb
42
32
  - lib/uxid/version.rb
43
- - uxid.gemspec
44
33
  homepage: https://github.com/riddler/uxid-rb
45
34
  licenses:
46
35
  - MIT
47
36
  metadata:
48
37
  homepage_uri: https://github.com/riddler/uxid-rb
49
38
  source_code_uri: https://github.com/riddler/uxid-rb
50
- post_install_message:
39
+ changelog_uri: https://github.com/riddler/uxid-rb/blob/main/CHANGELOG.md
40
+ rubygems_mfa_required: 'true'
51
41
  rdoc_options: []
52
42
  require_paths:
53
43
  - lib
@@ -55,16 +45,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
55
45
  requirements:
56
46
  - - ">="
57
47
  - !ruby/object:Gem::Version
58
- version: 2.3.0
48
+ version: '3.2'
59
49
  required_rubygems_version: !ruby/object:Gem::Requirement
60
50
  requirements:
61
51
  - - ">="
62
52
  - !ruby/object:Gem::Version
63
53
  version: '0'
64
54
  requirements: []
65
- rubygems_version: 3.1.4
66
- signing_key:
55
+ rubygems_version: 4.0.20
67
56
  specification_version: 4
68
- summary: 'Generates IDs like: cus_01EPEY1JMK and txn_01EPEY2P06TR1RTV07 (similar to
69
- Stripe identifiers).'
57
+ summary: Prefixed, K-sortable identifiers, wire-compatible with the Elixir uxid.
70
58
  test_files: []
@@ -1,72 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- pull_request:
5
- branches:
6
- - main
7
-
8
- jobs:
9
- danger:
10
- runs-on: ubuntu-latest
11
- if: github.event_name == 'pull_request' # only run pull request when multiple trigger workflow
12
- steps:
13
- - name: Checkout Repo
14
- uses: actions/checkout@v2
15
-
16
- - name: Setup Ruby
17
- uses: actions/setup-ruby@v1
18
- with:
19
- ruby-version: '2.6'
20
-
21
- - name: Cache Ruby Gems
22
- uses: actions/cache@v2
23
- with:
24
- path: vendor/bundle
25
- key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
26
- restore-keys: |
27
- ${{ runner.os }}-gems-
28
-
29
- - name: Bundle Install
30
- run: |
31
- bundle config path vendor/bundle
32
- bundle install --jobs 4 --retry 3
33
-
34
- - name: Danger linting
35
- uses: MeilCli/danger-action@v5
36
- with:
37
- plugins_file: Gemfile
38
- install_path: vendor/bundle
39
- danger_file: Dangerfile
40
- danger_id: danger-pr
41
- env:
42
- DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
43
-
44
- tests:
45
- runs-on: ubuntu-latest
46
- if: github.event_name == 'pull_request' # only run pull request when multiple trigger workflow
47
- steps:
48
- - name: Checkout Repo
49
- uses: actions/checkout@v2
50
-
51
- - name: Setup Ruby
52
- uses: actions/setup-ruby@v1
53
- with:
54
- ruby-version: '2.6'
55
-
56
- - name: Cache Ruby Gems
57
- uses: actions/cache@v2
58
- with:
59
- path: vendor/bundle
60
- key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
61
- restore-keys: |
62
- ${{ runner.os }}-gems-
63
-
64
- - name: Bundle Install
65
- run: |
66
- bundle config path vendor/bundle
67
- bundle install --jobs 4 --retry 3
68
-
69
- - name: Run Tests
70
- run: |
71
- bundle config path vendor/bundle
72
- bundle exec rake test
data/.gitignore DELETED
@@ -1,43 +0,0 @@
1
- # Custom files
2
-
3
- # Compiled source
4
- *.com
5
- *.class
6
- *.dll
7
- *.exe
8
- *.o
9
- *.so
10
-
11
- # Packages
12
- #
13
- # It's better to unpack these files and commit the raw source
14
- # git has its own built in compression methods
15
- *.7z
16
- *.dmg
17
- *.gz
18
- *.iso
19
- *.jar
20
- *.rar
21
- *.tar
22
- *.zip
23
-
24
- # Logs and databases
25
- *.log
26
- *.sql
27
- *.sqlite
28
-
29
- # OS generated files #
30
- .DS_Store
31
- .DS_Store?
32
- ._*
33
- .Spotlight-V100
34
- .Trashes
35
- ehthumbs.db
36
- Thumbs.db
37
-
38
- # Editor-made files
39
- *.swp
40
-
41
- #===== Ruby =====
42
- *.gem
43
- reports/
data/.rubocop.yml DELETED
@@ -1,103 +0,0 @@
1
- AllCops:
2
- TargetRubyVersion: 2.5
3
- DisabledByDefault: true
4
- Exclude:
5
- - pkg/**/*.rb
6
- - test/spec/**/*.rb
7
-
8
- Bundler/DuplicatedGem:
9
- Enabled: true
10
- Lint:
11
- Enabled: true
12
- Security:
13
- Enabled: true
14
-
15
- # Single quotes being faster is hardly measurable and only affects parse time.
16
- # Enforcing double quotes reduces the times where you need to change them
17
- # when introducing an interpolation. Use single quotes only if their semantics
18
- # are needed.
19
- Style/StringLiterals:
20
- EnforcedStyle: double_quotes
21
- Exclude:
22
- - lib/riddler/protobuf/*_pb.rb
23
-
24
- # Seattle style
25
- Style/MethodDefParentheses:
26
- EnforcedStyle: require_no_parentheses
27
-
28
- # Seattle style
29
- Style/MethodCallWithoutArgsParentheses:
30
- Enabled: true
31
-
32
- # Seattle style
33
- Style/MethodCallWithArgsParentheses:
34
- Enabled: true
35
- EnforcedStyle: omit_parentheses
36
- AllowParenthesesInChaining: true
37
- IgnoredMethods:
38
- - watch
39
-
40
- Style/SpecialGlobalVars:
41
- Enabled: false
42
-
43
- Layout/FirstArrayElementIndentation:
44
- Enabled: true
45
- EnforcedStyle: consistent
46
-
47
- Style/Lambda:
48
- Enabled: true
49
- EnforcedStyle: literal
50
-
51
- # Allow regex argument in Seattle style
52
- Lint/AmbiguousRegexpLiteral:
53
- Enabled: false
54
-
55
- # Allow block argument in Seattle style
56
- Lint/AmbiguousOperator:
57
- Enabled: false
58
-
59
- Layout/SpaceInsideHashLiteralBraces:
60
- Enabled: true
61
- EnforcedStyleForEmptyBraces: no_space
62
- EnforcedStyle: no_space
63
-
64
- Layout/FirstHashElementIndentation:
65
- Enabled: true
66
- EnforcedStyle: consistent
67
-
68
- Layout/CaseIndentation:
69
- Enabled: true
70
- EnforcedStyle: end
71
-
72
- Layout/EndAlignment:
73
- Enabled: true
74
- EnforcedStyleAlignWith: variable
75
-
76
- Layout/SpaceAroundBlockParameters:
77
- Enabled: true
78
-
79
- Layout/SpaceBeforeBlockBraces:
80
- Enabled: true
81
-
82
- Layout/SpaceInsideBlockBraces:
83
- Enabled: true
84
-
85
- Layout/LineLength:
86
- Enabled: true
87
- Max: 120
88
- Exclude:
89
- - lib/riddler/protobuf/*_pb.rb
90
-
91
- Metrics/ClassLength:
92
- Enabled: true
93
- Max: 200
94
- Exclude:
95
- - test/**/*.rb
96
-
97
- Lint/SuppressedException:
98
- Exclude:
99
- - lib/tasks/**/*.rake
100
-
101
- Lint/Debugger:
102
- Exclude:
103
- - test/**/*
data/.simplecov DELETED
@@ -1,8 +0,0 @@
1
- SimpleCov.start do
2
- coverage_dir "reports/coverage"
3
-
4
- add_filter "/config/"
5
- add_filter "/lib/tasks/"
6
-
7
- track_files "lib/**/*.rb"
8
- end
data/Dangerfile DELETED
@@ -1,5 +0,0 @@
1
- # Check format of commit message
2
- commit_lint.check fail: :all
3
-
4
- # Make it more obvious that a PR is a work in progress and shouldn't be merged yet
5
- warn "PR is classed as Work in Progress" if github.pr_title.include? "[WIP]"
data/Gemfile DELETED
@@ -1,15 +0,0 @@
1
- source "https://rubygems.org"
2
-
3
- # Specify your gem's dependencies in uxid.gemspec
4
- gemspec
5
-
6
- gem "danger"
7
- gem "danger-commit_lint"
8
- gem "minitest", "~> 5.0"
9
- gem "minitest-focus"
10
- gem "minitest-reporters"
11
- gem "pry-nav"
12
- gem "rake", "~> 12.0"
13
- gem "rubycritic", require: false
14
- gem "rubocop"
15
- gem "simplecov", require: false