pakyow-data 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +0 -0
  3. data/LICENSE +4 -0
  4. data/README.md +29 -0
  5. data/lib/pakyow/data/adapters/abstract.rb +58 -0
  6. data/lib/pakyow/data/adapters/sql/commands.rb +58 -0
  7. data/lib/pakyow/data/adapters/sql/dataset_methods.rb +29 -0
  8. data/lib/pakyow/data/adapters/sql/differ.rb +76 -0
  9. data/lib/pakyow/data/adapters/sql/migrator/adapter_methods.rb +95 -0
  10. data/lib/pakyow/data/adapters/sql/migrator.rb +181 -0
  11. data/lib/pakyow/data/adapters/sql/migrators/automator.rb +49 -0
  12. data/lib/pakyow/data/adapters/sql/migrators/finalizer.rb +96 -0
  13. data/lib/pakyow/data/adapters/sql/runner.rb +49 -0
  14. data/lib/pakyow/data/adapters/sql/source_extension.rb +31 -0
  15. data/lib/pakyow/data/adapters/sql/types.rb +50 -0
  16. data/lib/pakyow/data/adapters/sql.rb +247 -0
  17. data/lib/pakyow/data/behavior/config.rb +28 -0
  18. data/lib/pakyow/data/behavior/lookup.rb +75 -0
  19. data/lib/pakyow/data/behavior/serialization.rb +40 -0
  20. data/lib/pakyow/data/connection.rb +103 -0
  21. data/lib/pakyow/data/container.rb +273 -0
  22. data/lib/pakyow/data/errors.rb +169 -0
  23. data/lib/pakyow/data/framework.rb +42 -0
  24. data/lib/pakyow/data/helpers.rb +11 -0
  25. data/lib/pakyow/data/lookup.rb +85 -0
  26. data/lib/pakyow/data/migrator.rb +182 -0
  27. data/lib/pakyow/data/object.rb +98 -0
  28. data/lib/pakyow/data/proxy.rb +262 -0
  29. data/lib/pakyow/data/result.rb +53 -0
  30. data/lib/pakyow/data/sources/abstract.rb +82 -0
  31. data/lib/pakyow/data/sources/ephemeral.rb +72 -0
  32. data/lib/pakyow/data/sources/relational/association.rb +43 -0
  33. data/lib/pakyow/data/sources/relational/associations/belongs_to.rb +47 -0
  34. data/lib/pakyow/data/sources/relational/associations/has_many.rb +54 -0
  35. data/lib/pakyow/data/sources/relational/associations/has_one.rb +54 -0
  36. data/lib/pakyow/data/sources/relational/associations/through.rb +67 -0
  37. data/lib/pakyow/data/sources/relational/command.rb +531 -0
  38. data/lib/pakyow/data/sources/relational/migrator.rb +101 -0
  39. data/lib/pakyow/data/sources/relational.rb +587 -0
  40. data/lib/pakyow/data/subscribers/adapters/memory.rb +153 -0
  41. data/lib/pakyow/data/subscribers/adapters/redis/pipeliner.rb +45 -0
  42. data/lib/pakyow/data/subscribers/adapters/redis/scripts/_shared.lua +73 -0
  43. data/lib/pakyow/data/subscribers/adapters/redis/scripts/expire.lua +16 -0
  44. data/lib/pakyow/data/subscribers/adapters/redis/scripts/persist.lua +15 -0
  45. data/lib/pakyow/data/subscribers/adapters/redis/scripts/register.lua +37 -0
  46. data/lib/pakyow/data/subscribers/adapters/redis.rb +209 -0
  47. data/lib/pakyow/data/subscribers.rb +148 -0
  48. data/lib/pakyow/data/tasks/bootstrap.rake +18 -0
  49. data/lib/pakyow/data/tasks/create.rake +22 -0
  50. data/lib/pakyow/data/tasks/drop.rake +32 -0
  51. data/lib/pakyow/data/tasks/finalize.rake +56 -0
  52. data/lib/pakyow/data/tasks/migrate.rake +24 -0
  53. data/lib/pakyow/data/tasks/reset.rake +18 -0
  54. data/lib/pakyow/data/types.rb +37 -0
  55. data/lib/pakyow/data.rb +27 -0
  56. data/lib/pakyow/environment/data/auto_migrate.rb +31 -0
  57. data/lib/pakyow/environment/data/config.rb +54 -0
  58. data/lib/pakyow/environment/data/connections.rb +76 -0
  59. data/lib/pakyow/environment/data/memory_db.rb +23 -0
  60. data/lib/pakyow/validations/unique.rb +26 -0
  61. metadata +186 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 48569dc1d1d2f24fdf3a94b7b642ac33ecf1f80ed5fd2ae336300815eccc59ff
