Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Sequel #8

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
### Added

- The ability to configure the random generator for the gem via `KSUID.configure`. This allows you to set up random generation to the specifications you need, whether that is for speed or for security.
- A plugin for Sequel to support KSUID fields. You can include the plugin via `plugin :ksuid` within a `Sequel::Model` class. By default, the field will be saved as a string-serialized field; if you prefer binary KSUIDs, you can pass the `binary: true` option. You can also wrap the KSUID field in a typed value with `wrap: true`.

### Changed

Expand Down
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,8 @@ group :ci do
end

group :test do
gem 'jdbc-sqlite3', platforms: %i[jruby]
gem 'rspec', '~> 3.6'
gem 'sequel'
gem 'sqlite3', platforms: %i[mri mingw x64_mingw]
end
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,44 @@ KSUID.configure do |config|
end
```

### Using with Sequel

When using Sequel, you can enable a plugin to turn a field into an auto-generated KSUID. Because Sequel favors being explicit, nearly everything about the plugin is opt-in. The only default behavior is as follows:

1. The default column name is `ksuid`
2. The field is automatically generated prior to validation

There are two steps to use KSUIDs within Sequel. First, you will need to add a column to your model for the KSUID. By default, the plugin uses string serialization for its field, which looks like this:

```ruby
DB.create_table(:events) do
String :my_field_name
end
```

If you wish to use a binary-serialized column, you can use the `blob` method:

```ruby
DB.create_table(:events) do
blob :ksuid
end
```

To use the KSUID plugin, activate it within your model:

```ruby
class Event < Sequel::Model(:events)
plugin :ksuid
end
```

During this activation, there are a few options you can choose to enable:

* `binary: true` - If you prefer binary KSUIDs, you can switch from the default string serialization by specifying that you want it to be a binary field.
* `field: <my_field_name>` - By default, the column is named `ksuid`. If you want to specify a different name, you can use the `field` option to name it what you like.
* `force: true` - Typically, you will want to generate a KSUID when you're saving a record. If you want to ensure this happens, you can force the plugin to overwrite the field when doing the first save for a record. Note that the plugin will overwrite a manually set value in this mode.
* `wrap: true` - By default, the plugin will return your KSUID in its string- (or binary-) serialized form instead of as the KSUID type. If you want to wrap the accessors for the field to make them use the KSUID type, you can tell the plugin to wrap the field.

## Contributing

So you’re interested in contributing to KSUID? Check out our [contributing guidelines](CONTRIBUTING.md) for more information on how to do that.
Expand Down
1 change: 1 addition & 0 deletions config.reek
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
ManualDispatch:
exclude:
- "KSUID::Configuration#assert_generator_is_callable"
- "Sequel::Plugins::Ksuid::InstanceMethods#set_ksuid"

UncommunicativeModuleName:
exclude:
Expand Down
38 changes: 38 additions & 0 deletions lib/ksuid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require_relative 'ksuid/configuration'
require_relative 'ksuid/type'
require_relative 'ksuid/version'
require_relative 'sequel/plugins/ksuid' if defined?(Sequel)

# The K-Sortable Unique IDentifier (KSUID)
#
Expand Down Expand Up @@ -75,6 +76,28 @@ module KSUID
# @return [String]
MAX_STRING_ENCODED = 'aWgEPTl1tmebfsQzFP4bxwgy80V'

# Converts a KSUID-compatible value into an actual KSUID
#
# @api public
#
# @example Converts a base 62 KSUID string into a KSUID
# KSUID.call('15Ew2nYeRDscBipuJicYjl970D1')
#
# @param ksuid [String, Array<Integer>, KSUID::Type] the KSUID-compatible value
# @return [KSUID::Type] the converted KSUID
# @raise [ArgumentError] if the value is not KSUID-compatible
def self.call(ksuid)
return unless ksuid

case ksuid
when KSUID::Type then ksuid
when Array then KSUID.from_bytes(ksuid)
when String then cast_string(ksuid)
else
raise ArgumentError, "Cannot convert #{ksuid.inspect} to KSUID"
end
end

# The configuration for creating new KSUIDs
#
# @api private
Expand Down Expand Up @@ -167,4 +190,19 @@ def self.max
def self.new(payload: nil, time: Time.now)
Type.new(payload: payload, time: time)
end

# Casts a string into a KSUID
#
# @api private
#
# @param ksuid [String] the string to convert into a KSUID
# @return [KSUID::Type] the converted KSUID
def self.cast_string(ksuid)
if Base62.compatible?(ksuid)
KSUID.from_base62(ksuid)
else
KSUID.from_bytes(ksuid)
end
end
private_class_method :cast_string
end
15 changes: 15 additions & 0 deletions lib/ksuid/base62.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,21 @@ module Base62
# @api private
BASE = CHARSET.size

# Checks whether a string is a base 62-compatible string
#
# @api public
#
# @example Checks a KSUID for base 62 compatibility
# KSUID::Base62.compatible?("15Ew2nYeRDscBipuJicYjl970D1") #=> true
#
# @param string [String] the string to check for compatibility
# @return [Boolean]
def self.compatible?(string)
return false unless string.to_s == string

string.each_char.all? { |char| CHARSET.include?(char) }
end

# Decodes a base 62-encoded string into an integer
#
# @api public
Expand Down
236 changes: 236 additions & 0 deletions lib/sequel/plugins/ksuid.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
# frozen_string_literal: true

module Sequel # :nodoc:
module Plugins # :nodoc:
# Adds KSUID support to the Sequel ORM
#
# @api public
#
# @example Creates a model with a standard, string-based KSUID
# connection_string = 'sqlite:/'
# connection_string = 'jdbc:sqlite::memory:' if RUBY_ENGINE == 'jruby'
# DB = Sequel.connect(connection_string)
#
# DB.create_table!(:events) do
# Integer :id
# String :ksuid
# end
#
# class Event < Sequel::Model(:events)
# plugin :ksuid
# end
#
# @example Creates a model with a customized KSUID field
# connection_string = 'sqlite:/'
# connection_string = 'jdbc:sqlite::memory:' if RUBY_ENGINE == 'jruby'
# DB = Sequel.connect(connection_string)
#
# DB.create_table!(:events) do
# Integer :id
# String :correlation_id
# end
#
# class Event < Sequel::Model(:events)
# plugin :ksuid, field: :correlation_id
# end
#
# @example Creates a model that always overwrites the KSUID on save
# connection_string = 'sqlite:/'
# connection_string = 'jdbc:sqlite::memory:' if RUBY_ENGINE == 'jruby'
# DB = Sequel.connect(connection_string)
#
# DB.create_table!(:events) do
# Integer :id
# String :ksuid
# end
#
# class Event < Sequel::Model(:events)
# plugin :ksuid, force: true
# end
#
# @example Creates a model with a binary-encoded KSUID
# connection_string = 'sqlite:/'
# connection_string = 'jdbc:sqlite::memory:' if RUBY_ENGINE == 'jruby'
# DB = Sequel.connect(connection_string)
#
# DB.create_table!(:events) do
# Integer :id
# blob :ksuid
# end
#
# class Event < Sequel::Model(:events)
# plugin :ksuid, binary: true
# end
module Ksuid
# Configures the plugin by setting available options
#
# @api private
#
# @param model [Sequel::Model] the model to configure
# @param options [Hash] the hash of available options
# @option options [Boolean] :binary encode the KSUID as a binary string
# @option options [Boolean] :field the field to use as a KSUID
# @option options [Boolean] :force overwrite the field on save
# @option options [Boolean] :wrap wraps the KSUID into a KSUID type
# @return [void]
def self.configure(model, options = OPTS)
model.instance_exec do
extract_configuration(options)
define_ksuid_accessor
end
end

# Class methods that are extended onto an enabling model class
#
# @api private
module ClassMethods
# The field that is enabled with KSUID handling
#
# @api private
#
# @return [Symbol]
attr_reader :ksuid_field

# Checks whether the KSUID should be binary encoded
#
# @api private
#
# @return [Boolean]
def ksuid_binary?
@ksuid_binary
end

# Defines an accessor for the KSUID that converts it into a KSUID
#
# @api private
#
# @return [void]
def define_ksuid_accessor
return unless @ksuid_wrap

define_ksuid_getter
define_ksuid_setter
end

# Defines a getter for the KSUID that converts it into a KSUID
#
# @api private
#
# @return [void]
def define_ksuid_getter
define_method(@ksuid_field) do
KSUID.call(super())
end
end

# Defines a setter for the KSUID that converts the value properly
#
# @api private
#
# @return [void]
def define_ksuid_setter
define_method("#{@ksuid_field}=") do |ksuid|
ksuid = KSUID.call(ksuid)

if self.class.ksuid_binary?
super(ksuid.to_bytes)
else
super(ksuid.to_s)
end
end
end

# Extracts all configuration options from the configure step
#
# @api private
#
# @return [void]
def extract_configuration(options)
@ksuid_binary = options.fetch(:binary, false)
@ksuid_field = options.fetch(:field, :ksuid)
@ksuid_overwrite = options.fetch(:force, false)
@ksuid_wrap = options.fetch(:wrap, false)
end

# Checks whether the KSUID should be overwritten upon save
#
# @api private
#
# @return [Boolean]
def ksuid_overwrite?
@ksuid_overwrite
end

# Checks whether the model should wrap its KSUID field in a type
#
# @api private
#
# @return [Boolean]
def ksuid_wrap?
@ksuid_wrap
end

Plugins.inherited_instance_variables(
self,
:@ksuid_binary => nil,
:@ksuid_field => nil,
:@ksuid_overwrite => nil,
:@ksuid_wrap => nil
)
end

# Instance methods that are included in an enabling model class
#
# @api private
module InstanceMethods
# Generates a KSUID for the field before validation
#
# @api private
#
# @return [void]
def before_validation
set_ksuid if new?
super
end

private

# A hook method for generating a new KSUID
#
# @api private
#
# @return [String] a binary or base 62-encoded string
def create_ksuid
ksuid = KSUID.new

if self.class.ksuid_binary?
ksuid.to_bytes
else
ksuid.to_s
end
end

# Initializes the KSUID field when it is not set, or overwrites it if enabled
#
# Note: The disabled Rubocop rule is to allow the method to follow
# Sequel conventions.
#
# @api private
#
# @param ksuid [String] the normal string or byte string of the KSUID
# @return [void]
# rubocop:disable Naming/AccessorMethodName
def set_ksuid(ksuid = create_ksuid)
field = model.ksuid_field
setter = :"#{field}="

return unless respond_to?(field) &&
respond_to?(setter) &&
(model.ksuid_overwrite? || !get_column_value(field))

set_column_value(setter, ksuid)
end
end
end
end
end
Loading