schema_monkey 1.0.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +0 -1
  4. data/Gemfile +2 -0
  5. data/README.md +204 -28
  6. data/lib/schema_monkey.rb +30 -17
  7. data/lib/schema_monkey/active_record.rb +25 -0
  8. data/lib/schema_monkey/client.rb +65 -0
  9. data/lib/schema_monkey/errors.rb +6 -0
  10. data/lib/schema_monkey/{tool/module.rb → module.rb} +10 -7
  11. data/lib/schema_monkey/monkey.rb +31 -0
  12. data/lib/schema_monkey/{tool/rake.rb → rake.rb} +1 -1
  13. data/lib/schema_monkey/stack.rb +44 -0
  14. data/lib/schema_monkey/{tool/tasks → tasks}/insert.rake +0 -0
  15. data/lib/schema_monkey/version.rb +1 -1
  16. data/schema_dev.yml +0 -1
  17. data/schema_monkey.gemspec +2 -2
  18. data/spec/active_record_spec.rb +75 -0
  19. data/spec/middleware_spec.rb +0 -7
  20. data/spec/spec_helper.rb +2 -2
  21. metadata +18 -41
  22. data/lib/schema_monkey/core_extensions.rb +0 -23
  23. data/lib/schema_monkey/core_extensions/active_record/base.rb +0 -31
  24. data/lib/schema_monkey/core_extensions/active_record/connection_adapters/abstract_adapter.rb +0 -38
  25. data/lib/schema_monkey/core_extensions/active_record/connection_adapters/mysql2_adapter.rb +0 -31
  26. data/lib/schema_monkey/core_extensions/active_record/connection_adapters/postgresql_adapter.rb +0 -30
  27. data/lib/schema_monkey/core_extensions/active_record/connection_adapters/schema_statements.rb +0 -83
  28. data/lib/schema_monkey/core_extensions/active_record/connection_adapters/sqlite3_adapter.rb +0 -33
  29. data/lib/schema_monkey/core_extensions/active_record/connection_adapters/table_definition.rb +0 -42
  30. data/lib/schema_monkey/core_extensions/active_record/migration/command_recorder.rb +0 -19
  31. data/lib/schema_monkey/core_extensions/active_record/schema_dumper.rb +0 -227
  32. data/lib/schema_monkey/core_extensions/middleware.rb +0 -62
  33. data/lib/schema_monkey/tool.rb +0 -43
  34. data/lib/schema_monkey/tool/client.rb +0 -67
  35. data/lib/schema_monkey/tool/errors.rb +0 -4
  36. data/lib/schema_monkey/tool/monkey.rb +0 -46
  37. data/lib/schema_monkey/tool/stack.rb +0 -90
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fcf24614a0b60131f52dc7bdd8dae426dac1d814
4
- data.tar.gz: 8767b57fb97dc4b5f7ff17129b0dd6ddb0850f5f
3
+ metadata.gz: 2090c17d3f5bba79e1885f2f76a22a8c0bfcb389
4
+ data.tar.gz: d8fecf5bea771ace12cf1511583098408f9e2f38
5
5
  SHA512:
6
- metadata.gz: 33210d7c9683c756deb46931f049b25bdd0efdd3aea15a5fd984e69fe8529f234e6c1a5ef61424628535c3414dcd3b79befe4a02c50c87e0ae695fb5d1a0f8e1
7
- data.tar.gz: f573a50c27d09f31b0be37f86c278d6cc7b216370abfb06d015044c3c1b1ee210c33d71f052e1cf070edd4cd9690ad354b8920129400a2fa2859f0074a6cd074
6
+ metadata.gz: e2fdef632c27ed6619821a0b9fed86d1931a86eea897991ca0a98c88af7f7c3dfcb855243499f29af6c9d4f25397e6c3e42d2a630555eea14b820709c48cb808
7
+ data.tar.gz: ee53c8e8d0b1d7cb4dd824096cf208475ab16dc56cd2a9c5f2e84a844d00adcff40a60345809a30c98ed1e39bbd076c48417e642c01a1126836d1b060ebc5749
data/.gitignore CHANGED
@@ -1,6 +1,7 @@
1
1
  /coverage