4
+ data.tar.gz: b4fae401a84c092cdd0fa5df7564016a2f9209b6c53c81d408b8f5a3ae96900e
5
+ SHA512:
6
+ metadata.gz: 683e102bdc37a1806c78f181cdca846015d82277e3580ade5347f91a5f2182ba1e12f6cd48d1e2bd0184ce3fa7a96b9862a067924e0515f61319a4e2228f1efa
7
+ data.tar.gz: feeafc96cc49182ad4ad8819c628429fae848818fd284ade6906cdd34c32f0f8e1883cdcb72c23f037ebd86a70081a4f4f506ce430a7b5bd086cdab74077e0e7
data/CHANGELOG.md ADDED
File without changes
data/LICENSE ADDED
@@ -0,0 +1,4 @@
1
+ Copyright (c) Metabahn, LLC
2
+
3
+ Pakyow Data is an open-source project licensed under the terms of the LGPLv3 license.
4
+ See <https://choosealicense.com/licenses/lgpl-3.0/> for license text.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # pakyow-data
2
+
3
+ Data layer for Pakyow.
4
+
5
+ # Download
6
+
7
+ The latest version of Pakyow Data can be installed with RubyGems:
8
+
9
+ ```
10
+ gem install pakyow-data
11
+ ```
12
+
13
+ Source code can be downloaded as part of the Pakyow project on Github:
14
+
15
+ - https://github.com/pakyow/pakyow/tree/master/pakyow-data
16
+
17
+ # License
18
+
19
+ Pakyow Data is free and open-source under the [LGPLv3 license](https://choosealicense.com/licenses/lgpl-3.0/).
20
+
21
+ # Support
22
+
23
+ Found a bug? Tell us about it here:
24
+
25
+ - https://github.com/pakyow/pakyow/issues
26
+
27
+ We'd love to have you in the community:
28
+
29
+ - http://pakyow.org/get-involved
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pakyow
4
+ module Data
5
+ module Adapters
6
+ class Abstract
7
+ def initialize(opts, logger: nil)
8
+ @opts, @logger = opts, logger
9
+ end
10
+
11
+ def dataset_for_source(_source)
12
+ raise "dataset_for_source is not implemented on #{self}"
13
+ end
14
+
15
+ def result_for_attribute_value(_attribute, _value, _source)
16
+ raise "result_for_attribute_value is not implemented on #{self}"
17
+ end
18
+
19
+ def transaction
20
+ raise "transactions are not supported by #{self}"
21
+ end
22
+
23
+ def connected?
24
+ false
25
+ end
26
+
27
+ def migratable?
28
+ false
29
+ end
30
+
31
+ class << self
32
+ def types_for_adapter(_adapter)
33
+ end
34
+ end
35
+
36
+ module SourceExtension
37
+ end
38
+
39
+ module Commands
40
+ end
41
+
42
+ module DatasetMethods
43
+ def to_a(_dataset)
44
+ raise "to_a is not implemented on #{self}"
45
+ end
46
+
47
+ def one(_dataset)
48
+ raise "one is not implemented on #{self}"
49
+ end
50
+
51
+ def count(_dataset)
52
+ raise "count is not implemented on #{self}"
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pakyow
4
+ module Data
5
+ module Adapters
6
+ class Sql
7
+ module Commands
8
+ extend Support::Extension
9
+
10
+ apply_extension do
11
+ command :create, performs_create: true do |values|
12
+ begin
13
+ inserted_return_value = insert(values)
14
+ if self.class.primary_key_field
15
+ if Migrator::AUTO_INCREMENTING_TYPES.include?(self.class.primary_key_type)
16
+ where(self.class.primary_key_field => inserted_return_value)
17
+ else
18
+ where(self.class.primary_key_field => values[self.class.primary_key_field])
19
+ end
20
+ else
21
+ where(values)
22
+ end
23
+ rescue Sequel::UniqueConstraintViolation => error
24
+ raise UniqueViolation.build(error)
25
+ rescue Sequel::ForeignKeyConstraintViolation => error
26
+ raise ConstraintViolation.build(error)
27
+ end
28
+ end
29
+
30
+ command :update, performs_update: true do |values|
31
+ __getobj__.select(self.class.primary_key_field).map { |result|
32
+ result[self.class.primary_key_field]
33
+ }.tap do
34
+ begin
35
+ unless values.empty?
36
+ update(values)
37
+ end
38
+ rescue Sequel::UniqueConstraintViolation => error
39
+ raise UniqueViolation.build(error)
40
+ rescue Sequel::ForeignKeyConstraintViolation => error
41
+ raise ConstraintViolation.build(error)
42
+ end
43
+ end
44
+ end
45
+
46
+ command :delete, provides_dataset: false, performs_delete: true do
47
+ begin
48
+ delete
49
+ rescue Sequel::ForeignKeyConstraintViolation => error
50
+ raise ConstraintViolation.build(error)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pakyow
4
+ module Data
5
+ module Adapters
6
+ class Sql
7
+ module DatasetMethods
8
+ def to_a(dataset)
9
+ dataset.qualify.all
10
+ rescue Sequel::Error => error
11
+ raise QueryError.build(error)
12
+ end
13
+
14
+ def one(dataset)
15
+ dataset.qualify.first
16
+ rescue Sequel::Error => error
17
+ raise QueryError.build(error)
18
+ end
19
+
20
+ def count(dataset)
21
+ dataset.qualify.count
22
+ rescue Sequel::Error => error
23
+ raise QueryError.build(error)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pakyow
4
+ module Data
5
+ module Adapters
6
+ class Sql
7
+ class Differ
8
+ def initialize(connection:, source:, attributes: source.attributes)
9
+ @connection, @source, @attributes = connection, source, attributes
10
+ end
11
+
12
+ def exists?
13
+ raw_connection.table_exists?(table_name)
14
+ end
15
+
16
+ def changes?
17
+ attributes_to_add.any? || columns_to_remove.any? || column_types_to_change.any?
18
+ end
19
+
20
+ def table_name
21
+ @source.dataset_table
22
+ end
23
+
24
+ def attributes
25
+ Hash[@attributes.map { |attribute_name, attribute|
26
+ [attribute_name, @connection.adapter.finalized_attribute(attribute)]
27
+ }]
28
+ end
29
+
30
+ def attributes_to_add
31
+ {}.tap { |attributes|
32
+ self.attributes.each do |attribute_name, attribute_type|
33
+ unless schema.find { |column| column[0] == attribute_name }
34
+ attributes[attribute_name] = attribute_type
35
+ end
36
+ end
37
+ }
38
+ end
39
+
40
+ def columns_to_remove
41
+ {}.tap { |columns|
42
+ schema.each do |column_name, column_info|
43
+ unless @source.attributes.keys.find { |attribute_name| attribute_name == column_name }
44
+ columns[column_name] = column_info
45
+ end
46
+ end
47
+ }
48
+ end
49
+
50
+ def column_types_to_change
51
+ {}.tap { |attributes|
52
+ self.attributes.each do |attribute_name, attribute_type|
53
+ if found_column = schema.find { |column| column[0] == attribute_name }
54
+ column_name, column_info = found_column
55
+ unless column_info[:type] == attribute_type.meta[:column_type] && (!attribute_type.meta.include?(:native_type) || column_info[:db_type] == attribute_type.meta[:native_type])
56
+ attributes[column_name] = attribute_type.meta[:migration_type]
57
+ end
58
+ end
59
+ end
60
+ }
61
+ end
62
+
63
+ private
64
+
65
+ def raw_connection
66
+ @connection.adapter.connection
67
+ end
68
+
69
+ def schema
70
+ raw_connection.schema(table_name)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pakyow
4
+ module Data
5
+ module Adapters
6
+ class Sql
7
+ class Migrator
8
+ module AdapterMethods
9
+ module Mysql
10
+ def create!
11
+ handle_error do
12
+ @connection.adapter.connection.run("CREATE DATABASE `#{database}`")
13
+ end
14
+ end
15
+
16
+ def drop!
17
+ handle_error do
18
+ @connection.adapter.connection.run("DROP DATABASE `#{database}`")
19
+ end
20
+ end
21
+
22
+ def self.globalize_connection_opts!(connection_opts)
23
+ connection_opts[:initial] = Sql.build_opts(path: connection_opts[:path])
24
+ connection_opts[:path] = nil
25
+ end
26
+
27
+ private def database
28
+ if @connection.opts.key?(:initial)
29
+ @connection.opts[:initial][:path]
30
+ else
31
+ @connection.opts[:path]
32
+ end
33
+ end
34
+ end
35
+
36
+ module Postgres
37
+ def create!
38
+ handle_error do
39
+ @connection.adapter.connection.run("CREATE DATABASE \"#{database}\"")
40
+ end
41
+ end
42
+
43
+ def drop!
44
+ handle_error do
45
+ @connection.adapter.connection.run <<~SQL
46
+ SELECT
47
+ pg_terminate_backend(pid)
48
+ FROM
49
+ pg_stat_activity
50
+ WHERE
51
+ -- don't kill my own connection!
52
+ pid <> pg_backend_pid()
53
+ -- don't kill the connections to other databases
54
+ AND datname = '#{@connection.opts[:path]}';
55
+ SQL
56
+
57
+ @connection.adapter.connection.run("DROP DATABASE \"#{database}\"")
58
+ end
59
+ end
60
+
61
+ def self.globalize_connection_opts!(connection_opts)
62
+ connection_opts[:initial] = Sql.build_opts(path: connection_opts[:path])
63
+ connection_opts[:path] = "template1"
64
+ end
65
+
66
+ private def database
67
+ if @connection.opts.key?(:initial)
68
+ @connection.opts[:initial][:path]
69
+ else
70
+ @connection.opts[:path]
71
+ end
72
+ end
73
+ end
74
+
75
+ module Sqlite
76
+ def create!
77
+ # intentionally empty; automatically created on connect
78
+ end
79
+
80
+ def drop!
81
+ if File.exist?(@connection.opts[:path])
82
+ FileUtils.rm(@connection.opts[:path])
83
+ end
84
+ end
85
+
86
+ def self.globalize_connection_opts!(connection_opts)
87
+ # nothing to do here
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/data/adapters/sql/differ"
4
+ require "pakyow/data/sources/relational/migrator"
5
+
6
+ module Pakyow
7
+ module Data
8
+ module Adapters
9
+ class Sql
10
+ class Migrator < Sources::Relational::Migrator
11
+ require "pakyow/data/adapters/sql/migrator/adapter_methods"
12
+
13
+ def initialize(*)
14
+ super
15
+
16
+ extend self.class.adapter_methods_for_adapter(
17
+ @connection.opts[:adapter]
18
+ )
19
+ end
20
+
21
+ def disconnect!
22
+ @connection.disconnect
23
+ end
24
+
25
+ def create_source?(source)
26
+ !differ(source).exists?
27
+ end
28
+
29
+ def change_source?(source, attributes = source.attributes)
30
+ create_source?(source) || differ(source, attributes).changes?
31
+ end
32
+
33
+ def create_source!(source, attributes)
34
+ local_context = self
35
+ differ = differ(source, attributes)
36
+ create_table differ.table_name do
37
+ differ.attributes.each do |attribute_name, attribute|
38
+ local_context.send(:add_column_for_attribute, attribute_name, attribute, self, source)
39
+ end
40
+ end
41
+ end
42
+
43
+ def reassociate_source!(source, foreign_keys)
44
+ foreign_keys.each do |foreign_key_name, foreign_key|
45
+ differ = differ(source, foreign_key_name => foreign_key)
46
+
47
+ if create_source?(source) || differ.changes?
48
+ local_context = self
49
+
50
+ associate_table differ.table_name, with: foreign_key.meta[:foreign_key] do
51
+ attributes_to_add = if local_context.send(:create_source?, source)
52
+ differ.attributes
53
+ else
54
+ differ.attributes_to_add
55
+ end
56
+
57
+ attributes_to_add.each do |attribute_name, attribute|
58
+ local_context.send(:add_column_for_attribute, attribute_name, attribute, self, source, method_prefix: "add_")
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def change_source!(source, attributes)
66
+ differ = differ(source, attributes)
67
+
68
+ if differ.changes?
69
+ local_context = self
70
+
71
+ alter_table differ.table_name do
72
+ differ.attributes_to_add.each do |attribute_name, attribute|
73
+ local_context.send(:add_column_for_attribute, attribute_name, attribute, self, source, method_prefix: "add_")
74
+ end
75
+
76
+ differ.column_types_to_change.each do |column_name, _column_type|
77
+ local_context.send(:change_column_type_for_attribute, column_name, differ.attributes[column_name], self)
78
+ end
79
+
80
+ # TODO: revisit when we're ready to tackle foreign key removal
81
+ #
82
+ # differ.columns_to_remove.keys.each do |column_name|
83
+ # local_context.send(:remove_column_by_name, column_name, self)
84
+ # end
85
+ end
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def automator
92
+ require "pakyow/data/adapters/sql/migrators/automator"
93
+ Migrators::Automator.new(@connection, sources: @sources)
94
+ end
95
+
96
+ def finalizer
97
+ require "pakyow/data/adapters/sql/migrators/finalizer"
98
+ Migrators::Finalizer.new(@connection, sources: @sources)
99
+ end
100
+
101
+ def differ(source, attributes = source.attributes)
102
+ Differ.new(connection: @connection, source: source, attributes: attributes)
103
+ end
104
+
105
+ AUTO_INCREMENTING_TYPES = %i(integer bignum).freeze
106
+ def add_column_for_attribute(attribute_name, attribute, context, source, method_prefix: "")
107
+ if attribute.meta[:primary_key]
108
+ if AUTO_INCREMENTING_TYPES.include?(attribute.meta[:migration_type])
109
+ context.send(:"#{method_prefix}primary_key", attribute_name, type: type_for_attribute(attribute))
110
+ else
111
+ context.send(:"#{method_prefix}column", attribute_name, type_for_attribute(attribute), primary_key: true, **column_opts_for_attribute(attribute))
112
+ end
113
+ elsif attribute.meta[:foreign_key] && source.container.sources.any? { |potential_foreign_source| potential_foreign_source.plural_name == attribute.meta[:foreign_key] }
114
+ context.send(:"#{method_prefix}foreign_key", attribute_name, attribute.meta[:foreign_key], type: type_for_attribute(attribute))
115
+ else
116
+ context.send(:"#{method_prefix}column", attribute_name, type_for_attribute(attribute), **column_opts_for_attribute(attribute))
117
+ end
118
+ end
119
+
120
+ def change_column_type_for_attribute(attribute_name, attribute, context)
121
+ context.set_column_type(attribute_name, type_for_attribute(attribute), **column_opts_for_attribute(attribute))
122
+ end
123
+
124
+ def remove_column_by_name(column_name, context)
125
+ context.drop_column(column_name)
126
+ end
127
+
128
+ ALLOWED_COLUMN_OPTS = %i(size text)
129
+ def column_opts_for_attribute(attribute)
130
+ {}.tap do |opts|
131
+ ALLOWED_COLUMN_OPTS.each do |opt|
132
+ if attribute.meta.include?(opt)
133
+ opts[opt] = attribute.meta[opt]
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ def column_opts_string_for_attribute(attribute)
140
+ opts = column_opts_for_attribute(attribute)
141
+
142
+ if opts.any?
143
+ opts.each_with_object(String.new) { |(key, value), opts_string|
144
+ opts_string << ", #{key}: #{value.inspect}"
145
+ }
146
+ else
147
+ ""
148
+ end
149
+ end
150
+
151
+ def handle_error
152
+ yield
153
+ rescue Sequel::Error => error
154
+ Pakyow.logger.warn "#{error}"
155
+ end
156
+
157
+ class << self
158
+ def adapter_methods_for_adapter(adapter)
159
+ case adapter
160
+ when "mysql", "mysql2"
161
+ AdapterMethods::Mysql
162
+ when "postgres"
163
+ AdapterMethods::Postgres
164
+ when "sqlite"
165
+ AdapterMethods::Sqlite
166
+ end
167
+ end
168
+
169
+ def globalize_connection_opts!(connection_opts)
170
+ adapter_methods_for_adapter(
171
+ connection_opts[:adapter]
172
+ ).globalize_connection_opts!(
173
+ connection_opts
174
+ )
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/data/adapters/sql/migrator"
4
+
5
+ module Pakyow
6
+ module Data
7
+ module Adapters
8
+ class Sql
9
+ module Migrators
10
+ class Automator < Migrator
11
+ def associate_table(name, **, &block)
12
+ alter_table(name, &block)
13
+ end
14
+
15
+ def alter_table(name, &block)
16
+ @connection.adapter.connection.alter_table name do
17
+ AlterTable.new(self).instance_exec(&block)
18
+ end
19
+ end
20
+
21
+ def method_missing(name, *args, &block)
22
+ @connection.adapter.connection.public_send(name, *args, &block)
23
+ end
24
+
25
+ private
26
+
27
+ def type_for_attribute(attribute)
28
+ attribute.meta[:database_type]
29
+ end
30
+
31
+ class AlterTable
32
+ def initialize(table)
33
+ @table = table
34
+ end
35
+
36
+ def drop_column(*)
37
+ # Prevent columns from being dropped during auto migrate.
38
+ end
39
+
40
+ def method_missing(name, *args, &block)
41
+ @table.public_send(name, *args, &block)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/data/adapters/sql/migrator"
4
+
5
+ module Pakyow
6
+ module Data
7
+ module Adapters
8
+ class Sql
9
+ module Migrators
10
+ class Finalizer < Migrator
11
+ attr_reader :migrations
12
+
13
+ def initialize(*)
14
+ super
15
+ @migrations = []
16
+ end
17
+
18
+ def create_table(name, &block)
19
+ writer = Writer.new(root: true)
20
+ writer.create_table(name, &block)
21
+ @migrations << ["create_#{name}", writer]
22
+ end
23
+
24
+ def associate_table(name, with:, &block)
25
+ writer = Writer.new(root: true)
26
+ writer.alter_table(name, &block)
27
+ @migrations << ["associate_#{name}_with_#{with}", writer]
28
+ end
29
+
30
+ def alter_table(name, &block)
31
+ writer = Writer.new(root: true)
32
+ writer.alter_table(name, &block)
33
+ @migrations << ["change_#{name}", writer]
34
+ end
35
+
36
+ private
37
+
38
+ def type_for_attribute(attribute)
39
+ attribute.meta[:migration_type]
40
+ end
41
+
42
+ class Writer
43
+ def initialize(root: false)
44
+ @root, @content = root, String.new
45
+ end
46
+
47
+ def to_s
48
+ if @root
49
+ <<~CONTENT
50
+ change do
51
+ #{indent(@content.strip)}
52
+ end
53
+ CONTENT
54
+ else
55
+ @content.strip
56
+ end
57
+ end
58
+
59
+ def to_ary
60
+ [to_s]
61
+ end
62
+
63
+ def method_missing(name, *args, **kwargs, &block)
64
+ method_call = "#{name} #{args_to_string(args, kwargs)}"
65
+
66
+ if block_given?
67
+ @content << <<~CONTENT
68
+ #{method_call} do
69
+ #{indent(Writer.new.tap { |writer| writer.instance_exec(&block) }.to_s)}
70
+ end
71
+ CONTENT
72
+ else
73
+ @content << <<~CONTENT
74
+ #{method_call}
75
+ CONTENT
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def args_to_string(args, kwargs)
82
+ kwargs.each_with_object(args.map(&:inspect).join(", ")) { |(key, value), string|
83
+ string << ", #{key}: #{value.inspect}"
84
+ }
85
+ end
86
+
87
+ def indent(content)
88
+ content.split("\n").map { |line| " #{line}" }.join("\n")
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end