leifcr-activeuuid 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.hound.yml +3 -0
  4. data/.rspec +4 -0
  5. data/.rubocop.tb.yml +640 -0
  6. data/.rubocop.yml +12 -0
  7. data/.rvmrc +1 -0
  8. data/.travis.yml +52 -0
  9. data/Gemfile +6 -0
  10. data/LICENSE.md +19 -0
  11. data/README.mkd +167 -0
  12. data/Rakefile +15 -0
  13. data/activeuuid.gemspec +40 -0
  14. data/gemfiles/Gemfile.rails-4-0 +6 -0
  15. data/gemfiles/Gemfile.rails-4-1 +6 -0
  16. data/gemfiles/Gemfile.rails-4-2 +5 -0
  17. data/gemfiles/Gemfile.rails-5-0 +5 -0
  18. data/gemfiles/Gemfile.rails-5-1 +5 -0
  19. data/gemfiles/Gemfile.rails-head +5 -0
  20. data/lib/activeuuid/patches.rb +226 -0
  21. data/lib/activeuuid/railtie.rb +12 -0
  22. data/lib/activeuuid/uuid.rb +166 -0
  23. data/lib/activeuuid/version.rb +3 -0
  24. data/lib/activeuuid.rb +10 -0
  25. data/spec/fabricators/article_fabricator.rb +4 -0
  26. data/spec/fabricators/uuid_article_fabricator.rb +4 -0
  27. data/spec/fabricators/uuid_article_with_namespace_fabricator.rb +4 -0
  28. data/spec/fabricators/uuid_article_with_natural_key_fabricator.rb +4 -0
  29. data/spec/lib/activerecord_spec.rb +224 -0
  30. data/spec/lib/uuid_mokeypatch_spec.rb +30 -0
  31. data/spec/spec_helper.rb +61 -0
  32. data/spec/support/database.yml +12 -0
  33. data/spec/support/migrate/20111117081813_create_articles.rb +13 -0
  34. data/spec/support/migrate/20120817081813_create_uuid_articles.rb +15 -0
  35. data/spec/support/migrate/20141017204945_add_array_to_articles.rb +8 -0
  36. data/spec/support/models/article.rb +3 -0
  37. data/spec/support/models/uuid_article.rb +3 -0
  38. data/spec/support/models/uuid_article_with_namespace.rb +6 -0
  39. data/spec/support/models/uuid_article_with_natural_key.rb +5 -0
  40. data/spec/support/spec_for_adapter.rb +20 -0
  41. metadata +250 -0