2
2
  /tmp
3
3
  /pkg
4
+ Gemfile.local
4
5
 
5
6
  *.lock
6
7
  *.log
@@ -5,7 +5,6 @@
5
5
  ---
6
6
  sudo: false
7
7
  rvm:
8
- - 1.9.3
9
8
  - 2.1.5
10
9
  gemfile:
11
10
  - gemfiles/activerecord-4.2/Gemfile.mysql2
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
1
  source "http://rubygems.org"
2
2
 
3
3
  gemspec
4
+
5
+ File.exist?(gemfile_local = File.expand_path('../Gemfile.local', __FILE__)) and eval File.read(gemfile_local), binding, gemfile_local
data/README.md CHANGED
@@ -5,37 +5,227 @@
5
5
 
6
6
  # SchemaMonkey
7
7
 
8
- SchemaMonkey is a behind-the-scenes gem to facilitate writing extensions to ActiveRecord (typically other gems, but could also be in an application). It provides:
8
+ SchemaMonkey is a behind-the-scenes gem to make it easy to write extensions to ActiveRecord. It provides:
9
9
 
10
- * A "middleware"-style interface to key ActiveRecord internal functions. For example, there's a middleware hook to let you insert a handler for migration column definition options, and there are several hooks to insert handlers for the various parts of a schema dump.
10
+ * A simple convention-based mechanism to insert modules into ActiveRecord modules.
11
+ * A simple convention-based mechanism to create and use [Modware](https://rubygems.org/gems/modware) middleware stacks.
11
12
 
12
- * A convention-based protocol for `include`'ing custom modules into ActiveRecord. You just define your modules and SchemaMonkey will automatically include them in the right places.
13
+ SchemaMonkey by itself doesn't add any behavior -- SchemaMonkey is intended to make it easy to add clients that define methods and stacks, that are then available to other clients or the app. (In particular, most clients of SchemaMonkey will depend on [schema_plus_core](https://github.com/SchemaPlus/schema_plus_core), which is a SchemaMonkey client that provides an "internal extension API" to ActiveRecord.)
13
14
 
15
+ ## Installation
14
16
 
15
- The middleware interface has two benefits: it provides a clean API so that the gem or aplication code doesn't need to monkey-patch ActiveRecord (SchemaMonkey does all the monkey-patching for you), and it lets multiple client gems operate in parallel without concern about conflicting monkey-patches.
17
+ As usual:
16
18
 
19
+ ```ruby
20
+ gem "schema_monkey" # in a Gemfile
21
+ gem.add_dependency "schema_monkey" # in a .gemspec
22
+ ```
17
23
 
18
- ## Installation
24
+ To use with a rails app, also include
19
25
 
20
- As usual:
26
+ ```ruby
27
+ gem "schema_monkey_rails"
28
+ ```
29
+
30
+ which creates a Railtie to insert SchemaMonkey appropriately into the rails stack. To use with Padrino, see [schema_monkey_padrino](https://github.com/SchemaPlus/schema_monkey_padrino).
31
+
32
+ ## Usage
33
+
34
+ SchemaMonkey works with the notion of a "client" -- which is a module containining definitions. A typical SchemaMonkey client looks like
21
35
 
22
36
  ```ruby
23
- # In a gem's .gemspec:
24
- spec.add_dependency "schema_monkey", "~> <MAJOR>.<MINOR>", ">= <MAJOR>.<MINOR>.<PATCH>"
37
+ require 'schema_monkey'
38
+ require 'other-client1' # if needed
39
+ require 'other-client2' # as needed
40
+
41
+ module MyClient
42
+
43
+ module ActiveRecord
44
+ #
45
+ # active record extensions, if any
46
+ #
47
+ end
48
+
49
+ module Middleware
50
+ #
51
+ # middleware stack modules, if any
52
+ #
53
+ end
54
+
55
+ end
25
56
 
26
- # In an applications Gemfile:
27
- gem "schema_monkey", "~> <MAJOR>.<MINOR>", ">= <MAJOR>.<MINOR>.<PATCH>"
57
+ SchemaMonkey.register MyClient # <--- That's it! No configuration needed
28
58
  ```
29
59
 
30
- SchemaMonkey follows semantic versioning; it's a good idea to explicitly use the `~>` and `>=` dependencies to make sure your gem's clients don't accidentally pull in a version of SchemaMonkey that your gem isn't compatible with.
60
+ of course a typical client will be split into files corresponding to submodules; e.g. here's the top level of [schema_plus_indexes](https://github.com/SchemaPlus/schema_plus_indexes):
31
61
 
32
- To use with a rails app, also include
62
+ ```ruby
63
+ require 'schema_plus/core'
64
+
65
+ require_relative 'schema_plus_indexes/active_record/base'
66
+ require_relative 'schema_plus_indexes/active_record/connection_adapters/abstract_adapter'
67
+ require_relative 'schema_plus_indexes/active_record/connection_adapters/index_definition'
68
+
69
+ require_relative 'schema_plus_indexes/middleware/dumper'
70
+ require_relative 'schema_plus_indexes/middleware/migration'
71
+ require_relative 'schema_plus_indexes/middleware/model'
72
+ require_relative 'schema_plus_indexes/middleware/query'
73
+
74
+ SchemaMonkey.register SchemaPlusIndexes
75
+ ```
76
+
77
+ The details of ActiveRecord exentions and Middleware modules are described below.
78
+
79
+ ## ActiveRecord Extensions
80
+
81
+ Here's a simple example of an extension to ActiveRecord:
33
82
 
34
83
  ```ruby
35
- gem "schema_monkey_rails"
84
+ require 'schema_monkey'
85
+
86
+ module PracticalJoker
87
+ module ActiveRecord
88
+ module Base
89
+
90
+ def save(*args)
91
+ raise "April Fools!" if Time.now.yday == 31
92
+ super
93
+ end
94
+
95
+ module ClassMethods
96
+ def columns
97
+ raise "Boo!" if Time.now.yday == 304
98
+ super
99
+ end
100
+ end
101
+
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ SchemaMonkey.register PracticalJoker
108
+ ```
109
+
110
+ SchemaMonkey inserts each submodule of `MyClient::ActiveRecord` into the corresponding module of ActiveRecord, with `ClassMethods` inserted as class methods.
111
+
112
+ This works for arbitrary submodule paths, such as `MyClient::ActiveRecord::ConnectionAdapters::TableDefinition`. SchemaMonkey will raise an error if the client defines a module that does not have a corresponding ActiveRecord module.
113
+
114
+ Notice that insertion is done using `:prepend`, so that client modules can override existing methods and use `super`.
115
+
116
+ ### DBMS-specific insertion
117
+
118
+ If a client module's name includes one the dbms names `Mysql`, `PostgreSQL` or `SQLite3` (case insensitive), the insertion will only be performed if that's the dbms in use. So, e.g. `MyClient::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter` will only be inserted if the app is using PostgreSQL.
119
+
120
+ Additionally, for ActiveRecord modules that are not inherently dbms-specific, you can use one of the dbms names (case insensitive) as a component in the client module's path to do dbms-specific insertion. E.g.
121
+
122
+ ```ruby
123
+ module MyClient
124
+ module ActiveRecord
125
+ module ConnectionAdapters
126
+ module Sqlite3
127
+ module TableDefinition
128
+ #
129
+ # SQLite3-specific enhancements to
130
+ # ActiveRecord::ConnectionAdapters::TableDefinition
131
+ #
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+ ```
138
+
139
+ The dbms name component can be anywhere in the module path after `MyClient::ActiveRecord`
140
+
141
+ ### `insert` vs `prepend`
142
+
143
+
144
+ * By default, SchemaMonkey inserts a client module using `prepend`, and a client ClassMethods module using `singleton_class.prepend`. This allows overriding existing methods and using `super`. On insertion, Ruby will of course call the module's `self.prepended` method, if one is defined.
145
+
146
+ * However, if the client module defines a module method `self.included` then SchemaMonkey will use `include` for a module and `singleton_class.include` for a ClassMethods module -- and Ruby will of course call that method.
147
+
148
+ Note that in the case of a ClassMethods module, when Ruby calls `self.prepended` or `self.included`, it will pass the singleton class. For convience SchemaMonkey will also call `self.extended` if defined passing it the ActiveRecord module itself, just as Ruby would if `extend` were used.
149
+
150
+ ## Middleware Modlues
151
+
152
+ SchemaMonkey provides a convention-based front end to using [Modware](https://github.com/ronen/modware) middleware stacks.
153
+
154
+ SchemaMonkey uses Ruby modules to organize the stacks: Each stack is contained in a submodule of `SchemaMonkey::Middleware`
155
+
156
+ ### Defining a stack
157
+
158
+ Here's an example of defining a middleware stack:
159
+
160
+ ```ruby
161
+ module MyClient
162
+ module Middleware
163
+ module Index
164
+ module Exists
165
+ Env = [:connection, :table_name, :column_name, :options, :result]
166
+ end
167
+ end
168
+ end
169
+ end
170
+ ```
171
+
172
+ This defines a stack available at `SchemaMonkey::Middleware::Index::Exists`. You can use any module path you want for organizational convenience. The const `Env` signals to SchemaMonkey to create a stack at that location; the environment object for the stack will have the listed fields. (Env actually can be an array of symbols or a Class, as per `Modware::Stack.new`.)
173
+
174
+ SchemaMonkey will raise an error if a stack had already been defined there.
175
+
176
+ The defined module has a module method `start` that delegates to `Modware::Stack.start`. Here's an example of using the above stack as a wrapper around ActiveRecord's `index_exists?` method:
177
+
178
+ ```ruby
179
+ module MyClient
180
+ module ActiveRecord
181
+ module ConnectionAdapters
182
+ module SchemaStatements
183
+ def index_exists?(table_name, column_name, options = {})
184
+ SchemaMonkey::Middleware::Index::Exists.start(connection: self, table_name: table_name, column_name: column_name, options: options) { |env|
185
+ env.result = super env.table_name, env.column_name, env.options
186
+ }.result
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
36
192
  ```
37
193
 
38
- which creates a Railtie to insert SchemaMonkey appropriately into the rails stack.
194
+ This is a fairly typical idiom for wrapping behavior in a stack:
195
+
196
+ 1. Pass `self` and the method arguments to the stack environment
197
+ 2. Call the base implementation, passing it argument values from the environment (giving clients a chance to modify them in `before` or `around` methods)
198
+ 3. Place the result in the environment (giving clients a chance to modify it in `after` or `around` methods
199
+ 4. `start` returns the environment object -- the method returns the result that's stored in the environment
200
+
201
+ ### Inserting Middleware in a stack
202
+
203
+ If an earlier client defined a stack, a later client can insert middleware into the stack:
204
+
205
+ ```ruby
206
+ require 'my_client' # earlier client defines the stack
207
+
208
+ module UColumnImpliesUnique
209
+ module Middlware
210
+ module Index
211
+ module Exists
212
+ def before(env)
213
+ env.options.reverse_merge!(unique: env.column_name.start_with? 'u')
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end
219
+
220
+ SchemaMonkey.register(UColumnImpliesUnique)
221
+ ```
222
+
223
+ SchemaMonkey uses the module `MyLaterClient::Middleware::Index::Exists` as [Modware](https://github.com/ronen/modware) middleware for the corresponding stack. The middleware module can define middleware methods `before`, `arround`, `after`, or `implementation` as per [Modware](https://github.com/ronen/modware)
224
+
225
+ Note that the distinguishing feature between defining and using a stack is whether `Env` is defined.
226
+
227
+
228
+
39
229
 
40
230
  ## Compatibility
41
231
 
@@ -43,16 +233,10 @@ SchemaMonkey is tested on:
43
233
 
44
234
  <!-- SCHEMA_DEV: MATRIX - begin -->
45
235
  <!-- These lines are auto-generated by schema_dev based on schema_dev.yml -->
46
- * ruby **1.9.3** with activerecord **4.2**, using **mysql2**, **sqlite3** or **postgresql**
47
236
  * ruby **2.1.5** with activerecord **4.2**, using **mysql2**, **sqlite3** or **postgresql**
48
237
 
49
238
  <!-- SCHEMA_DEV: MATRIX - end -->
50
239
 
51
- ## Usage
52
-
53
-
54
- **Sorry -- no real documentation yet. See examples in [schema_plus_indexes](https://github/SchemaPlus/schema_plus_indexes) and [schema_plus_pg_indexes](https://github/SchemaPlus/schema_plus_pg_indexes)**
55
-
56
240
 
57
241
 
58
242
  ## Development & Testing
@@ -62,14 +246,6 @@ the standard protocol: fork, feature branch, develop, push, and issue pull reque
62
246
 
63
247
  Some things to know about to help you develop and test:
64
248
 
65
- * SchemaMonkey is a wrapper around two subparts:
66
-
67
- * `SchemaMonkey::Tool` provides the convention-based mechanism for registering clients that extend ActiveRecord using `include`'s and middleware.
68
-
69
- * `SchemaMonkey::CoreExtensions` defines the ActiveRecord extension API. It is itself just the first client registered with `SchemaMonkey::Tool`. **Ugh. Currently no specs for `SchemaMonkey::CoreExtensions`; testing indirectly by testing the client gems that use it. Working on it...**
70
-
71
- One day might actually split these into separate gems to decouple their development and testing. And actually the middleware mechanism of `SchemaMonkey::Tool` could be a split out separate gem.
72
-
73
249
  * **schema_dev**: SchemaMonkey uses [schema_dev](https://github.com/SchemaPlus/schema_dev) to
74
250
  facilitate running rspec tests on the matrix of ruby, rails, and database
75
251
  versions that the gem supports, both locally and on
@@ -1,5 +1,15 @@
1
- require_relative "schema_monkey/core_extensions"
2
- require_relative "schema_monkey/tool"
1
+ require 'active_record'
2
+ require 'active_support/core_ext/string'
3
+ require 'its-it'
4
+ require 'modware'
5
+
6
+ require_relative "schema_monkey/active_record"
7
+ require_relative "schema_monkey/client"
8
+ require_relative "schema_monkey/errors"
9
+ require_relative "schema_monkey/module"
10
+ require_relative "schema_monkey/monkey"
11
+ require_relative "schema_monkey/stack"
12
+ require_relative 'schema_monkey/rake'
3
13
 
4
14
  #
5
15
  # Middleware contents will be created dynamically
@@ -10,31 +20,34 @@ module SchemaMonkey
10
20
  end
11
21
 
12
22
  #
13
- # Wrap public API of SchemaMonkey::Tool
23
+ #
14
24
  #
15
25
  module SchemaMonkey
26
+
27
+ DBMS = [:PostgreSQL, :Mysql, :SQLite3]
28
+
16
29
  def self.register(mod)
17
- Tool::register(mod)
30
+ monkey.register(mod)
18
31
  end
19
32
 
20
33
  def self.insert(opts={})
21
- Tool::insert(opts)
34
+ monkey.insert(opts)
35
+ end
36
+
37
+ private
38
+
39
+ def self.monkey
40
+ @monkey ||= Monkey.new
22
41
  end
23
42
 
24
- def self.include_once(*args)
25
- Tool::Module.include_once(*args)
43
+ def self.reset_for_rspec
44
+ @monkey = nil
45
+ self.reset_middleware
26
46
  end
27
47
 
28
- module Rake
29
- def self.insert(*args)
30
- Tool::Rake::insert(*args)
31
- end
48
+ def self.reset_middleware
49
+ SchemaMonkey.send :remove_const, :Middleware
50
+ SchemaMonkey.send :const_set, :Middleware, ::Module.new
32
51
  end
33
52
 
34
- MiddlewareError = Tool::MiddlewareError
35
53
  end
36
-
37
- #
38
- # Register CoreExtensions
39
- #
40
- SchemaMonkey::Tool.register(SchemaMonkey::CoreExtensions)
@@ -0,0 +1,25 @@
1
+ module SchemaMonkey
2
+ module ActiveRecord
3
+ module ConnectionAdapters
4
+ module AbstractAdapter
5
+ def initialize(*args)
6
+ super
7
+ dbm = case adapter_name
8
+ when /^MySQL/i then :Mysql
9
+ when 'PostgreSQL', 'PostGIS' then :PostgreSQL
10
+ when 'SQLite' then :SQLite3
11
+ end
12
+ SchemaMonkey.insert(dbm: dbm)
13
+ end
14
+ end
15
+ end
16
+
17
+ def self.insert(relative_path, mod)
18
+ class_methods = relative_path.sub!(/::ClassMethods$/, '')
19
+ base = Module.const_lookup(::ActiveRecord, relative_path)
20
+ raise InsertionError, "No module ActiveRecord::#{relative_path} to insert #{mod}" unless base
21
+ Module.insert (class_methods ? base.singleton_class : base), mod
22
+ mod.extended base if class_methods and mod.respond_to? :extended
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,65 @@
1
+ module SchemaMonkey
2
+ class Client
3
+
4
+ def initialize(mod)
5
+ @root = mod
6
+ @inserted_middleware = {}
7
+ end
8
+
9
+ def insert(dbm: nil)
10
+ insert_active_record(dbm: dbm)
11
+ insert_middleware(dbm: dbm)
12
+ end
13
+
14
+ private
15
+
16
+ def insert_active_record(dbm: nil)
17
+ # Kernel.warn "--- inserting active_record for #{@root}, dbm=#{dbm.inspect}"
18
+ find_modules(:ActiveRecord, dbm: dbm).each do |mod|
19
+ next if mod.is_a? Class
20
+ relative_path = canonicalize_path(mod, :ActiveRecord, dbm)
21
+ ActiveRecord.insert(relative_path, mod)
22
+ end
23
+ end
24
+
25
+ def insert_middleware(dbm: nil)
26
+ find_modules(:Middleware, dbm: dbm).each do |mod|
27
+ next if @inserted_middleware[mod]
28
+ relative_path = canonicalize_path(mod, :Middleware, dbm)
29
+ Stack.insert(relative_path, mod) unless relative_path.empty?
30
+ @inserted_middleware[mod] = true
31
+ end
32
+ end
33
+
34
+ def canonicalize_path(mod, base, dbm)
35
+ path = mod.to_s.sub(/^#{@root}::#{base}::/, '')
36
+ if dbm
37
+ path = path.split('::')
38
+ if (i = path.find_index(&it =~ /\b#{dbm}\b/i)) # delete first occurence
39
+ path.delete_at i
40
+ end
41
+ path = path.join('::').gsub(/#{dbm}/i, dbm.to_s) # canonicalize case for things like PostgreSQLAdapter
42
+ end
43
+ path
44
+ end
45
+
46
+ def find_modules(container, dbm: nil)
47
+ return [] unless (container = Module.const_lookup @root, container)
48
+
49
+ if dbm
50
+ accept = /#{dbm}/i
51
+ reject = nil
52
+ else
53
+ accept = nil
54
+ reject = /\b(#{DBMS.join('|')})/i
55
+ end
56
+
57
+ modules = []
58
+ modules += Module.descendants(container, can_load: accept)
59
+ modules.select!(&it.to_s =~ accept) if accept
60
+ modules.reject!(&it.to_s =~ reject) if reject
61
+ modules
62
+ end
63
+
64
+ end
65
+ end