activeid 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +21 -0
  3. data/.github/workflows/macos.yml +45 -0
  4. data/.github/workflows/ubuntu.yml +47 -0
  5. data/.github/workflows/windows.yml +40 -0
  6. data/.gitignore +195 -0
  7. data/.hound.yml +3 -0
  8. data/.rubocop.yml +18 -0
  9. data/Gemfile +7 -0
  10. data/LICENSE.md +19 -0
  11. data/README.adoc +411 -0
  12. data/Rakefile +27 -0
  13. data/activeid.gemspec +42 -0
  14. data/examples/name_based_uuids.rb +92 -0
  15. data/examples/registering_active_record_type.rb +74 -0
  16. data/examples/storing_uuids_as_binaries.rb +88 -0
  17. data/examples/storing_uuids_as_strings.rb +81 -0
  18. data/examples/storing_uuids_natively.rb +93 -0
  19. data/examples/time_based_uuids.rb +58 -0
  20. data/examples/using_migrations.rb +50 -0
  21. data/gemfiles/Rails-5_0.gemfile +8 -0
  22. data/gemfiles/Rails-5_1.gemfile +8 -0
  23. data/gemfiles/Rails-5_2.gemfile +8 -0
  24. data/gemfiles/Rails-head.gemfile +8 -0
  25. data/lib/active_id.rb +12 -0
  26. data/lib/active_id/all.rb +2 -0
  27. data/lib/active_id/connection_patches.rb +65 -0
  28. data/lib/active_id/model.rb +55 -0
  29. data/lib/active_id/railtie.rb +12 -0
  30. data/lib/active_id/type.rb +100 -0
  31. data/lib/active_id/utils.rb +77 -0
  32. data/lib/active_id/version.rb +3 -0
  33. data/spec/integration/examples_for_uuid_models.rb +92 -0
  34. data/spec/integration/examples_for_uuid_models_having_namespaces.rb +12 -0
  35. data/spec/integration/examples_for_uuid_models_having_natural_keys.rb +11 -0
  36. data/spec/integration/migrations_spec.rb +92 -0
  37. data/spec/integration/model_without_uuids_spec.rb +44 -0
  38. data/spec/integration/no_patches_spec.rb +26 -0
  39. data/spec/integration/storing_uuids_as_binaries_spec.rb +34 -0
  40. data/spec/integration/storing_uuids_as_strings_spec.rb +22 -0
  41. data/spec/spec_helper.rb +64 -0
  42. data/spec/support/0_logger.rb +2 -0
  43. data/spec/support/1_db_connection.rb +3 -0
  44. data/spec/support/2_db_cleaner.rb +14 -0
  45. data/spec/support/database.yml +12 -0
  46. data/spec/support/fabricators.rb +15 -0
  47. data/spec/support/models.rb +41 -0
  48. data/spec/support/schema.rb +38 -0
  49. data/spec/unit/attribute_type_spec.rb +70 -0
  50. data/spec/unit/utils_spec.rb +97 -0
  51. metadata +313 -0
