typeid_rails 0.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.
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypeidRails
4
+ class Error < StandardError; end
5
+
6
+ # Raised when parsing a string that is not a well-formed resource identifier.
7
+ class InvalidFormatError < Error
8
+ attr_reader :input
9
+
10
+ def initialize(input:, message:)
11
+ @input = input
12
+ super(message)
13
+ end
14
+ end
15
+
16
+ # Raised for a prefix outside TypeID's grammar, whether parsed or declared.
17
+ class InvalidPrefixError < InvalidFormatError; end
18
+
19
+ # Raised when a resource identifier carries a prefix other than the expected one.
20
+ # Only raised by strict consumers; lenient ones substitute the null UUID instead.
21
+ class PrefixMismatchError < Error
22
+ attr_reader :expected, :actual, :input
23
+
24
+ def initialize(expected:, actual:, input:)
25
+ @expected = expected
26
+ @actual = actual
27
+ @input = input
28
+ super("Expected prefix '#{expected}', got '#{actual}' in '#{input}'")
29
+ end
30
+ end
31
+
32
+ # Raised when two different models claim the same prefix.
33
+ class DuplicatePrefixError < Error
34
+ attr_reader :prefix, :existing_model, :conflicting_model
35
+
36
+ def initialize(prefix:, existing_model:, conflicting_model:)
37
+ @prefix = prefix
38
+ @existing_model = existing_model
39
+ @conflicting_model = conflicting_model
40
+ super("Duplicate prefix '#{prefix}' used by #{existing_model} and #{conflicting_model}")
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ require_relative "model_options"
6
+ require_relative "registry"
7
+
8
+ module TypeidRails
9
+ # Gives a model a public identifier: its UUID primary key is read and written as a
10
+ # prefixed string, and the prefix is registered so the model can be found from one.
11
+ #
12
+ # class User < ApplicationRecord
13
+ # include TypeidRails::Identifiable
14
+ # id_prefix "u"
15
+ # end
16
+ #
17
+ # User.first.id # => "u_2n1t201rmv87aae5j4csam8000"
18
+ module Identifiable
19
+ extend ActiveSupport::Concern
20
+ include ModelOptions
21
+
22
+ class_methods do
23
+ # Declares (with an argument) or reads (without) this model's prefix.
24
+ # @param prefix [String, nil]
25
+ # @param strict [Boolean, nil] see Type#initialize; nil follows typeid_options
26
+ # @param fuzzy [Boolean, nil] see Type#initialize; nil follows typeid_options
27
+ # @return [String, nil]
28
+ def id_prefix(prefix = nil, strict: nil, fuzzy: nil)
29
+ if prefix
30
+ attribute :id, build_typeid_type(prefix, strict:, fuzzy:)
31
+ @id_prefix = prefix
32
+ Registry.register(prefix, self)
33
+ end
34
+ @id_prefix
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+ require "active_support/core_ext/class/attribute"
5
+
6
+ require_relative "type"
7
+
8
+ module TypeidRails
9
+ # Per-model defaults for the attribute types this gem installs. Declared on an
10
+ # abstract base class, they are inherited by every model below it:
11
+ #
12
+ # class ApplicationRecord < ActiveRecord::Base
13
+ # primary_abstract_class
14
+ # include TypeidRails::AssociationSupport
15
+ # typeid_options fuzzy: true, strict: true
16
+ # end
17
+ #
18
+ # Identifiable and AssociationSupport include this, so it rarely needs including directly.
19
+ module ModelOptions
20
+ extend ActiveSupport::Concern
21
+
22
+ KEYS = [:fuzzy, :strict].freeze
23
+
24
+ included do
25
+ # @type self: singleton(ActiveRecord::Base)
26
+ defaults = {} #: Hash[Symbol, bool]
27
+ class_attribute :typeid_rails_options, instance_accessor: false, default: defaults.freeze
28
+ end
29
+
30
+ class_methods do
31
+ # Merges into the options inherited from the superclass, and returns the result.
32
+ # Applies to attributes declared afterwards.
33
+ # @param fuzzy [Boolean] see Type#initialize; unset follows TypeidRails.config
34
+ # @param strict [Boolean] see Type#initialize; unset means false
35
+ def typeid_options(**options)
36
+ unknown = options.keys - KEYS
37
+ raise ArgumentError, "Unknown typeid_options: #{unknown.join(", ")}" unless unknown.empty?
38
+
39
+ self.typeid_rails_options = typeid_rails_options.merge(options).freeze unless options.empty?
40
+ typeid_rails_options
41
+ end
42
+
43
+ # Builds a Type, filling options the caller left nil from this model's.
44
+ private def build_typeid_type(prefix, strict: nil, fuzzy: nil)
45
+ options = typeid_rails_options
46
+ Type.new(
47
+ prefix:,
48
+ strict: strict.nil? ? options.fetch(:strict, false) : strict,
49
+ fuzzy: fuzzy.nil? ? options[:fuzzy] : fuzzy,
50
+ )
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "type"
4
+
5
+ module TypeidRails
6
+ # An attribute type for a uuid column whose rows point at different models, with a
7
+ # sibling column naming which. Accepts an identifier under any prefix, or a raw
8
+ # UUID, and reads back the bare UUID, since only the owning record knows which
9
+ # prefix applies:
10
+ #
11
+ # class Preference < ApplicationRecord
12
+ # attribute :owner_id, TypeidRails::PolymorphicType.new
13
+ #
14
+ # def owner_id
15
+ # model = OWNER_MODELS[owner_type]
16
+ # model ? model.type_for_attribute("id").cast(super) : super
17
+ # end
18
+ # end
19
+ class PolymorphicType < Type
20
+ # @param strict [Boolean] when true, malformed input raises instead of becoming
21
+ # the null UUID
22
+ # @param fuzzy [Boolean, nil] see Type#initialize
23
+ def initialize(strict: false, fuzzy: nil)
24
+ super(prefix: "", strict:, fuzzy:)
25
+ end
26
+
27
+ def deserialize(value)
28
+ value&.downcase
29
+ end
30
+
31
+ def cast(value)
32
+ serialize(value)
33
+ end
34
+
35
+ def serialize(value)
36
+ return if value.nil?
37
+
38
+ value.is_a?(Value) ? value.to_uuid : string_to_uuid(value.to_s)
39
+ end
40
+
41
+ private def parse_or_null(string)
42
+ Value.parse(string, fuzzy: fuzzy?)
43
+ rescue InvalidFormatError
44
+ raise if strict?
45
+
46
+ nil
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Reaching this file means Rails is present, so the ActiveRecord integration is
4
+ # wanted and can simply be loaded. The railtie itself carries no behaviour.
5
+ require_relative "active_record"
6
+
7
+ module TypeidRails
8
+ class Railtie < ::Rails::Railtie
9
+ end
10
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+
5
+ module TypeidRails
6
+ # Process-wide prefix -> model mapping, letting a bare identifier string be routed
7
+ # back to the class that owns it.
8
+ module Registry
9
+ @prefixes = {}
10
+ @models = {}
11
+
12
+ class << self
13
+ # @raise [DuplicatePrefixError] when another model already claims this prefix
14
+ def register(prefix, model)
15
+ existing = @prefixes[prefix]
16
+ # Compared by name, not identity: a code reloader hands us a fresh class object
17
+ # for the same constant, which is a re-registration rather than a conflict.
18
+ if existing && name_of(existing) != name_of(model)
19
+ raise DuplicatePrefixError.new(
20
+ prefix:,
21
+ existing_model: name_of(existing),
22
+ conflicting_model: name_of(model),
23
+ )
24
+ end
25
+
26
+ @models.delete(existing) if existing
27
+ @prefixes[prefix] = model
28
+ @models[model] = prefix
29
+ model
30
+ end
31
+
32
+ def all_prefixes
33
+ @prefixes.dup
34
+ end
35
+
36
+ def model_for(prefix)
37
+ @prefixes[prefix]
38
+ end
39
+
40
+ def prefix_for(model)
41
+ @models[model]
42
+ end
43
+
44
+ def reset!
45
+ @prefixes = {}
46
+ @models = {}
47
+ end
48
+
49
+ private def name_of(model) = model.name || model.to_s
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+
5
+ require_relative "encoder"
6
+ require_relative "errors"
7
+ require_relative "value"
8
+
9
+ module TypeidRails
10
+ # An ActiveRecord attribute type that stores a UUID but exposes the prefixed
11
+ # identifier string, so a record's public identifier never has to be built by hand.
12
+ #
13
+ # Reading gives back a plain String rather than a Value, so the attribute stays
14
+ # comparable and serializable in the ways application code already expects.
15
+ class Type < ActiveRecord::Type::Value
16
+ attr_reader :prefix
17
+
18
+ # @param prefix [String] the type tag this attribute is presented under
19
+ # @param strict [Boolean] when true, malformed input and prefix mismatches raise.
20
+ # When false (the default), they collapse to the null UUID, which matches no row.
21
+ # @param fuzzy [Boolean, nil] see Configuration#fuzzy; nil follows the configuration
22
+ # at the time of each cast
23
+ # @raise [InvalidPrefixError]
24
+ def initialize(prefix:, strict: false, fuzzy: nil)
25
+ Value.validate_prefix!(prefix)
26
+ @prefix = prefix
27
+ @strict = strict
28
+ @fuzzy = fuzzy
29
+ super()
30
+ end
31
+
32
+ def strict? = @strict
33
+
34
+ def fuzzy?
35
+ fuzzy = @fuzzy
36
+ fuzzy.nil? ? TypeidRails.config.fuzzy : fuzzy
37
+ end
38
+
39
+ def type = :uuid
40
+
41
+ def deserialize(value)
42
+ return if value.nil?
43
+
44
+ Value.from_uuid(value, prefix:).to_s
45
+ end
46
+
47
+ def cast(value)
48
+ return if value.nil?
49
+
50
+ case value
51
+ when Value then value.to_s
52
+ when String then cast_string(value)
53
+ else cast_string(value.to_s)
54
+ end
55
+ end
56
+
57
+ def serialize(value)
58
+ return if value.nil?
59
+
60
+ case value
61
+ when Value then value.to_uuid
62
+ when String then string_to_uuid(value)
63
+ else value
64
+ end
65
+ end
66
+
67
+ # The exposed value is a frozen String rebuilt on every read, so in-place mutation
68
+ # is not a thing that can happen.
69
+ def changed_in_place?(_raw_old_value, _new_value)
70
+ false
71
+ end
72
+
73
+ private def cast_string(string)
74
+ uuid = string_to_uuid(string)
75
+ Value.from_uuid(uuid, prefix:).to_s
76
+ end
77
+
78
+ private def string_to_uuid(string)
79
+ # Raw UUIDs are accepted so that seeds, migrations and hand-written queries
80
+ # do not have to encode first.
81
+ return string.downcase if string.match?(Encoder::UUID_REGEX)
82
+
83
+ parsed = parse_or_null(string)
84
+ parsed ? parsed.to_uuid : Encoder::NULL_UUID
85
+ end
86
+
87
+ private def parse_or_null(string)
88
+ parsed = Value.parse(string, expected_prefix: prefix, fuzzy: fuzzy?)
89
+ raise PrefixMismatchError.new(expected: prefix, actual: string.rpartition("_").first, input: string) if parsed.nil? && strict?
90
+
91
+ parsed
92
+ rescue InvalidFormatError
93
+ raise if strict?
94
+
95
+ nil
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ require_relative "configuration"
6
+ require_relative "encoder"
7
+ require_relative "errors"
8
+
9
+ module TypeidRails
10
+ # An immutable TypeID: a 128-bit UUID plus the type prefix it is presented under.
11
+ # Renders as "<prefix>_<base32>", e.g. "u_2n1t201rmv87aae5j4csam8000", or as the
12
+ # bare suffix when the prefix is empty.
13
+ class Value
14
+ # Lowercase ASCII letters and underscores, at most 63 characters, neither starting
15
+ # nor ending with an underscore. Empty is allowed.
16
+ PREFIX_FORMAT = /\A(?:[a-z](?:[a-z_]{0,61}[a-z])?)?\z/
17
+
18
+ attr_reader :uuid, :prefix, :encoded
19
+
20
+ def initialize(uuid:, prefix:, encoded:)
21
+ @uuid = uuid.downcase.freeze
22
+ @prefix = prefix.freeze
23
+ @encoded = encoded.downcase.freeze
24
+ freeze
25
+ end
26
+
27
+ class << self
28
+ def valid_prefix?(prefix)
29
+ prefix.is_a?(String) && prefix.match?(PREFIX_FORMAT)
30
+ end
31
+
32
+ # @raise [InvalidPrefixError]
33
+ def validate_prefix!(prefix)
34
+ return if valid_prefix?(prefix)
35
+
36
+ raise InvalidPrefixError.new(input: prefix.to_s, message: "Invalid prefix: #{prefix.inspect}")
37
+ end
38
+
39
+ # @raise [InvalidPrefixError]
40
+ def from_uuid(uuid, prefix:)
41
+ validate_prefix!(prefix)
42
+ normalized_uuid = uuid.downcase
43
+ new(uuid: normalized_uuid, prefix:, encoded: Encoder.encode(normalized_uuid))
44
+ end
45
+
46
+ # Mints a new identifier from a random UUID.
47
+ # @param version [Integer] 4 or 7; see Configuration#uuid_version
48
+ def generate(prefix:, version: TypeidRails.config.uuid_version)
49
+ Configuration.validate_uuid_version!(version)
50
+ from_uuid(version == 7 ? SecureRandom.uuid_v7 : SecureRandom.uuid, prefix:)
51
+ end
52
+
53
+ # @param string [String, nil] a "<prefix>_<base32>" identifier, or a bare suffix
54
+ # @param expected_prefix [String, nil] when given, returns nil on a prefix mismatch
55
+ # @param fuzzy [Boolean] see Configuration#fuzzy; affects the suffix only
56
+ # @raise [InvalidFormatError] when the string is not a well-formed identifier
57
+ # @return [Value, nil]
58
+ def parse(string, expected_prefix: nil, fuzzy: TypeidRails.config.fuzzy)
59
+ return if string.nil?
60
+
61
+ string = string.to_s
62
+ # Prefixes may contain underscores, so the separator is the last one.
63
+ prefix, separator, encoded_part = string.rpartition("_")
64
+ if !separator.empty? && prefix.empty?
65
+ raise InvalidFormatError.new(input: string, message: "Invalid format: separator without a prefix")
66
+ end
67
+ unless valid_prefix?(prefix)
68
+ raise InvalidPrefixError.new(input: string, message: "Invalid format: invalid prefix")
69
+ end
70
+ unless Encoder.valid?(encoded_part, fuzzy:)
71
+ raise InvalidFormatError.new(input: string, message: "Invalid format: invalid suffix")
72
+ end
73
+
74
+ return if expected_prefix && prefix != expected_prefix
75
+
76
+ from_uuid(Encoder.decode(encoded_part, fuzzy:), prefix:)
77
+ end
78
+ end
79
+
80
+ def to_s
81
+ prefix.empty? ? encoded : "#{prefix}_#{encoded}"
82
+ end
83
+
84
+ def to_uuid
85
+ uuid
86
+ end
87
+
88
+ # @see Configuration#equality
89
+ def ==(other)
90
+ uuid_only = TypeidRails.config.equality == :uuid
91
+ case other
92
+ when Value
93
+ uuid == other.uuid && (uuid_only || prefix == other.prefix)
94
+ when String
95
+ to_s == other || (uuid_only && uuid == other.downcase)
96
+ else
97
+ false
98
+ end
99
+ end
100
+ alias_method :eql?, :==
101
+
102
+ def hash
103
+ TypeidRails.config.equality == :uuid ? uuid.hash : [uuid, prefix].hash
104
+ end
105
+
106
+ def inspect
107
+ "#<#{self.class.name} #{self}>"
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypeidRails
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "typeid_rails/version"
4
+ require_relative "typeid_rails/configuration"
5
+ require_relative "typeid_rails/errors"
6
+ require_relative "typeid_rails/base32"
7
+ require_relative "typeid_rails/encoder"
8
+ require_relative "typeid_rails/value"
9
+ require_relative "typeid_rails/registry"
10
+
11
+ # TypeID identifiers (https://github.com/jetify-com/typeid) for 128-bit UUIDs.
12
+ #
13
+ # TypeidRails.encode("1cb2589e-342d-4829-958f-e63d8ae58365", prefix: "u")
14
+ # # => "u_0wp9c9wd1d90msb3z67p5eb0v5"
15
+ module TypeidRails
16
+ # Loaded on first use so the `sha3` gem stays an optional dependency.
17
+ autoload :Derivation, "typeid_rails/derivation"
18
+
19
+ class << self
20
+ # @return [String] the "<prefix>_<base32>" rendering of a UUID, or the bare
21
+ # suffix when the prefix is empty
22
+ def encode(uuid, prefix:)
23
+ Value.from_uuid(uuid, prefix:).to_s
24
+ end
25
+
26
+ # @return [String] a newly minted identifier
27
+ # @see Value.generate
28
+ def generate(prefix:, version: config.uuid_version)
29
+ Value.generate(prefix:, version:).to_s
30
+ end
31
+
32
+ # @return [Value, nil]
33
+ # @raise [InvalidFormatError] when the string is not a well-formed identifier
34
+ def parse(string, expected_prefix: nil, fuzzy: config.fuzzy)
35
+ Value.parse(string, expected_prefix:, fuzzy:)
36
+ end
37
+
38
+ # Extracts the UUID from an identifier, or nil when it is malformed or carries
39
+ # an unexpected prefix. Use #parse when a malformed input should raise instead.
40
+ # @return [String, nil]
41
+ def to_uuid(prefixed_id, expected_prefix: nil, fuzzy: config.fuzzy)
42
+ return if prefixed_id.nil?
43
+
44
+ Value.parse(prefixed_id, expected_prefix:, fuzzy:)&.to_uuid
45
+ rescue InvalidFormatError
46
+ nil
47
+ end
48
+
49
+ # Derives a stable identifier from its input parts. Requires the `sha3` gem.
50
+ # @see Derivation.uuid
51
+ def derive_uuid(*parts)
52
+ Derivation.uuid(*parts)
53
+ end
54
+ end
55
+ end
56
+
57
+ require_relative "typeid_rails/railtie" if defined?(::Rails::Railtie)
@@ -0,0 +1,29 @@
1
+ module TypeidRails
2
+ # See the note on _IdentifiableClass: this is the surface of the including class
3
+ # that the `class_methods` block body uses.
4
+ interface _AssociationSupportClass
5
+ def attribute: (Symbol | String name, untyped type) -> untyped
6
+ def belongs_to: (untyped name, ?untyped? scope, **untyped options) ?{ () -> void } -> untyped
7
+ def attribute_types: () -> Hash[String, untyped]
8
+ def reflect_on_association: (untyped name) -> untyped
9
+ def reflect_on_all_associations: (?Symbol macro) -> Array[untyped]
10
+ end
11
+
12
+ module AssociationSupport : Module, _AssociationSupportClass, _ModelOptionsClass
13
+ extend ActiveSupport::Concern
14
+ include ModelOptions
15
+
16
+ def belongs_to: (untyped name, ?untyped? scope, **untyped options) ?{ () -> void } -> untyped
17
+ def configure_foreign_keys!: () -> void
18
+
19
+ private
20
+
21
+ def configure_foreign_key_for_association: (untyped association) -> void
22
+ def resolve_association_class: (untyped association) -> untyped
23
+
24
+ # Generated by ActiveSupport::Concern; extended into the including class.
25
+ module ClassMethods
26
+ def configure_foreign_keys!: () -> void
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,19 @@
1
+ module TypeidRails
2
+ module Base32
3
+ ALPHABET: String
4
+ ALPHABET_CHARS: Array[String]
5
+ BYTE_SIZE: Integer
6
+ LENGTH: Integer
7
+ STRICT_DECODE_MAP: Hash[String, Integer]
8
+ FUZZY_DECODE_MAP: Hash[String, Integer]
9
+ SHIFT: Integer
10
+ MASK: Integer
11
+
12
+ def self.encode_bytes: (String bytes) -> String
13
+ def self.decode_bytes: (String base32, ?fuzzy: bool) -> String
14
+ def self.valid128?: (untyped string, ?fuzzy: bool) -> bool
15
+
16
+ private
17
+ def self.to_values: (String string, fuzzy: bool) -> Array[Integer]?
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ module TypeidRails
2
+ class Configuration
3
+ UUID_VERSIONS: Array[Integer]
4
+ EQUALITY_MODES: Array[Symbol]
5
+
6
+ attr_accessor fuzzy: bool
7
+ attr_reader uuid_version: Integer
8
+ attr_reader equality: Symbol
9
+
10
+ def initialize: () -> void
11
+ def uuid_version=: (Integer version) -> Integer
12
+ def equality=: (Symbol mode) -> Symbol
13
+ def self.validate_uuid_version!: (untyped version) -> void
14
+ end
15
+
16
+ self.@config: Configuration?
17
+
18
+ def self.config: () -> Configuration
19
+ def self.configure: () { (Configuration) -> void } -> void
20
+ def self.reset_config!: () -> Configuration
21
+ end
@@ -0,0 +1,5 @@
1
+ module TypeidRails
2
+ module Derivation
3
+ def self.uuid: (*String parts) -> String
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ module TypeidRails
2
+ module Encoder
3
+ NULL_UUID: String
4
+ UUID_REGEX: Regexp
5
+
6
+ def self.encode: (String uuid) -> String
7
+ def self.decode: (String base32, ?fuzzy: bool) -> String
8
+ def self.valid?: (untyped string, ?fuzzy: bool) -> bool
9
+ def self.format_uuid: (String hex) -> String
10
+ end
11
+ end
@@ -0,0 +1,29 @@
1
+ module TypeidRails
2
+ class Error < StandardError
3
+ end
4
+
5
+ class InvalidFormatError < Error
6
+ attr_reader input: String
7
+
8
+ def initialize: (input: String, message: String) -> void
9
+ end
10
+
11
+ class InvalidPrefixError < InvalidFormatError
12
+ end
13
+
14
+ class PrefixMismatchError < Error
15
+ attr_reader expected: String
16
+ attr_reader actual: String?
17
+ attr_reader input: String
18
+
19
+ def initialize: (expected: String, actual: String?, input: String) -> void
20
+ end
21
+
22
+ class DuplicatePrefixError < Error
23
+ attr_reader prefix: String
24
+ attr_reader existing_model: String
25
+ attr_reader conflicting_model: String
26
+
27
+ def initialize: (prefix: String, existing_model: String, conflicting_model: String) -> void
28
+ end
29
+ end
@@ -0,0 +1,23 @@
1
+ module TypeidRails
2
+ # ActiveSupport::Concern's `class_methods` block body is checked as instance methods
3
+ # of the concern itself, with `self` being the including class. This describes the
4
+ # part of that class the block relies on.
5
+ interface _IdentifiableClass
6
+ def attribute: (Symbol | String name, untyped type) -> untyped
7
+ end
8
+
9
+ module Identifiable : Module, _IdentifiableClass, _ModelOptionsClass
10
+ extend ActiveSupport::Concern
11
+ include ModelOptions
12
+
13
+ @id_prefix: String?
14
+
15
+ # Declares this model's prefix when given one, and reads it back either way.
16
+ def id_prefix: (?String? prefix, ?strict: bool?, ?fuzzy: bool?) -> String?
17
+
18
+ # Generated by ActiveSupport::Concern; extended into the including class.
19
+ module ClassMethods
20
+ def id_prefix: (?String? prefix, ?strict: bool?, ?fuzzy: bool?) -> String?
21
+ end
22
+ end
23
+ end