enum_kit 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +46 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +21 -0
  5. data/.travis.yml +13 -0
  6. data/Gemfile +5 -0
  7. data/Guardfile +17 -0
  8. data/LICENSE +21 -0
  9. data/README.md +82 -0
  10. data/Rakefile +8 -0
  11. data/bin/console +7 -0
  12. data/bin/setup +6 -0
  13. data/config.ru +9 -0
  14. data/enum_kit.gemspec +35 -0
  15. data/lib/enum_kit/active_record_extensions/connection_adapters/postgresql/column_dumper.rb +29 -0
  16. data/lib/enum_kit/active_record_extensions/connection_adapters/postgresql/schema_dumper.rb +29 -0
  17. data/lib/enum_kit/active_record_extensions/connection_adapters/postgresql_adapter.rb +74 -0
  18. data/lib/enum_kit/active_record_extensions/migration/command_recorder.rb +45 -0
  19. data/lib/enum_kit/active_record_extensions/schema_dumper.rb +31 -0
  20. data/lib/enum_kit/active_record_patches/connection_adapters/postgresql/column_methods.rb +32 -0
  21. data/lib/enum_kit/active_record_patches/connection_adapters/postgresql/oid/enum.rb +32 -0
  22. data/lib/enum_kit/active_record_patches/connection_adapters/postgresql/oid/type_map_initializer.rb +27 -0
  23. data/lib/enum_kit/active_record_patches/enum/enum_type.rb +25 -0
  24. data/lib/enum_kit/active_record_patches/enum.rb +39 -0
  25. data/lib/enum_kit/active_record_patches/validations/pg_enum_validator.rb +31 -0
  26. data/lib/enum_kit/constants.rb +7 -0
  27. data/lib/enum_kit/helpers.rb +84 -0
  28. data/lib/enum_kit.rb +62 -0
  29. data/spec/active_record/base_spec.rb +15 -0
  30. data/spec/active_record/connection_adapters/postgresql_adapter_spec.rb +62 -0
  31. data/spec/active_record/validations/pg_enum_validator_spec.rb +19 -0
  32. data/spec/enum_kit/constants_spec.rb +11 -0
  33. data/spec/enum_kit/helpers_spec.rb +106 -0
  34. data/spec/internal/app/models/shirt.rb +7 -0
  35. data/spec/internal/config/database.yml +3 -0
  36. data/spec/internal/db/schema.rb +10 -0
  37. data/spec/internal/log/.gitignore +1 -0
  38. data/spec/spec_helper.rb +96 -0
  39. metadata +231 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0edccc6b488902ea7344f63e24014979d60f2a315c79335fc959c7cfeafb1473