@@ -0,0 +1,58 @@
1
+ # Time-based UUIDs (version 1) store timestamp of their creation, and are
2
+ # monotonically increasing in time. This is very advantageous in some
3
+ # use cases.
4
+
5
+ ENV["DB"] ||= "sqlite3"
6
+
7
+ require "bundler/setup"
8
+ Bundler.require :development
9
+
10
+ require "active_id"
11
+ require_relative "../spec/support/0_logger"
12
+ require_relative "../spec/support/1_db_connection"
13
+
14
+ #### SCHEMA ####
15
+
16
+ ActiveRecord::Schema.define do
17
+ create_table :authors, id: false, force: true do |t|
18
+ t.string :id, limit: 36, primary_key: true
19
+ t.string :name
20
+ t.timestamps
21
+ end
22
+ end
23
+
24
+ #### MODELS ####
25
+
26
+ class Author < ActiveRecord::Base
27
+ include ActiveID::Model
28
+ attribute :id, ActiveID::Type::StringUUID.new
29
+ uuid_generator :time
30
+ end
31
+
32
+ #### PROOF ####
33
+
34
+ SolidAssert.enable_assertions
35
+
36
+ poe = Author.create! name: "Edgar Alan Poe"
37
+ thu = Author.create! name: "Thucydides"
38
+ fon = Author.create! name: "Jean de La Fontaine"
39
+ kas = Author.create! name: "Jan Kasprowicz"
40
+
41
+ # Version 1 means time-based UUIDs
42
+ assert poe.id.version == 1
43
+
44
+ # Timestamp can be extracted from UUID
45
+ assert poe.id.timestamp.between? 1.minute.ago, 1.minute.from_now
46
+
47
+ # Lexicographical ordering of version 1 UUIDs reflects their temporal ordering
48
+ assert Author.all.order(id: :asc).to_a == [poe, thu, fon, kas]
49
+
50
+ #### PROVE THAT ASSERTIONS WERE WORKING ####
51
+
52
+ begin
53
+ assert 1 == 2
54
+ rescue SolidAssert::AssertionFailedError
55
+ puts "All OK."
56
+ else
57
+ raise "Assertions do not work!"
58
+ end
@@ -0,0 +1,50 @@
1
+ # Active UUID features a convenience #uuid method, which may be used to create
2
+ # a binary column in database migration. Since it involves monkey patching,
3
+ # "active_id/all" must be loaded.
4
+
5
+ ENV["DB"] ||= "sqlite3"
6
+
7
+ require "bundler/setup"
8
+ Bundler.require :development
9
+
10
+ # Note "active_id/all", which registers new column definitions!
11
+ require "active_id/all"
12
+
13
+ require_relative "../spec/support/0_logger"
14
+ require_relative "../spec/support/1_db_connection"
15
+
16
+ #### SCHEMA ####
17
+
18
+ ActiveRecord::Schema.define do
19
+ create_table :authors, id: false, force: true do |t|
20
+ t.uuid :id, primary_key: true
21
+ t.string :name
22
+ t.timestamps
23
+ end
24
+ end
25
+
26
+ #### PROOF ####
27
+
28
+ SolidAssert.enable_assertions
29
+
30
+ id_column = ActiveRecord::Base.connection.columns("authors")[0]
31
+ assert id_column.name == "id"
32
+
33
+ case ENV["DB"]
34
+ when "sqlite3"
35
+ assert id_column.sql_type == "binary(16)"
36
+ when "mysql"
37
+ assert id_column.sql_type == "varbinary(16)"
38
+ when "postgresql"
39
+ assert id_column.sql_type == "uuid"
40
+ end
41
+
42
+ #### PROVE THAT ASSERTIONS WERE WORKING ####
43
+
44
+ begin
45
+ assert 1 == 2
46
+ rescue SolidAssert::AssertionFailedError
47
+ puts "All OK."
48
+ else
49
+ raise "Assertions do not work!"
50
+ end
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec path: "../"
4
+
5
+ gem "activerecord", "~> 5.0.0"
6
+
7
+ gem "codecov", require: false, group: :test
8
+ gem "simplecov", require: false, group: :test
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec path: "../"
4
+
5
+ gem "activerecord", "~> 5.1.0"
6
+
7
+ gem "codecov", require: false, group: :test
8
+ gem "simplecov", require: false, group: :test
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec path: "../"
4
+
5
+ gem "activerecord", "~> 5.2.0"
6
+
7
+ gem "codecov", require: false, group: :test
8
+ gem "simplecov", require: false, group: :test
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec path: "../"
4
+
5
+ gem "activerecord", github: "rails/rails"
6
+
7
+ gem "codecov", require: false, group: :test
8
+ gem "simplecov", require: false, group: :test
@@ -0,0 +1,12 @@
1
+ require "active_id/version"
2
+ require "active_id/utils"
3
+ require "active_id/model"
4
+ require "active_id/type"
5
+ require "active_id/railtie" if defined?(Rails::Railtie)
6
+ require "pp"
7
+
8
+ module ActiveID
9
+ class << self
10
+ delegate :quote_as_binary, to: Utils
11
+ end
12
+ end
@@ -0,0 +1,2 @@
1
+ require_relative "../active_id"
2
+ require_relative "connection_patches"
@@ -0,0 +1,65 @@
1
+ require "active_record"
2
+ require "active_support/concern"
3
+
4
+ module ActiveID
5
+ module ConnectionPatches
6
+ module ColumnMethods
7
+ def uuid(*args, **options)
8
+ args.each { |name| column(name, :uuid, options) }
9
+ end
10
+ end
11
+
12
+ module Quoting
13
+ extend ActiveSupport::Concern
14
+
15
+ def self.prepended(_klass)
16
+ def native_database_types
17
+ super.merge(uuid: { name: "binary", limit: 16 })
18
+ end
19
+ end
20
+ end
21
+
22
+ module PostgreSQLQuoting
23
+ extend ActiveSupport::Concern
24
+
25
+ def self.prepended(_klass)
26
+ def native_database_types
27
+ super.merge(uuid: { name: "uuid" })
28
+ end
29
+ end
30
+ end
31
+
32
+ module ConnectionHandling
33
+ def establish_connection(*_args) # rubocop:disable Metrics/MethodLength
34
+ super
35
+
36
+ arca = ActiveRecord::ConnectionAdapters
37
+
38
+ arca::Table.include(ColumnMethods)
39
+ arca::TableDefinition.include(ColumnMethods)
40
+
41
+ if defined? arca::MysqlAdapter
42
+ arca::MysqlAdapter.prepend(Quoting)
43
+ end
44
+
45
+ if defined? arca::Mysql2Adapter
46
+ arca::Mysql2Adapter.prepend(Quoting)
47
+ end
48
+
49
+ if defined? arca::SQLite3Adapter
50
+ arca::SQLite3Adapter.prepend(Quoting)
51
+ end
52
+
53
+ if defined? arca::PostgreSQLAdapter
54
+ arca::PostgreSQLAdapter.prepend(PostgreSQLQuoting)
55
+ end
56
+ end
57
+ end
58
+
59
+ def self.apply!
60
+ ActiveRecord::Base.singleton_class.prepend ConnectionHandling
61
+ end
62
+ end
63
+ end
64
+
65
+ ActiveID::ConnectionPatches.apply!
@@ -0,0 +1,55 @@
1
+ require "active_support"
2
+
3
+ module ActiveID
4
+ # Include this module into all models which are meant to store UUIDs.
5
+ module Model
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :_natural_key, instance_writer: false
10
+ class_attribute :_uuid_namespace, instance_writer: false
11
+ class_attribute :_uuid_generator, instance_writer: false
12
+ self._uuid_generator = :random
13
+
14
+ before_create :generate_uuids_if_needed
15
+ end
16
+
17
+ module ClassMethods
18
+ def natural_key(*attributes)
19
+ self._natural_key = attributes
20
+ end
21
+
22
+ def uuid_namespace(namespace)
23
+ namespace = Utils.cast_to_uuid(namespace)
24
+ self._uuid_namespace = namespace
25
+ end
26
+
27
+ def uuid_generator(generator_name)
28
+ self._uuid_generator = generator_name
29
+ end
30
+ end
31
+
32
+ def create_uuid
33
+ if _natural_key
34
+ # TODO if all the attributes return nil you might want to warn about this
35
+ chained = _natural_key.map { |attribute| send(attribute) }.join("-")
36
+ UUIDTools::UUID.sha1_create(_uuid_namespace || UUIDTools::UUID_OID_NAMESPACE, chained)
37
+ else
38
+ case _uuid_generator
39
+ when :random
40
+ UUIDTools::UUID.random_create
41
+ when :time
42
+ UUIDTools::UUID.timestamp_create
43
+ end
44
+ end
45
+ end
46
+
47
+ def generate_uuids_if_needed
48
+ primary_key = self.class.primary_key
49
+ primary_key_attribute_type = self.class.type_for_attribute(primary_key)
50
+ if ::ActiveID::Type::Base === primary_key_attribute_type
51
+ send("#{primary_key}=", create_uuid) unless send("#{primary_key}?")
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,12 @@
1
+ require "active_id"
2
+ require "rails"
3
+
4
+ module ActiveID
5
+ class Railtie < Rails::Railtie
6
+ railtie_name :activeid
7
+
8
+ config.to_prepare do
9
+ ActiveID::ConnectionPatches.apply!
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,100 @@
1
+ require "active_record"
2
+
3
+ module ActiveID
4
+ # ActiveRecord's attribute types for serializing UUIDs.
5
+ #
6
+ # ==== Examples
7
+ #
8
+ # class Book
9
+ # attribute :id, ActiveID::Type::BinaryUUID.new
10
+ # end
11
+ #
12
+ # ==== See also
13
+ #
14
+ # * docs for +::ActiveRecord::Attributes::ClassMethods#attribute+
15
+ # * docs for +::ActiveRecord::Type::Value+
16
+ module Type
17
+ # See subclasses.
18
+ #
19
+ # @abstract Subclasses should define at least +instantiate_storage+ method.
20
+ class Base < ::ActiveRecord::Type::Value
21
+ attr_reader :storage_type
22
+
23
+ delegate :cast_to_uuid, to: ActiveID::Utils
24
+ delegate :serialize, :deserialize, to: :storage_type, prefix: :s
25
+
26
+ def initialize
27
+ @storage_type = instantiate_storage
28
+ end
29
+
30
+ # Converts binary values into UUIDs.
31
+ def deserialize(value)
32
+ cast_to_uuid(s_deserialize(value))
33
+ end
34
+
35
+ # Converts strings into UUIDs on user input assignment, called internally
36
+ # from #cast.
37
+ def cast_value(value)
38
+ cast_to_uuid(value)
39
+ end
40
+ end
41
+
42
+ # ActiveRecord's attribute type which serializes UUIDs as binaries. Useful
43
+ # for RDBSes which do not support UUIDs natively (i.e. MySQL, SQLite3).
44
+ #
45
+ # UUIDs serialized as binaries are more space efficient (16 bytes vs
46
+ # 36 characters of their text representation), which may also lead to
47
+ # performance boost if given column is indexed (a bigger piece of index can
48
+ # be kept in memory). The downside is that this representation is less
49
+ # readable for humans who access serialized values outside Rails
50
+ # (i.e. in a database console).
51
+ #
52
+ # ==== Accessing in database console
53
+ #
54
+ # In MySQL (but not in MariaDB), there is
55
+ # a {+BIN_TO_UUID()+}[https://mysqlserverteam.com/mysql-8-0-uuid-support/]
56
+ # function which converts binaries to UUID strings.
57
+ # There is {a feature request}[https://jira.mariadb.org/browse/MDEV-15854]
58
+ # in MariaDB's issue tracker to add a similar feature.
59
+ #
60
+ # ==== Caveat
61
+ #
62
+ # Does not work with PostgreSQL adapter. Nevertheless, there should not be
63
+ # any good reason to use {BinaryUUID} with PostgreSQL. Open a feature
64
+ # request if you find any.
65
+ #
66
+ # In PostgreSQL, {StringUUID} attribute type is recommended as it is
67
+ # compatible with Postgres-specific +UUID+ data type.
68
+ class BinaryUUID < Base
69
+ def serialize(value)
70
+ s_serialize(cast_to_uuid(value)&.raw)
71
+ end
72
+
73
+ protected
74
+
75
+ def instantiate_storage
76
+ ::ActiveRecord::Type::Binary.new
77
+ end
78
+ end
79
+
80
+ # ActiveRecord's attribute type which serializes UUIDs as strings.
81
+ #
82
+ # UUIDs are serialized as 36 characters long strings
83
+ # (`xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`). In PostgreSQL, this
84
+ # representation is compatible with +UUID+ data type, which is a unique
85
+ # feature of this RDBS. In other RDBSes, this attribute type can be
86
+ # used with textual data types (e.g. +VARCHAR(36)+), however {BinaryUUID}
87
+ # should be preferred when performance matters.
88
+ class StringUUID < Base
89
+ def serialize(value)
90
+ s_serialize(cast_to_uuid(value)&.to_s)
91
+ end
92
+
93
+ protected
94
+
95
+ def instantiate_storage
96
+ ::ActiveRecord::Type::String.new
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,77 @@
1
+ require "uuidtools"
2
+
3
+ module ActiveID
4
+ # Variety of convenience functions.
5
+ module Utils
6
+ module_function
7
+
8
+ # Casts +value+ to +UUIDTools::UUID+.
9
+ #
10
+ # When an instance of +UUIDTools::UUID+ or +nil+ is given, then this method
11
+ # returns that argument. When a +String+ is given, then it is parsed,
12
+ # and respective instance of UUIDTools::UUID is returned.
13
+ #
14
+ # A variety of string formats is recognized:
15
+ #
16
+ # - 36 characters long strings (hexadecimal number interpolated with dashes)
17
+ # - 32 characters long strings (hexadecimal number without dashes)
18
+ # - 16 bytes long strings (binary representation of UUID)
19
+ #
20
+ # @param value [UUIDTools::UUID, String, nil] value to be casted
21
+ # to +UUIDTools::UUID+.
22
+ # @raise [ArgumentError] argument cannot be casted to UUIDTools::UUID.
23
+ # @return [UUIDTools::UUID, nil] respective UUID instance or nil.
24
+ def cast_to_uuid(value)
25
+ case value
26
+ when UUIDTools::UUID, nil
27
+ value
28
+ when String
29
+ parse_uuid_string(value)
30
+ else
31
+ m = "UUID, String, or nil required, but '#{value.inspect}' was given"
32
+ raise ArgumentError, m
33
+ end
34
+ end
35
+
36
+ # Casts UUID to binary and quotes it, so that it can be used in SQL query
37
+ # interpolation.
38
+ #
39
+ # model.where("id = ?", ActiveID.quote_as_binary(some_uuid))
40
+ #
41
+ # This method is unable to determine the correct attribute type.
42
+ # It always casts UUIDs to their binary form, which may be unwanted in some
43
+ # contexts, i.e. in case of UUIDs which are meant to be serialized as
44
+ # strings or as Postgres' native +UUID+ data type. Due to this fact,
45
+ # it is generally recommended to avoid SQL query interpolation if possible.
46
+ #
47
+ # @param value [UUIDTools::UUID, String, nil] UUID or its representation
48
+ # to be quoted.
49
+ # @raise [ArgumentError] see cast_to_uuid.
50
+ # @return [::ActiveRecord::Type::Binary::Data, nil] a binary value which
51
+ # can be used in SQL queries.
52
+ def quote_as_binary(value)
53
+ uuid = cast_to_uuid(value)
54
+ uuid && ::ActiveRecord::Type::Binary::Data.new(uuid.raw)
55
+ end
56
+
57
+ # :nodoc:
58
+ # Unfortunately, UUIDTools is missing some validations, hence we have to do
59
+ # them here.
60
+ #
61
+ # TODO More validations, see specs.
62
+ def self.parse_uuid_string(str)
63
+ case str.size
64
+ when 16 then uuid = UUIDTools::UUID.parse_raw(str)
65
+ when 32 then uuid = UUIDTools::UUID.parse_hexdigest(str)
66
+ when 36 then uuid = UUIDTools::UUID.parse(str)
67
+ end
68
+ ensure
69
+ unless uuid # Guard for both exceptions and nil return values
70
+ raise ArgumentError, "Expected string which is 16, 32, or 36 " +
71
+ "characters long, and represents UUID, but #{str.inspect} was given"
72
+ end
73
+ end
74
+
75
+ private_class_method :parse_uuid_string
76
+ end
77
+ end