@@ -0,0 +1,166 @@
1
+ require "uuidtools"
2
+
3
+ # monkey-patch Friendly::UUID to serialize UUIDs
4
+ module UUIDTools
5
+ class UUID
6
+ alias_method :id, :raw
7
+
8
+ # duck typing activerecord 3.1 dirty hack )
9
+ def gsub(*); self; end
10
+
11
+ def ==(another_uuid)
12
+ to_s == another_uuid.to_s
13
+ end
14
+
15
+ def next
16
+ self.class.random_create
17
+ end
18
+
19
+ def quoted_id
20
+ s = raw.unpack("H*")[0]
21
+ "x'#{s}'"
22
+ end
23
+
24
+ def as_json(_options = nil)
25
+ to_s
26
+ end
27
+
28
+ def to_param
29
+ to_s
30
+ end
31
+
32
+ def self.serialize(value)
33
+ case value
34
+ when self
35
+ value
36
+ when String
37
+ parse_string value
38
+ end
39
+ end
40
+
41
+ def bytesize
42
+ 16
43
+ end
44
+
45
+ private
46
+
47
+ def self.parse_string(str)
48
+ return nil if str.empty?
49
+ if str.length == 36
50
+ parse str
51
+ elsif str.length == 32
52
+ parse_hexdigest str
53
+ else
54
+ parse_raw str
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ module Arel
61
+ module Visitors
62
+ class DepthFirst < Arel::Visitors::Visitor
63
+ def visit_UUIDTools_UUID(o, _a = nil)
64
+ o.quoted_id
65
+ end
66
+ end
67
+
68
+ class MySQL < Arel::Visitors::ToSql
69
+ def visit_UUIDTools_UUID(o, _a = nil)
70
+ o.quoted_id
71
+ end
72
+ end
73
+
74
+ class WhereSql < Arel::Visitors::ToSql
75
+ def visit_UUIDTools_UUID(o)
76
+ o.quoted_id
77
+ end
78
+ end
79
+
80
+ class SQLite < Arel::Visitors::ToSql
81
+ def visit_UUIDTools_UUID(o, _a = nil)
82
+ o.quoted_id
83
+ end
84
+ end
85
+
86
+ class PostgreSQL < Arel::Visitors::ToSql
87
+ def visit_UUIDTools_UUID(o, _a = nil)
88
+ "'#{o}'"
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ module ActiveUUID
95
+ module UUID
96
+ extend ActiveSupport::Concern
97
+
98
+ included do
99
+ class_attribute :_natural_key, instance_writer: false
100
+ class_attribute :_uuid_namespace, instance_writer: false
101
+ class_attribute :_uuid_generator, instance_writer: false
102
+ self._uuid_generator = :random
103
+
104
+ singleton_class.prepend Instantiation
105
+ before_create :generate_uuids_if_needed
106
+ end
107
+
108
+ module ClassMethods
109
+ def natural_key(*attributes)
110
+ self._natural_key = attributes
111
+ end
112
+
113
+ def uuid_namespace(namespace)
114
+ namespace = UUIDTools::UUID.parse_string(namespace) unless namespace.is_a? UUIDTools::UUID
115
+ self._uuid_namespace = namespace
116
+ end
117
+
118
+ def uuid_generator(generator_name)
119
+ self._uuid_generator = generator_name
120
+ end
121
+
122
+ def uuids(*_attributes)
123
+ ActiveSupport::Deprecation.warn <<-EOS
124
+ ActiveUUID detects uuid columns independently.
125
+ There is no more need to use uuid method.
126
+ EOS
127
+ end
128
+
129
+ def uuid_columns
130
+ @uuid_columns ||= columns.select { |c| c.type == :uuid }.map(&:name)
131
+ end
132
+ end
133
+
134
+ module Instantiation
135
+ def instantiate(record, _record_models = nil)
136
+ uuid_columns.each do |uuid_column|
137
+ record[uuid_column] = UUIDTools::UUID.serialize(record[uuid_column]).to_s if record[uuid_column]
138
+ end
139
+
140
+ super(record)
141
+ end
142
+ end
143
+
144
+ def create_uuid
145
+ if _natural_key
146
+ # TODO if all the attributes return nil you might want to warn about this
147
+ chained = _natural_key.map { |attribute| send(attribute) }.join("-")
148
+ UUIDTools::UUID.sha1_create(_uuid_namespace || UUIDTools::UUID_OID_NAMESPACE, chained)
149
+ else
150
+ case _uuid_generator
151
+ when :random
152
+ UUIDTools::UUID.random_create
153
+ when :time
154
+ UUIDTools::UUID.timestamp_create
155
+ end
156
+ end
157
+ end
158
+
159
+ def generate_uuids_if_needed
160
+ primary_key = self.class.primary_key
161
+ if self.class.columns_hash[primary_key].type == :uuid
162
+ send("#{primary_key}=", create_uuid) unless send("#{primary_key}?")
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,3 @@
1
+ module Activeuuid
2
+ VERSION = "0.6.1".freeze
3
+ end
data/lib/activeuuid.rb ADDED
@@ -0,0 +1,10 @@
1
+ require "activeuuid/version"
2
+ require "activeuuid/patches"
3
+ require "activeuuid/uuid"
4
+ require "activeuuid/railtie" if defined?(Rails::Railtie)
5
+ require "pp"
6
+
7
+ module ActiveUUID
8
+ end
9
+
10
+ ActiveUUID::Patches.apply!
@@ -0,0 +1,4 @@
1
+ Fabricator(:article) do
2
+ title { Forgery::LoremIpsum.word }
3
+ body { Forgery::LoremIpsum.sentence }
4
+ end
@@ -0,0 +1,4 @@
1
+ Fabricator(:uuid_article) do
2
+ title { Forgery::LoremIpsum.word }
3
+ body { Forgery::LoremIpsum.sentence }
4
+ end
@@ -0,0 +1,4 @@
1
+ Fabricator(:uuid_article_with_namespace) do
2
+ title { Forgery::LoremIpsum.word }
3
+ body { Forgery::LoremIpsum.sentence }
4
+ end
@@ -0,0 +1,4 @@
1
+ Fabricator(:uuid_article_with_natural_key) do
2
+ title { Forgery::LoremIpsum.word }
3
+ body { Forgery::LoremIpsum.sentence }
4
+ end
@@ -0,0 +1,224 @@
1
+ require "spec_helper"
2
+
3
+ describe ActiveRecord::Base do
4
+ context ".connection" do
5
+ def table_exists?(connection, table_name)
6
+ connection.respond_to?(:data_source_exists?) ?
7
+ connection.data_source_exists?(table_name) :
8
+ connection.table_exists?(table_name)
9
+ end
10
+
11
+ let!(:connection) { ActiveRecord::Base.connection }
12
+ let(:table_name) { :test_uuid_field_creation }
13
+
14
+ before do
15
+ connection.drop_table(table_name) if table_exists?(connection, table_name)
16
+ connection.create_table(table_name)
17
+ end
18
+
19
+ after do
20
+ connection.drop_table table_name
21
+ end
22
+
23
+ specify { expect(table_exists?(connection, table_name)).to be_truthy }
24
+
25
+ context "#add_column" do
26
+ let(:column_name) { :uuid_column }
27
+ let(:column) { connection.columns(table_name).detect { |c| c.name.to_sym == column_name } }
28
+
29
+ before { connection.add_column table_name, column_name, :uuid }
30
+
31
+ specify { expect(connection.column_exists?(table_name, column_name)).to be_truthy }
32
+ specify { expect(column).not_to be_nil }
33
+
34
+ it "should have proper sql type" do
35
+ spec_for_adapter do |adapters|
36
+ adapters.sqlite3 { expect(column.sql_type).to eq("binary(16)") }
37
+ adapters.mysql2 { expect(column.sql_type).to eq("binary(16)") }
38
+ adapters.postgresql { expect(column.sql_type).to eq("uuid") }
39
+ end
40
+ end
41
+ end
42
+
43
+ context "#change_column" do
44
+ let(:column_name) { :string_col }
45
+ let(:column) { connection.columns(table_name).detect { |c| c.name.to_sym == column_name } }
46
+
47
+ before do
48
+ connection.add_column table_name, column_name, :string
49
+ spec_for_adapter do |adapters|
50
+ adapters.sqlite3 { connection.change_column table_name, column_name, :uuid }
51
+ adapters.mysql2 { connection.change_column table_name, column_name, :uuid }
52
+ end
53
+ end
54
+
55
+ it "support changing type from string to uuid" do
56
+ spec_for_adapter do |adapters|
57
+ adapters.sqlite3 { expect(column.sql_type).to eq("binary(16)") }
58
+ adapters.mysql2 { expect(column.sql_type).to eq("binary(16)") }
59
+ adapters.postgresql { skip("postgresql can`t change column type to uuid") }
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ describe Article do
67
+ let!(:article) { Fabricate :article }
68
+ let(:id) { article.id }
69
+ let(:model) { Article }
70
+ subject { model }
71
+
72
+ context "model" do
73
+ its(:all) { should == [article] }
74
+ its(:first) { should == article }
75
+ end
76
+
77
+ context "existance" do
78
+ subject { article }
79
+ its(:id) { should be_a Integer }
80
+ end
81
+
82
+ context ".find" do
83
+ specify { expect(model.find(id)).to eq(article) }
84
+ end
85
+
86
+ context ".where" do
87
+ specify { expect(model.where(id: id).first).to eq(article) }
88
+ end
89
+
90
+ context "#destroy" do
91
+ subject { article }
92
+ its(:delete) { should be_truthy }
93
+ its(:destroy) { should be_truthy }
94
+ end
95
+
96
+ context "#save" do
97
+ subject { article }
98
+ let(:array) { [1, 2, 3] }
99
+
100
+ its(:save) { should be_truthy }
101
+
102
+ context "when change array field" do
103
+ before { article.some_array = array }
104
+ its(:save) { should be_truthy }
105
+ end
106
+ end
107
+ end
108
+
109
+ describe UuidArticle do
110
+ let!(:article) { Fabricate :uuid_article }
111
+ let!(:id) { article.id }
112
+ let(:model) { UuidArticle }
113
+ subject { model }
114
+
115
+ context "model" do
116
+ its(:primary_key) { should == "id" }
117
+ its(:all) { should == [article] }
118
+ its(:first) { should == article }
119
+ end
120
+
121
+ context "existance" do
122
+ subject { article }
123
+ its(:id) { should be_a UUIDTools::UUID }
124
+ end
125
+
126
+ context "interpolation" do
127
+ specify { model.where("id = :id", id: article.id) }
128
+ end
129
+
130
+ context "batch interpolation" do
131
+ before { model.update_all(["title = CASE WHEN id = :id THEN 'Passed' ELSE 'Nothing' END", id: article.id]) }
132
+ specify { expect(article.reload.title).to eq("Passed") }
133
+ end
134
+
135
+ context ".find" do
136
+ specify { expect(model.find(article.id)).to eq(article) }
137
+ specify { expect(model.find(id)).to eq(article) }
138
+ specify { expect(model.find(id.to_s)).to eq(article) }
139
+ specify { expect(model.find(id.raw)).to eq(article) }
140
+ end
141
+
142
+ context ".where" do
143
+ specify { expect(model.where(id: article).first).to eq(article) }
144
+ specify { expect(model.where(id: id).first).to eq(article) }
145
+ specify { expect(model.where(id: id.to_s).first).to eq(article) }
146
+ specify { expect(model.where(id: id.raw).first).to eq(article) }
147
+ end
148
+
149
+ context "#destroy" do
150
+ subject { article }
151
+ its(:delete) { should be_truthy }
152
+ its(:destroy) { should be_truthy }
153
+ end
154
+
155
+ context "#reload" do
156
+ subject { article }
157
+ its(:'reload.id') { should == id }
158
+ specify { expect(subject.reload(select: :another_uuid).id).to eq(id) }
159
+ end
160
+
161
+ context "columns" do
162
+ %i[id another_uuid].each do |column|
163
+ context column do
164
+ subject { model.columns_hash[column.to_s] }
165
+ its(:type) { should == :uuid }
166
+ end
167
+ end
168
+ end
169
+
170
+ context "typecasting" do
171
+ let(:uuid) { UUIDTools::UUID.random_create }
172
+ let(:string) { uuid.to_s }
173
+ context "primary" do
174
+ before { article.id = string }
175
+ specify do
176
+ expect(article.id).to eq(uuid)
177
+ expect(article.id_before_type_cast).to eq(string)
178
+ end
179
+ specify do
180
+ expect(article.id_before_type_cast).to eq(string)
181
+ expect(article.id).to eq(uuid)
182
+ end
183
+ end
184
+
185
+ context "non-primary" do
186
+ before { article.another_uuid = string }
187
+ specify do
188
+ expect(article.another_uuid).to eq(uuid)
189
+ expect(article.another_uuid_before_type_cast).to eq(string)
190
+ end
191
+ specify do
192
+ expect(article.another_uuid_before_type_cast).to eq(string)
193
+ expect(article.another_uuid).to eq(uuid)
194
+ end
195
+ specify do
196
+ article.save
197
+ article.reload
198
+ expect(article.another_uuid_before_type_cast).to eq(string)
199
+ expect(article.another_uuid).to eq(uuid)
200
+ end
201
+ end
202
+ end
203
+ end
204
+
205
+ describe UuidArticleWithNaturalKey do
206
+ let!(:article) { Fabricate :uuid_article_with_natural_key }
207
+ let!(:id) { article.id }
208
+ let!(:uuid) { UUIDTools::UUID.sha1_create(UUIDTools::UUID_OID_NAMESPACE, article.title) }
209
+ subject { article }
210
+ context "natural_key" do
211
+ its(:id) { should == uuid }
212
+ end
213
+ end
214
+
215
+ describe UuidArticleWithNamespace do
216
+ let!(:article) { Fabricate :uuid_article_with_namespace }
217
+ let!(:id) { article.id }
218
+ let!(:namespace) { UuidArticleWithNamespace._uuid_namespace }
219
+ let!(:uuid) { UUIDTools::UUID.sha1_create(namespace, article.title) }
220
+ subject { article }
221
+ context "natural_key_with_namespace" do
222
+ its(:id) { should == uuid }
223
+ end
224
+ end
@@ -0,0 +1,30 @@
1
+ require "spec_helper"
2
+
3
+ describe UUIDTools::UUID do
4
+ let(:input) { "e4618518-cb9f-11e1-aa7c-14dae903e06a" }
5
+ let(:hex) { "E4618518CB9F11E1AA7C14DAE903E06A" }
6
+ let(:uuid) { described_class.parse input }
7
+
8
+ context "instance methods" do
9
+ subject { uuid }
10
+ let(:sql_out) { "x'e4618518cb9f11e1aa7c14dae903e06a'" }
11
+
12
+ its(:quoted_id) { should == sql_out }
13
+ its(:as_json) { should == uuid.to_s }
14
+ its(:to_param) { should == uuid.to_s }
15
+ its(:next) { should be_a(described_class) }
16
+ end
17
+
18
+ describe ".serialize" do
19
+ subject { described_class }
20
+ let(:raw) { uuid.raw }
21
+
22
+ specify { expect(subject.serialize(uuid)).to eq(uuid) }
23
+ specify { expect(subject.serialize(input)).to eq(uuid) }
24
+ specify { expect(subject.serialize(hex)).to eq(uuid) }
25
+ specify { expect(subject.serialize(raw)).to eq(uuid) }
26
+ specify { expect(subject.serialize(nil)).to be_nil }
27
+ specify { expect(subject.serialize("")).to be_nil }
28
+ specify { expect(subject.serialize(5)).to be_nil }
29
+ end
30
+ end
@@ -0,0 +1,61 @@
1
+ require "rubygems"
2
+ require "bundler/setup"
3
+
4
+ Bundler.require :development
5
+
6
+ require "active_record"
7
+ require "active_support/all"
8
+
9
+ ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
10
+ ActiveRecord::Base.configurations = YAML::safe_load(File.read(File.dirname(__FILE__) + "/support/database.yml"))
11
+
12
+ require "activeuuid"
13
+
14
+ ActiveRecord::Base.establish_connection((ENV["DB"] || "sqlite3").to_sym)
15
+
16
+ if ENV["DB"] == "mysql"
17
+ if ActiveRecord::VERSION::MAJOR == 4 && ActiveRecord::VERSION::MINOR <= 1
18
+ class ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter
19
+ NATIVE_DATABASE_TYPES[:primary_key] = "int(11) auto_increment PRIMARY KEY"
20
+ end
21
+ elsif ActiveRecord::VERSION::MAJOR == 3
22
+ class ActiveRecord::ConnectionAdapters::Mysql2Adapter
23
+ NATIVE_DATABASE_TYPES[:primary_key] = "int(11) auto_increment PRIMARY KEY"
24
+ end
25
+ end
26
+ end
27
+
28
+ ActiveRecord::Migrator.migrate(File.dirname(__FILE__) + "/support/migrate")
29
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, STDOUT)
30
+
31
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
32
+ Dir["#{File.dirname(__FILE__)}/fabricators/**/*.rb"].each { |f| require f }
33
+
34
+ RSpec.configure do |config|
35
+ # Remove this line if you don't want RSpec's should and should_not
36
+ # methods or matchers
37
+ require "rspec/expectations"
38
+ config.include RSpec::Matchers
39
+
40
+ # == Mock Framework
41
+ config.mock_with :rspec
42
+
43
+ config.before(:suite) do
44
+ DatabaseCleaner.clean_with(:truncation)
45
+ DatabaseCleaner.strategy = :transaction
46
+ end
47
+
48
+ config.before(:each) do
49
+ DatabaseCleaner.start
50
+ end
51
+
52
+ config.after(:each) do
53
+ DatabaseCleaner.clean
54
+ end
55
+
56
+ def spec_for_adapter
57
+ switcher = ActiveUUID::SpecSupport::SpecForAdapter.new
58
+ yield switcher
59
+ switcher.run(connection)
60
+ end
61
+ end
@@ -0,0 +1,12 @@
1
+ sqlite3:
2
+ adapter: sqlite3
3
+ database: ":memory:"
4
+ postgresql:
5
+ adapter: postgresql
6
+ host: localhost
7
+ database: activeuuid_test
8
+ mysql:
9
+ adapter: mysql2
10
+ username: root
11
+ database: activeuuid_test
12
+ encoding: utf8
@@ -0,0 +1,13 @@
1
+ parent_class = ActiveRecord::Migration.respond_to?(:[]) ?
2
+ ActiveRecord::Migration[4.2] :
3
+ ActiveRecord::Migration
4
+ class CreateArticles < parent_class
5
+ def change
6
+ create_table :articles do |t|
7
+ t.string :title
8
+ t.text :body
9
+
10
+ t.timestamps
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ parent_class = ActiveRecord::Migration.respond_to?(:[]) ?
2
+ ActiveRecord::Migration[4.2] :
3
+ ActiveRecord::Migration
4
+ class CreateUuidArticles < parent_class
5
+ def change
6
+ create_table :uuid_articles, id: false do |t|
7
+ t.uuid :id, primary_key: true
8
+ t.string :title
9
+ t.text :body
10
+ t.uuid :another_uuid
11
+
12
+ t.timestamps
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,8 @@
1
+ parent_class = ActiveRecord::Migration.respond_to?(:[]) ?
2
+ ActiveRecord::Migration[4.2] :
3
+ ActiveRecord::Migration
4
+ class AddArrayToArticles < parent_class
5
+ def change
6
+ add_column :articles, :some_array, :integer, array: true
7
+ end
8
+ end
@@ -0,0 +1,3 @@
1
+ class Article < ActiveRecord::Base
2
+
3
+ end
@@ -0,0 +1,3 @@
1
+ class UuidArticle < ActiveRecord::Base
2
+ include ActiveUUID::UUID
3
+ end
@@ -0,0 +1,6 @@
1
+ class UuidArticleWithNamespace < ActiveRecord::Base
2
+ include ActiveUUID::UUID
3
+ self.table_name = "uuid_articles"
4
+ natural_key :title
5
+ uuid_namespace "45e676ea-8a43-4ffe-98ca-c142b0062a83" # a random UUID
6
+ end
@@ -0,0 +1,5 @@
1
+ class UuidArticleWithNaturalKey < ActiveRecord::Base
2
+ include ActiveUUID::UUID
3
+ self.table_name = "uuid_articles"
4
+ natural_key :title
5
+ end
@@ -0,0 +1,20 @@
1
+ require "activeuuid"
2
+
3
+ module ActiveUUID::SpecSupport
4
+ class SpecForAdapter
5
+ def initialize
6
+ @specs = {}
7
+ end
8
+
9
+ %i[sqlite3 mysql2 postgresql].each do |name|
10
+ send :define_method, name do |&block|
11
+ @specs[name] = block
12
+ end
13
+ end
14
+
15
+ def run(connection)
16
+ name = connection.adapter_name.downcase.to_sym
17
+ @specs[name].call if @specs.include? name
18
+ end
19
+ end
20
+ end