4
+ data.tar.gz: 2776e409ad4108dfaf9b504d6cdb3d43cafe3368e9889d05f27635f8240c33d6
5
+ SHA512:
6
+ metadata.gz: 7f41dc0fd5399088b5d5cdc02fb2bd48fd0a1c3ff3b4fdb44d53920d9c6d4b12ece952c2d6ee9c3174423bd79d3f96b7babd1e7a107315acb35ea58a6aa8cda5
7
+ data.tar.gz: c43cd3f3a197c828d958de2f5374d6a8d98d1ff1d6f381ec8d1265753d081682cca917af8cd220317ca0014313812102bfe6cde7e999913ab161fac480002395
data/.gitignore ADDED
@@ -0,0 +1,46 @@
1
+ # Packages #
2
+ ############
3
+ *.7z
4
+ *.dmg
5
+ *.gz
6
+ *.iso
7
+ *.jar
8
+ *.rar
9
+ *.tar
10
+ *.zip
11
+
12
+ # Logs #
13
+ ########
14
+ *.log
15
+
16
+ # Databases #
17
+ #############
18
+ *.sql
19
+ *.sqlite
20
+
21
+ # OS Files #
22
+ ############
23
+ .DS_Store
24
+ .Trashes
25
+ ehthumbs.db
26
+ Icon?
27
+ Thumbs.db
28
+
29
+ # Vagrant #
30
+ ###########
31
+ .vagrant
32
+
33
+ # Ruby Files #
34
+ ##############
35
+ /.bundle/
36
+ /.yardoc
37
+ /Gemfile.lock
38
+ /_yardoc/
39
+ /coverage/
40
+ /doc/
41
+ /pkg/
42
+ /spec/reports/
43
+ /spec/examples.txt
44
+ /tmp/
45
+ /vendor/bundle/
46
+ *.gem
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,21 @@
1
+ AllCops:
2
+ Exclude:
3
+ - 'bin/**/*'
4
+
5
+ Metrics/AbcSize:
6
+ Max: 30
7
+
8
+ Metrics/LineLength:
9
+ Max: 120
10
+
11
+ Metrics/BlockLength:
12
+ Enabled: false
13
+
14
+ Metrics/ClassLength:
15
+ Enabled: false
16
+
17
+ Metrics/MethodLength:
18
+ Enabled: false
19
+
20
+ Style/Documentation:
21
+ Enabled: false
data/.travis.yml ADDED
@@ -0,0 +1,13 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.6.5
4
+ services:
5
+ - postgresql
6
+ before_install:
7
+ - gem update --system
8
+ - gem install bundler
9
+ before_script:
10
+ - psql -c 'CREATE DATABASE enum_kit_test;' -U postgres
11
+ env:
12
+ global:
13
+ - DATABASE_URL="postgresql://127.0.0.1:5432/enum_kit_test"
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ guard :rspec, cmd: 'bundle exec rspec' do
4
+ require 'guard/rspec/dsl'
5
+
6
+ dsl = Guard::RSpec::Dsl.new(self)
7
+
8
+ # RSpec files
9
+ rspec = dsl.rspec
10
+ watch(rspec.spec_helper) { rspec.spec_dir }
11
+ watch(rspec.spec_support) { rspec.spec_dir }
12
+ watch(rspec.spec_files)
13
+
14
+ # Ruby files
15
+ ruby = dsl.ruby
16
+ dsl.watch_spec_files_for(ruby.lib_files)
17
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Nialto Services
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # EnumKit
2
+
3
+ EnumKit provides native support for PostgreSQL enums in Ruby on Rails projects.
4
+
5
+ ## Installation
6
+
7
+ You can install **EnumKit** using the following command:
8
+
9
+ $ gem install enum_kit
10
+
11
+ Or, by adding the following to your `Gemfile`:
12
+
13
+ ```ruby
14
+ gem 'enum_kit', '~> 0.1'
15
+ ```
16
+
17
+ ### Usage
18
+
19
+ Here's a sample migration file which creates the enum `:shirt_size`, then adds the column `:size` to the `:shirts`
20
+ table using the `:shirt_size` enum as the underlying type:
21
+
22
+ ```ruby
23
+ class CreateShirts < ActiveRecord::Migration[6.0]
24
+ def change
25
+ create_enum :shirt_size, %i[small medium large]
26
+
27
+ create_table :shirts do |t|
28
+ t.string :name
29
+ t.enum :size, name: :shirt_size
30
+
31
+ t.timestamps
32
+ end
33
+ end
34
+ end
35
+ ```
36
+
37
+ You can remove the enum later using something similar to this:
38
+
39
+ ```ruby
40
+ class DropShirts < ActiveRecord::Migration[6.0]
41
+ def change
42
+ drop_table :shirts
43
+ drop_enum :shirt_size
44
+ end
45
+ end
46
+ ```
47
+
48
+ Once you've defined an enum in a migration file, you can use it in the associated model:
49
+
50
+ ```ruby
51
+ class Shirt < ActiveRecord::Base
52
+ pg_enum :size
53
+ end
54
+ ```
55
+
56
+ Note that you don't need to define the enum's cases again.
57
+ The `pg_enum` method automatically queries the database when Rails boots for the acceptable values!
58
+
59
+ ---
60
+
61
+ When setting the enum to an unsupported value, an exception is raised. This can be problematic in cases where you don't
62
+ have control over the input (such as when using APIs).
63
+
64
+ To improve this, you can optionally specify that exceptions should not be raised on a per enum basis. Note that when
65
+ opting for this feature, you'd ideally specify a validation to capture any unsupported values:
66
+
67
+ ```ruby
68
+ class Shirt < ActiveRecord::Base
69
+ pg_enum :size, exceptions: false
70
+
71
+ validates :size, pg_enum: true
72
+ end
73
+ ```
74
+
75
+ The above prevents exceptions from being raised and checks that the assigned value is one of the cases supported by the
76
+ enum.
77
+
78
+ ## Development
79
+
80
+ After checking out the repo, run `bundle exec rake spec` to run the tests.
81
+
82
+ To install this gem onto your machine, run `bundle exec rake install`.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'enum_kit'
5
+ require 'irb'
6
+
7
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
data/config.ru ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems"
4
+ require "bundler"
5
+
6
+ Bundler.require :default, :development
7
+
8
+ Combustion.initialize! :all
9
+ run Combustion::Application
data/enum_kit.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require 'enum_kit/constants'
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = 'enum_kit'
10
+ spec.version = EnumKit::VERSION
11
+ spec.authors = ['Nialto Services']
12
+ spec.email = ['support@nialtoservices.co.uk']
13
+
14
+ spec.summary = 'Native PostgreSQL enum support for Ruby on Rails.'
15
+ spec.homepage = 'https://github.com/nialtoservices/enum_kit'
16
+ spec.license = 'MIT'
17
+
18
+ spec.files = `git ls-files -z`.split("\x0")
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.metadata['yard.run'] = 'yri'
23
+
24
+ spec.add_runtime_dependency 'activerecord', '>= 4.0.0'
25
+ spec.add_runtime_dependency 'activesupport', '>= 4.0.0'
26
+ spec.add_runtime_dependency 'pg'
27
+
28
+ spec.add_development_dependency 'bundler', '~> 2.0'
29
+ spec.add_development_dependency 'combustion', '~> 1.1'
30
+ spec.add_development_dependency 'guard-rspec', '~> 4.7'
31
+ spec.add_development_dependency 'rake', '~> 13.0'
32
+ spec.add_development_dependency 'rspec', '~> 3.8'
33
+ spec.add_development_dependency 'rspec-rails', '~> 3.8'
34
+ spec.add_development_dependency 'yard', '~> 0.9.20'
35
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nodoc:
4
+ #
5
+ module EnumKit
6
+ # :nodoc:
7
+ #
8
+ module ActiveRecordExtensions
9
+ # :nodoc:
10
+ #
11
+ module ConnectionAdapters
12
+ # :nodoc:
13
+ #
14
+ module PostgreSQL
15
+ # :nodoc:
16
+ #
17
+ module ColumnDumper
18
+ # :nodoc:
19
+ #
20
+ def prepare_column_options(column)
21
+ spec = super
22
+ spec[:name] = column.sql_type.inspect if column.type == :enum
23
+ spec
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nodoc:
4
+ #
5
+ module EnumKit
6
+ # :nodoc:
7
+ #
8
+ module ActiveRecordExtensions
9
+ # :nodoc:
10
+ #
11
+ module ConnectionAdapters
12
+ # :nodoc:
13
+ #
14
+ module PostgreSQL
15
+ # :nodoc:
16
+ #
17
+ module SchemaDumper
18
+ # :nodoc:
19
+ #
20
+ def prepare_column_options(column)
21
+ spec = super
22
+ spec[:name] = column.sql_type.inspect if column.type == :enum
23
+ spec
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nodoc:
4
+ #
5
+ module EnumKit
6
+ # :nodoc:
7
+ #
8
+ module ActiveRecordExtensions
9
+ # :nodoc:
10
+ #
11
+ module ConnectionAdapters
12
+ # :nodoc:
13
+ #
14
+ module PostgreSQLAdapter
15
+ # @return [String] An SQL query that returns all available enum types in the database.
16
+ #
17
+ ENUM_QUERY = <<~SQL
18
+ SELECT
19
+ pg_type.OID,
20
+ pg_type.typname,
21
+ pg_type.typtype,
22
+ array_to_string(array_agg(pg_enum.enumlabel ORDER BY pg_enum.enumsortorder), '\t\t', '') as values
23
+ FROM pg_type
24
+ LEFT JOIN pg_enum ON pg_enum.enumtypid = pg_type.oid
25
+ WHERE pg_type.typtype = 'e'
26
+ GROUP BY pg_type.OID, pg_type.typname, pg_type.typtype
27
+ ORDER BY pg_type.typname
28
+ SQL
29
+
30
+ # @return [Hash] The enum types available in the database.
31
+ #
32
+ def enums
33
+ @enums ||= select_all(ENUM_QUERY.tr("\n", ' ').strip).each_with_object({}) do |row, enums|
34
+ enums[row['typname'].to_sym] = row['values'].split("\t\t")
35
+ end
36
+ end
37
+
38
+ # Create a new enum type in the database.
39
+ #
40
+ # @param name [Symbol] The name of the new enum type.
41
+ # @param values [Array] The enum's acceptable values.
42
+ #
43
+ def create_enum(name, values)
44
+ name = EnumKit.sanitize_name!(name)
45
+ values = EnumKit.sanitize_values!(values)
46
+
47
+ execute "CREATE TYPE #{name} AS ENUM #{EnumKit.sqlize(values)}"
48
+ end
49
+
50
+ # Drop an existing enum type from the database.
51
+ #
52
+ # @param name [Symbol] The name of the existing enum type.
53
+ #
54
+ def drop_enum(name)
55
+ execute "DROP TYPE #{name}"
56
+ end
57
+
58
+ # :nodoc:
59
+ #
60
+ def migration_keys
61
+ super + [:name]
62
+ end
63
+
64
+ # :nodoc:
65
+ #
66
+ def prepare_column_options(column, types)
67
+ spec = super(column, types)
68
+ spec[:name] = column.cast_type.type.inspect if column.type == :enum
69
+ spec
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nodoc:
4
+ #
5
+ module EnumKit
6
+ # :nodoc:
7
+ #
8
+ module ActiveRecordExtensions
9
+ # :nodoc:
10
+ #
11
+ module Migration
12
+ # :nodoc:
13
+ #
14
+ module CommandRecorder
15
+ # :nodoc:
16
+ #
17
+ def create_enum(*args)
18
+ record(:create_enum, args)
19
+ end
20
+
21
+ # :nodoc:
22
+ #
23
+ def drop_enum(*args)
24
+ record(:drop_enum, args)
25
+ end
26
+
27
+ # :nodoc:
28
+ #
29
+ def invert_create_enum(args)
30
+ record(:drop_enum, args.first)
31
+ end
32
+
33
+ # :nodoc:
34
+ #
35
+ def invert_drop_enum(args)
36
+ unless args.length > 1
37
+ raise ::ActiveRecord::IrreversibleMigration, 'drop_enum is only reversible if given an Array of values.'
38
+ end
39
+
40
+ record(:create_enum, args)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nodoc:
4
+ #
5
+ module EnumKit
6
+ # :nodoc:
7
+ #
8
+ module ActiveRecordExtensions
9
+ # :nodoc:
10
+ #
11
+ module SchemaDumper
12
+ # :nodoc:
13
+ #
14
+ def tables(stream)
15
+ export_enums(stream)
16
+ super
17
+ end
18
+
19
+ # :nodoc:
20
+ #
21
+ def export_enums(stream)
22
+ @connection.enums.each do |name, values|
23
+ values = values.map(&:inspect).join(', ')
24
+ stream.puts " create_enum #{name.inspect}, [#{values}]"
25
+ end
26
+
27
+ stream.puts if @connection.enums.any?
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nodoc:
4
+ #
5
+ module ActiveRecord
6
+ # :nodoc:
7
+ #
8
+ module ConnectionAdapters
9
+ # :nodoc:
10
+ #
11
+ module PostgreSQL
12
+ # :nodoc:
13
+ #
14
+ module ColumnMethods
15
+ # Create an enum column with the provided name.
16
+ #
17
+ # By default, the enum type will match the name of the column.
18
+ # You can change this behaviour by providing the enum type as an option under the `:name` key.
19
+ #
20
+ # @example Creating a user role.
21
+ # t.enum :role, name: :user_role
22
+ #
23
+ # @param name [String] The name of the enum column.
24
+ # @param options [Hash] The options (including the name of the enum type).
25
+ #
26
+ def enum(name, options = {})
27
+ column(name, options[:name] || name, options.except(:name))
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nodoc:
4
+ #
5
+ module ActiveRecord
6
+ # :nodoc:
7
+ #
8
+ module ConnectionAdapters
9
+ # :nodoc:
10
+ #
11
+ module PostgreSQL
12
+ # :nodoc:
13
+ #
14
+ module OID
15
+ # :nodoc:
16
+ #
17
+ class Enum < Type::Value
18
+ # @return [String] The PostgreSQL type for the enum.
19
+ #
20
+ attr_reader :name
21
+
22
+ # :nodoc:
23
+ #
24
+ def initialize(options = {})
25
+ @name = options.delete(:name).to_sym
26
+ super
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nodoc:
4
+ #
5
+ module ActiveRecord
6
+ # :nodoc:
7
+ #
8
+ module ConnectionAdapters
9
+ # :nodoc:
10
+ #
11
+ module PostgreSQL
12
+ # :nodoc:
13
+ #
14
+ module OID
15
+ # :nodoc:
16
+ #
17
+ class TypeMapInitializer
18
+ # :nodoc:
19
+ #
20
+ def register_enum_type(row)
21
+ register row['oid'], ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Enum.new(name: row['typname'])
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nodoc:
4
+ #
5
+ module ActiveRecord
6
+ # :nodoc:
7
+ #
8
+ module Enum
9
+ # :nodoc:
10
+ #
11
+ class EnumType < Type::Value
12
+ # @return [Boolean] Whether to prevent an exception from being raised when the enum is set to an invalid value.
13
+ #
14
+ attr_accessor :disable_exceptions
15
+
16
+ # :nodoc:
17
+ #
18
+ def assert_valid_value(value)
19
+ return value if value.blank? || mapping.key?(value) || mapping.value?(value) || disable_exceptions
20
+
21
+ raise ArgumentError, "'#{value}' is not a valid #{name}"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nodoc:
4
+ #
5
+ module ActiveRecord
6
+ # :nodoc:
7
+ #
8
+ module Enum
9
+ # Retrieve the acceptable values for the enum type associated with the given column.
10
+ #
11
+ # @param [String, Symbol] The name of an enum column.
12
+ # @return [Array] The acceptable values for the enum type associated with the column.
13
+ #
14
+ def pg_enum_values(name)
15
+ # Determine the PostgreSQL type name for the enum.
16
+ type = type_for_attribute(name)
17
+ type = type.instance_eval { subtype } if type.is_a?(ActiveRecord::Enum::EnumType)
18
+
19
+ # Query the PostgreSQL database for the enum's acceptable values.
20
+ connection.enums[type.name]
21
+ end
22
+
23
+ # Define a PostgreSQL enum type.
24
+ #
25
+ # @param name [String] The name of an enum column.
26
+ # @param options [Hash] The options.
27
+ #
28
+ def pg_enum(name, options = {})
29
+ values = pg_enum_values(name).map { |value| [value.to_sym, value.to_s] }
30
+
31
+ enum(name => Hash[values])
32
+
33
+ enum = type_for_attribute(name)
34
+ enum.disable_exceptions = options.key?(:exceptions) && !options[:exceptions]
35
+
36
+ nil
37
+ end
38
+ end
39
+ end