reactive_record 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ Njg2MDk2YTkyZDExNzdjODJhZWJkNTAwM2M3OWY3ZTBkNTE1YjdkMw==
5
+ data.tar.gz: !binary |-
6
+ NDBiM2E0YzViNDMzNjJjNWE3NjJjMThiZmQwZjI5NmI0ZjIyYTBhMQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ YWRhOWFiMjE0YWI0MTM4N2E1MTAxZGY4N2MzOTMxYTIzOWEwNWJhYWFhOTMw
10
+ YThlOWNjMzc4ZTY0ODNjODA2M2MyM2Y2YjAyYTIwYjIzZTFmOTMwMGEzMTkz
11
+ MzIyZDQ4NWMxYmEwMDNkYzQzNDJkODA2NDQ3NzNmN2QzMjZkZDg=
12
+ data.tar.gz: !binary |-
13
+ YzBlMGU0ZDcxNzU5ZGZjZTZiYTc0YzJmNWRkNTk4N2E3ODI1ZjI0ZTE1NGRj
14
+ OTFhZWEwNjg2Yjc0NDQ5NGQxMjZhODQ0NWNjOTQ3N2I4ZjgxMDhlYTg0NDVk
15
+ YmJlYmNjNGE4MTczZThhNThmOWQ2MDI3NGE0MTliOTIzOWNiNTM=
data/.gitignore ADDED
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .ruby-version
19
+ .ruby-gemset
20
+ lib/parser.rb
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in reactive_record.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 Chris Wilson & Joe Nelson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included
14
+ in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # reactive_record
2
+
3
+ Generates ActiveRecord models to fit a pre-existing Postgres database.
4
+ Now you can use Rails with the db schema you always wanted. It's
5
+ **your** convention over configuration.
6
+
7
+ ## Why?
8
+
9
+ 1. Your app is specific to Postgres and proud of it. You use the mature
10
+ declarative data validation that only a real database can provide.
11
+ 1. You have inherited a database or are more comfortable creating one
12
+ yourself.
13
+ 1. You're a grown-ass DBA who doesn't want to speak ORM baby-talk.
14
+
15
+ ## Features
16
+
17
+ * Fully automatic. It just works.
18
+ * Creates a model for every table.
19
+ * Creates a comprehensive initial migration.
20
+ * Declares key-, uniqueness-, and presence-constraints.
21
+ * Creates associations.
22
+ * Adds custom validation methods for `CHECK` constraints.
23
+
24
+ ## Usage
25
+
26
+ ### Already familiar with Rails?
27
+
28
+ * Set up a postgres db normally
29
+ * Set `config.active_record.schema_format = :sql` to use a SQL `schema.rb`
30
+ * After you have migrated up a table, use `rails generate reactive_record:install`
31
+ * Go examine your generated models
32
+
33
+ ### Want more details?
34
+
35
+ **First** Include the `reactive_record` gem in your project's
36
+ `Gemfile`. *Oh by the way*, you'll have to use postgres in your
37
+ project. Setting up Rails for use with postgres is a bit outside
38
+ the scope of this document. Please see [Configuring a Database]
39
+ (http://guides.rubyonrails.org/configuring.html#configuring-a-database)
40
+ for what you need to do.
41
+
42
+ ``` ruby
43
+ gem 'reactive_record'
44
+ ```
45
+
46
+ Bundle to include the library
47
+
48
+ ``` shell
49
+ $ bundle
50
+ ```
51
+
52
+ **Next** Tell `ActiveRecord` to go into beast-mode. Edit your
53
+ `config/application.rb`, adding this line to use sql as the schema
54
+ format:
55
+
56
+ ``` ruby
57
+ module YourApp
58
+ class Application < Rails::Application
59
+ # other configuration bric-a-brac...
60
+ config.active_record.schema_format = :sql
61
+ end
62
+ end
63
+ ```
64
+
65
+ **Next** Create the database(s) just like you normally would:
66
+
67
+ ``` shell
68
+ rake db:create
69
+ ```
70
+
71
+ **Next** Generate a migration that will create the initial table:
72
+
73
+ ``` shell
74
+ $ rails generate migration create_employees
75
+ ```
76
+
77
+ Use your SQL powers to craft some
78
+ [DDL](http://en.wikipedia.org/wiki/Data_definition_language), perhaps
79
+ the "Hello, World!" of DB applications, `employees`?
80
+
81
+ ``` ruby
82
+ class CreateEmployees < ActiveRecord::Migration
83
+ def up
84
+ execute <<-SQL
85
+ CREATE TABLE employees (
86
+ id SERIAL,
87
+ name VARCHAR(255) NOT NULL,
88
+ email VARCHAR(255) NOT NULL UNIQUE,
89
+ start_date DATE NOT NULL,
90
+
91
+ PRIMARY KEY (id),
92
+ CONSTRAINT company_email CHECK (email LIKE '%@example.com')
93
+ );
94
+ SQL
95
+ end
96
+
97
+ def down
98
+ drop_table :employees
99
+ end
100
+ end
101
+ ```
102
+
103
+ **Lastly** Deploy the `reactive_record` generator:
104
+
105
+ ``` shell
106
+ $ rails generate reactive_record
107
+ ```
108
+
109
+ Go look at the generated file:
110
+
111
+ ``` ruby
112
+ class Employees < ActiveRecord::Base
113
+ set_table_name 'employees'
114
+ set_primary_key :id
115
+ validate :id, :name, :email, :start_date, presence: true
116
+ validate :email, uniqueness: true
117
+ validate { errors.add(:email, "Expected TODO") unless email =~ /.*@example.com/ }
118
+ end
119
+ ```
120
+
121
+ Reactive record does not currently attempt to generate any kind of
122
+ reasonable error message (I'm working on it) :)
123
+
124
+ **Enjoy**
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ rule '.rb' => '.y' do |t|
7
+ sh "racc -l -o #{t.name} #{t.source}"
8
+ end
9
+
10
+ task :compile => 'lib/parser.rb'
11
+
12
+ task :spec => :compile
13
+
14
+ task :default => :spec
@@ -0,0 +1,279 @@
1
+ class Node
2
+ def initialize val, *args
3
+ @value = val
4
+ @children = args
5
+ end
6
+
7
+ def gen
8
+ raise "gen is not implemented: #{self}"
9
+ end
10
+
11
+ def column
12
+ 'base'
13
+ end
14
+ end
15
+
16
+ class UniqueNode < Node
17
+ def initialize val; super(val, nil); end
18
+
19
+ def gen
20
+ "validates :#{@value.gen}, uniqueness: true"
21
+ end
22
+ end
23
+
24
+ class IdentNode < Node
25
+ def initialize val; super(val, nil); end
26
+
27
+ def gen
28
+ # bottoms out: IDENT is a string
29
+ @value
30
+ end
31
+
32
+ def column
33
+ # ident is likely a column (must be?)
34
+ @value
35
+ end
36
+ end
37
+
38
+ class ExprNode < Node
39
+ def initialize val; super(val); end
40
+
41
+ def column
42
+ @value.column
43
+ end
44
+
45
+ def gen
46
+ @value.gen
47
+ end
48
+ end
49
+
50
+ class EmptyExprNode < Node
51
+ def gen
52
+ nil
53
+ end
54
+
55
+ def column
56
+ nil
57
+ end
58
+ end
59
+
60
+ class CheckNode < Node
61
+ def initialize val; super(val); end
62
+
63
+ def gen
64
+ column = @value.column
65
+ val = @value.gen
66
+ if val
67
+ "validate { errors.add(:#{column}, \"Expected TODO\") unless #{val} }"
68
+ else
69
+ "validate { true }"
70
+ end
71
+ end
72
+ end
73
+
74
+ class TypedExprNode < Node
75
+ def initialize val, *args
76
+ @children = args
77
+ @type = @children[0]
78
+ @value = case @type.gen
79
+ when 'text' then TextNode.new(val)
80
+ when 'date' then DateExprNode.new(val)
81
+ else
82
+ raise "Unknown type: #{@children[0]}"
83
+ end
84
+ end
85
+
86
+ def column
87
+ @value.column
88
+ end
89
+
90
+ def gen
91
+ @value.gen
92
+ end
93
+ end
94
+
95
+ class TextNode < Node
96
+ def gen
97
+ @value.gen
98
+ end
99
+
100
+ def column
101
+ @value.column
102
+ end
103
+ end
104
+
105
+ class DateExprNode < Node
106
+ def initialize val
107
+ @value = val
108
+ end
109
+
110
+ def gen
111
+ val = @value.gen
112
+ if val == 'now'
113
+ "Time.now.to_date"
114
+ else
115
+ # YYYY-MM-DD
116
+ "Date.parse(\"#{val}\")"
117
+ end
118
+ end
119
+ end
120
+
121
+ class StrLitNode < Node
122
+ def initialize val; super(val); end
123
+
124
+ def gen
125
+ #FIXME HACK
126
+ if @value.respond_to? :gen
127
+ val = @value.gen
128
+ else
129
+ val = @value
130
+ end
131
+ val.gsub(/^'(.*)'$/, '\1')
132
+ end
133
+ end
134
+
135
+ class IntNode < Node
136
+ def initialize val; super(val); end
137
+
138
+ def gen
139
+ @value.to_s
140
+ end
141
+ end
142
+
143
+ class OperatorNode < Node
144
+ def initialize op, *args
145
+ @value = op
146
+ @children = args
147
+ @expr1 = @children[0]
148
+ @expr2 = @children[1]
149
+ end
150
+
151
+ def operation
152
+ case @value
153
+ when :gteq, :lteq, :neq, :eq, :gt, :lt
154
+ ComparisonExpr.new @value, @expr1, @expr2
155
+ when :plus
156
+ MathExpr.new @value, @expr1, @expr2
157
+ when :match
158
+ MatchExpr.new @expr1, @expr2
159
+ end
160
+ end
161
+
162
+ def error_msg
163
+ case @value
164
+ when :gteq then 'to be greater than or equal to'
165
+ when :lteq then 'to be less than or equal to'
166
+ when :neq then 'to not equal'
167
+ when :eq then 'to be equal to'
168
+ when :gt then 'to be greater than'
169
+ when :lt then 'to be less than'
170
+ when :plus then 'plus'
171
+ when :match then 'to match'
172
+ end
173
+ end
174
+
175
+ def column
176
+ c1 = @expr1.column
177
+ c2 = @expr1.column
178
+ return c1 if c1 != 'base'
179
+ return c2 if c2 != 'base'
180
+ 'base'
181
+ end
182
+
183
+ def gen
184
+ operation.gen
185
+ end
186
+ end
187
+
188
+ class ComparisonExpr
189
+ def initialize comparison, e1, e2
190
+ @e1, @e2 = e1, e2
191
+ @comparison = {
192
+ gteq: '>=',
193
+ lteq: '<=',
194
+ neq: '!=',
195
+ eq: '==',
196
+ gt: '>',
197
+ lt: '<',
198
+ }[comparison]
199
+ end
200
+
201
+ def gen
202
+ "#{@e1.gen} #{@comparison} #{@e2.gen}"
203
+ end
204
+ end
205
+
206
+ class MathExpr
207
+ def initialize op, e1, e2
208
+ @e1, @e2 = e1, e2
209
+ @operation = { plus: '+', minus: '-' }[op]
210
+ end
211
+
212
+ def gen
213
+ "#{@e1.gen} #{@operation} #{@e2.gen}"
214
+ end
215
+ end
216
+
217
+ class MatchExpr
218
+ def initialize e1, e2
219
+ @e1 = e1
220
+ @e2 = e2
221
+ end
222
+
223
+ def convert_wildcard str
224
+ str.gsub(/%/, '.*')
225
+ end
226
+
227
+ def gen
228
+ #raise "RHS: #{@e2.class} #{@e2.gen.class}"
229
+ "#{@e1.gen} =~ /#{convert_wildcard @e2.gen}/"
230
+ end
231
+ end
232
+
233
+ class TableNode < Node
234
+ def initialize tab, col
235
+ @tab = tab
236
+ @col = col
237
+ end
238
+
239
+ def table_to_class
240
+ @tab.gen.capitalize
241
+ end
242
+
243
+ def key_name
244
+ @col.gen
245
+ end
246
+
247
+ def gen
248
+ @tab.gen
249
+ end
250
+ end
251
+
252
+ class ActionNode < Node
253
+ def initialize action, consequence
254
+ @action = action
255
+ @consequence = consequence
256
+ end
257
+
258
+ def gen
259
+ "ON #{@action} #{@consequence}"
260
+ end
261
+ end
262
+
263
+ class ForeignKeyNode < Node
264
+ def initialize col, table, *actions
265
+ @col = col
266
+ @table = table
267
+ if actions.count > 0
268
+ @action = actions[0]
269
+ end
270
+ end
271
+
272
+ def gen
273
+ col = @col.gen
274
+ table_name = @table.gen
275
+ class_name = @table.table_to_class
276
+ key = @table.key_name
277
+ "belongs_to :#{table_name}, foreign_key: '#{col}', class: '#{class_name}', primary_key: '#{key}'"
278
+ end
279
+ end
@@ -0,0 +1,23 @@
1
+ require "reactive_record"
2
+
3
+ module ReactiveRecord
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ include ReactiveRecord
7
+
8
+ desc "Adds models based upon your existing Postgres DB"
9
+
10
+ def create_models
11
+ db_env = Rails.configuration.database_configuration[Rails.env]
12
+ raise 'You must use the pg database adapter' unless db_env['adapter'] == 'postgresql'
13
+ db = PG.connect dbname: db_env['database']
14
+ table_names(db).each do |table|
15
+ unless table == 'schema_migrations'
16
+ create_file "app/models/#{table.underscore.pluralize}.rb", model_definition(db, table)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+
data/lib/lexer.rb ADDED
@@ -0,0 +1,91 @@
1
+ require 'strscan'
2
+
3
+ module ConstraintParser
4
+ class Lexer
5
+ CASCADE = /CASCADE/
6
+ CHECK = /CHECK/
7
+ COMMA = /,/
8
+ DELETE = /DELETE/
9
+ EQ = /\=/
10
+ FOREIGN_KEY = /FOREIGN KEY/
11
+ GT = /\>/
12
+ GTEQ = /\>=/
13
+ IDENT = /[a-zA-Z][a-zA-Z0-9_]*/
14
+ INT = /[0-9]+/
15
+ LPAREN = /\(/
16
+ LT = /\</
17
+ LTEQ = /\<=/
18
+ MATCH_OP = /~~/
19
+ NEQ = /\<\>/
20
+ NEWLINE = /\n/
21
+ ON = /ON/
22
+ PLUS = /\+/
23
+ PRIMARY_KEY = /PRIMARY KEY/
24
+ REFERENCES = /REFERENCES/
25
+ RESTRICT = /RESTRICT/
26
+ RPAREN = /\)/
27
+ SPACE = /[\ \t]+/
28
+ STRLIT = /\'(\\.|[^\\'])*\'/
29
+ TYPE = /::/
30
+ UNIQUE = /UNIQUE/
31
+
32
+ def initialize io
33
+ @ss = StringScanner.new io.read
34
+ end
35
+
36
+ def next_token
37
+ return if @ss.eos?
38
+
39
+ result = false
40
+ while !result
41
+ result = case
42
+ when text = @ss.scan(SPACE) then #ignore whitespace
43
+
44
+ # Operators
45
+ when text = @ss.scan(GTEQ) then [:GTEQ, text]
46
+ when text = @ss.scan(LTEQ) then [:LTEQ, text]
47
+ when text = @ss.scan(NEQ) then [:NEQ, text]
48
+ when text = @ss.scan(GT) then [:GT, text]
49
+ when text = @ss.scan(LT) then [:LT, text]
50
+ when text = @ss.scan(EQ) then [:EQ, text]
51
+ when text = @ss.scan(PLUS) then [:PLUS, text]
52
+ when text = @ss.scan(MATCH_OP) then [:MATCH_OP, text]
53
+
54
+ # SQL Keywords
55
+ when text = @ss.scan(CHECK) then [:CHECK, text]
56
+ when text = @ss.scan(UNIQUE) then [:UNIQUE, text]
57
+ when text = @ss.scan(PRIMARY_KEY) then [:PRIMARY_KEY, text]
58
+ when text = @ss.scan(FOREIGN_KEY) then [:FOREIGN_KEY, text]
59
+ when text = @ss.scan(REFERENCES) then [:REFERENCES, text]
60
+ when text = @ss.scan(DELETE) then [:DELETE, text]
61
+ when text = @ss.scan(ON) then [:ON, text]
62
+ when text = @ss.scan(RESTRICT) then [:RESTRICT, text]
63
+ when text = @ss.scan(CASCADE) then [:CASCADE, text]
64
+
65
+ # Values
66
+ when text = @ss.scan(IDENT) then [:IDENT, text]
67
+ when text = @ss.scan(STRLIT) then [:STRLIT, text]
68
+ when text = @ss.scan(INT) then [:INT, text]
69
+
70
+ # Syntax
71
+ when text = @ss.scan(LPAREN) then [:LPAREN, text]
72
+ when text = @ss.scan(RPAREN) then [:RPAREN, text]
73
+ when text = @ss.scan(TYPE) then [:TYPE, text]
74
+ when text = @ss.scan(COMMA) then [:COMMA, text]
75
+ else
76
+ x = @ss.getch
77
+ [x, x]
78
+ end
79
+ end
80
+ result
81
+ end
82
+
83
+ def tokenize
84
+ out = []
85
+ while (tok = next_token)
86
+ out << tok
87
+ end
88
+ out
89
+ end
90
+ end
91
+ end
data/lib/parser.y ADDED
@@ -0,0 +1,98 @@
1
+ class ConstraintParser::Parser
2
+ prechigh
3
+ left PLUS MATCH_OP
4
+ left GTEQ LTEQ NEQ EQ GT LT
5
+ preclow
6
+
7
+ token CASCADE
8
+ CHECK
9
+ COMMA
10
+ DELETE
11
+ EQ
12
+ FOREIGN_KEY
13
+ GT
14
+ GTEQ
15
+ IDENT
16
+ INT
17
+ LPAREN
18
+ LT
19
+ LTEQ
20
+ MATCH_OP
21
+ NEQ
22
+ NEWLINE
23
+ ON
24
+ PLUS
25
+ PRIMARY_KEY
26
+ REFERENCES
27
+ RESTRICT
28
+ RPAREN
29
+ SPACE
30
+ STRLIT
31
+ TYPE
32
+ UNIQUE
33
+
34
+ rule
35
+ constraint : unique_column { result = val[0] }
36
+ | check_statement { result = val[0] }
37
+ | foreign_key { result = val[0] }
38
+ ;
39
+
40
+ unique_column : UNIQUE LPAREN IDENT RPAREN { result = UniqueNode.new(IdentNode.new(val[2])) }
41
+ ;
42
+
43
+ check_statement : CHECK expr { result = CheckNode.new(val[1]) }
44
+
45
+ expr : { result = EmptyExprNode.new :empty }
46
+ | LPAREN expr RPAREN { result = ExprNode.new(val[1]) }
47
+ | expr type_signature { result = TypedExprNode.new(val[0], val[1]) }
48
+ | expr operator expr { result = OperatorNode.new(val[1], val[0], val[2]) }
49
+ | IDENT { result = IdentNode.new(val[0]) }
50
+ | STRLIT { result = StrLitNode.new(val[0]) }
51
+ | INT { result = IntNode.new(val[0]) }
52
+ ;
53
+
54
+ operator : GTEQ { result = :gteq }
55
+ | LTEQ { result = :lteq }
56
+ | NEQ { result = :neq }
57
+ | EQ { result = :eq }
58
+ | GT { result = :gt }
59
+ | LT { result = :lt }
60
+ | PLUS { result = :plus }
61
+ | MATCH_OP { result = :match }
62
+ ;
63
+
64
+ type_signature : TYPE IDENT { result = IdentNode.new(val[1]) }
65
+ ;
66
+
67
+ foreign_key : FOREIGN_KEY column_spec REFERENCES table_spec { result = ForeignKeyNode.new(val[1], val[3]) }
68
+ | FOREIGN_KEY column_spec REFERENCES table_spec action_spec { result = ForeignKeyNode.new(val[1], val[3], val[4]) }
69
+ ;
70
+
71
+ column_spec : LPAREN IDENT RPAREN { result = IdentNode.new(val[1]) }
72
+ ;
73
+
74
+ table_spec : IDENT column_spec { result = TableNode.new(IdentNode.new(val[0]), val[1]) }
75
+ ;
76
+
77
+ action_spec : ON DELETE RESTRICT { result = ActionNode.new(:delete, :restrict) }
78
+ | ON DELETE CASCADE { result = ActionNode.new(:delete, :cascade) }
79
+ ;
80
+ end
81
+
82
+ ---- inner
83
+
84
+ require 'lexer'
85
+ require 'code_generator'
86
+
87
+ def initialize tokenizer, handler = nil
88
+ @tokenizer = tokenizer
89
+ super()
90
+ end
91
+
92
+ def next_token
93
+ @tokenizer.next_token
94
+ end
95
+
96
+ def parse
97
+ do_parse
98
+ end
@@ -0,0 +1,115 @@
1
+ require_relative "./reactive_record/version"
2
+
3
+ require 'pg'
4
+ require 'active_support/inflector'
5
+ require 'parser'
6
+
7
+ module ReactiveRecord
8
+ def model_definition db, table_name
9
+ header = "class #{table_name.classify.pluralize} < ActiveRecord::Base\n"
10
+ footer = "end\n"
11
+
12
+ body = []
13
+ body << "set_table_name '#{table_name}'"
14
+ body << "set_primary_key :#{primary_key db, table_name}"
15
+ body << "#{validate_definition non_nullable_columns(db, table_name), 'presence'}"
16
+ body << "#{validate_definition unique_columns(db, table_name), 'uniqueness'}"
17
+
18
+ generate_constraints(db, table_name).each do |con|
19
+ body << con
20
+ end
21
+
22
+ indent = " "
23
+ body = indent + body.join("\n" + indent) + "\n"
24
+ header + body + footer
25
+ end
26
+
27
+ def validate_definition cols, type
28
+ return '' if cols.empty?
29
+ symbols = cols.map { |c| ":#{c}" }
30
+ "validate #{symbols.join ', '}, #{type}: true"
31
+ end
32
+
33
+ def table_names db
34
+ results = db.exec(
35
+ "select table_name from information_schema.tables where table_schema = $1",
36
+ ['public']
37
+ )
38
+ results.map { |r| r['table_name'] }
39
+ end
40
+
41
+ def constraints db, table
42
+ db.exec("""
43
+ SELECT c.relname AS table_name,
44
+ con.conname AS constraint_name,
45
+ pg_get_constraintdef( con.oid, false) AS constraint_src
46
+ FROM pg_constraint con
47
+ JOIN pg_namespace n on (n.oid = con.connamespace)
48
+ JOIN pg_class c on (c.oid = con.conrelid)
49
+ WHERE con.conrelid != 0
50
+ AND c.relname = $1
51
+ ORDER BY con.conname;
52
+ """, [table])
53
+ end
54
+
55
+ def generate_constraints db, table
56
+ key_or_pkey = lambda do |row|
57
+ row['constraint_name'].end_with?('_key') || row['constraint_name'].end_with?('_pkey')
58
+ end
59
+
60
+ constraints(db, table)
61
+ .reject(&key_or_pkey)
62
+ .map(&parse_constraint)
63
+ end
64
+
65
+ def parse_constraint
66
+ lambda do |row|
67
+ src = row['constraint_src']
68
+ parser = ConstraintParser::Parser.new(ConstraintParser::Lexer.new(StringIO.new src))
69
+ parser.parse.gen
70
+ end
71
+ end
72
+
73
+ def cols_with_contype db, table_name, type
74
+ db.exec """
75
+ SELECT column_name, conname
76
+ FROM pg_constraint, information_schema.columns
77
+ WHERE table_name = $1
78
+ AND contype = $2
79
+ AND ordinal_position = any(conkey);
80
+ """, [table_name, type]
81
+ end
82
+
83
+ def column_name
84
+ lambda {|row| row['column_name']}
85
+ end
86
+
87
+ def table_name
88
+ lambda {|row| row['table_name']}
89
+ end
90
+
91
+ def primary_key db, table_name
92
+ matching_primary_key = lambda {|row| row['conname'] == "#{table_name}_pkey"}
93
+ cols_with_contype(db, table_name, 'p')
94
+ .select(&matching_primary_key)
95
+ .map(&column_name)
96
+ .first
97
+ end
98
+
99
+ def unique_columns db, table_name
100
+ matching_unique_constraint = lambda {|row| row['conname'] == "#{table_name}_#{row['column_name']}_key"}
101
+ cols_with_contype(db, table_name, 'u')
102
+ .select(&matching_unique_constraint)
103
+ .map(&column_name)
104
+ end
105
+
106
+ def non_nullable_columns db, table_name
107
+ result = db.exec """
108
+ SELECT column_name
109
+ FROM information_schema.columns
110
+ WHERE table_name = $1
111
+ AND is_nullable = $2
112
+ """, [table_name, 'NO']
113
+ result.map { |r| r['column_name'] }
114
+ end
115
+ end
@@ -0,0 +1,3 @@
1
+ module ReactiveRecord
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'reactive_record/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "reactive_record"
8
+ gem.version = ReactiveRecord::VERSION
9
+ gem.authors = ["Joe Nelson", "Chris Wilson"]
10
+ gem.email = ["christopher.j.wilson@gmail.com"]
11
+ gem.description = %q{Generate ActiveRecord models from a pre-existing Postgres db}
12
+ gem.summary = %q{Use the schema you always wanted.}
13
+ gem.homepage = "https://github.com/twopoint718/reactive_record"
14
+ gem.licenses = ['MIT']
15
+
16
+ gem.files = `git ls-files`.split($/)
17
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
+ gem.require_paths = ["lib"]
20
+
21
+ gem.add_dependency "pg"
22
+ gem.add_dependency "activesupport"
23
+
24
+ gem.add_development_dependency 'rspec'
25
+ gem.add_development_dependency 'racc'
26
+ end
@@ -0,0 +1,145 @@
1
+ require 'rspec'
2
+ require_relative '../lib/lexer.rb'
3
+
4
+ describe ConstraintParser::Lexer do
5
+ let(:lex){ConstraintParser::Lexer}
6
+
7
+ it "CHECK (((email)::text ~~ '%@bendyworks.com'::text))" do
8
+ @lex = lex.new \
9
+ StringIO.new "CHECK (((email)::text ~~ '%@bendyworks.com'::text))"
10
+ @lex.tokenize.should == [
11
+ [:CHECK, 'CHECK'],
12
+ [:LPAREN, '('],
13
+ [:LPAREN, '('],
14
+ [:LPAREN, '('],
15
+ [:IDENT, 'email'],
16
+ [:RPAREN, ')'],
17
+ [:TYPE, '::'],
18
+ [:IDENT, 'text'],
19
+ [:MATCH_OP, '~~'],
20
+ [:STRLIT, "'%@bendyworks.com'"],
21
+ [:TYPE, '::'],
22
+ [:IDENT, 'text'],
23
+ [:RPAREN, ')'],
24
+ [:RPAREN, ')'],
25
+ ]
26
+ end
27
+
28
+ it "UNIQUE (email)" do
29
+ @lex = lex.new StringIO.new "UNIQUE (email)"
30
+ @lex.tokenize.should == [
31
+ [:UNIQUE, 'UNIQUE'],
32
+ [:LPAREN, '('],
33
+ [:IDENT, 'email'],
34
+ [:RPAREN, ')']
35
+ ]
36
+ end
37
+
38
+ it "PRIMARY KEY (employee_id)" do
39
+ @lex = lex.new StringIO.new "PRIMARY KEY (employee_id)"
40
+ @lex.tokenize.should == [
41
+ [:PRIMARY_KEY, 'PRIMARY KEY'],
42
+ [:LPAREN, '('],
43
+ [:IDENT, 'employee_id'],
44
+ [:RPAREN, ')']
45
+ ]
46
+ end
47
+
48
+ it "CHECK ((start_date >= '2009-04-13'::date))" do
49
+ @lex = lex.new StringIO.new "CHECK ((start_date >= '2009-04-13'::date))"
50
+ @lex.tokenize.should == [
51
+ [:CHECK, 'CHECK'],
52
+ [:LPAREN, '('],
53
+ [:LPAREN, '('],
54
+ [:IDENT, 'start_date'],
55
+ [:GTEQ, '>='],
56
+ [:STRLIT, "'2009-04-13'"],
57
+ [:TYPE, '::'],
58
+ [:IDENT, 'date'],
59
+ [:RPAREN, ')'],
60
+ [:RPAREN, ')'],
61
+ ]
62
+ end
63
+
64
+ it '"CHECK ((start_date <= ((\'now\'::text)::date + 365)))"' do
65
+ @lex = lex.new StringIO.new "CHECK ((start_date <= ((\'now\'::text)::date + 365)))"
66
+ @lex.tokenize.should == [
67
+ [:CHECK, 'CHECK'],
68
+ [:LPAREN, '('],
69
+ [:LPAREN, '('],
70
+ [:IDENT, 'start_date'],
71
+ [:LTEQ, '<='],
72
+ [:LPAREN, '('],
73
+ [:LPAREN, '('],
74
+ [:STRLIT, "'now'"],
75
+ [:TYPE, '::'],
76
+ [:IDENT, 'text'],
77
+ [:RPAREN, ')'],
78
+ [:TYPE, '::'],
79
+ [:IDENT, 'date'],
80
+ [:PLUS, '+'],
81
+ [:INT, '365'],
82
+ [:RPAREN, ')'],
83
+ [:RPAREN, ')'],
84
+ [:RPAREN, ')'],
85
+ ]
86
+ end
87
+
88
+ it "FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE RESTRICT" do
89
+ @lex = lex.new StringIO.new "FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE RESTRICT"
90
+ @lex.tokenize.should == [
91
+ [:FOREIGN_KEY, 'FOREIGN KEY'],
92
+ [:LPAREN, '('],
93
+ [:IDENT, 'employee_id'],
94
+ [:RPAREN, ')'],
95
+ [:REFERENCES, 'REFERENCES'],
96
+ [:IDENT, 'employees'],
97
+ [:LPAREN, '('],
98
+ [:IDENT, 'employee_id'],
99
+ [:RPAREN, ')'],
100
+ [:ON, 'ON'],
101
+ [:DELETE, 'DELETE'],
102
+ [:RESTRICT, 'RESTRICT'],
103
+ ]
104
+ end
105
+
106
+ it "PRIMARY KEY (employee_id, project_id)" do
107
+ @lex = lex.new StringIO.new "PRIMARY KEY (employee_id, project_id)"
108
+ @lex.tokenize.should == [
109
+ [:PRIMARY_KEY, 'PRIMARY KEY'],
110
+ [:LPAREN, '('],
111
+ [:IDENT, 'employee_id'],
112
+ [:COMMA, ','],
113
+ [:IDENT, 'project_id'],
114
+ [:RPAREN, ')']
115
+ ]
116
+ end
117
+
118
+ it "FOREIGN KEY (project_id) REFERENCES projects(project_id) ON DELETE CASCADE" do
119
+ @lex = lex.new StringIO.new "FOREIGN KEY (project_id) REFERENCES projects(project_id) ON DELETE CASCADE"
120
+ @lex.tokenize.should == [
121
+ [:FOREIGN_KEY, 'FOREIGN KEY'],
122
+ [:LPAREN, '('],
123
+ [:IDENT, 'project_id'],
124
+ [:RPAREN, ')'],
125
+ [:REFERENCES, 'REFERENCES'],
126
+ [:IDENT, 'projects'],
127
+ [:LPAREN, '('],
128
+ [:IDENT, 'project_id'],
129
+ [:RPAREN, ')'],
130
+ [:ON, 'ON'],
131
+ [:DELETE, 'DELETE'],
132
+ [:CASCADE, 'CASCADE'],
133
+ ]
134
+ end
135
+
136
+ it "PRIMARY KEY (project_id)" do
137
+ @lex = lex.new StringIO.new "PRIMARY KEY (project_id)"
138
+ @lex.tokenize.should == [
139
+ [:PRIMARY_KEY, 'PRIMARY KEY'],
140
+ [:LPAREN, '('],
141
+ [:IDENT, 'project_id'],
142
+ [:RPAREN, ')'],
143
+ ]
144
+ end
145
+ end
@@ -0,0 +1,39 @@
1
+ require 'rspec'
2
+ require_relative '../lib/lexer.rb'
3
+ require_relative '../lib/parser.rb'
4
+ require_relative '../lib/code_generator.rb'
5
+
6
+ describe ConstraintParser::Parser do
7
+ let(:lex){ConstraintParser::Lexer}
8
+ let(:par){ConstraintParser::Parser}
9
+
10
+ it 'CHECK ()' do
11
+ @parser = par.new(lex.new StringIO.new 'CHECK ()')
12
+ @parser.parse.gen.should == 'validate { true }'
13
+ end
14
+
15
+ it "CHECK (((email)::text ~~ '%@bendyworks.com'::text))" do
16
+ @parser = par.new(lex.new StringIO.new "CHECK (((email)::text ~~ '%@bendyworks.com'::text))")
17
+ @parser.parse.gen.should == 'validate { errors.add(:email, "Expected TODO") unless email =~ /.*@bendyworks.com/ }'
18
+ end
19
+
20
+ it "UNIQUE (email)" do
21
+ @parser = par.new(lex.new StringIO.new 'UNIQUE (email)')
22
+ @parser.parse.gen.should == "validates :email, uniqueness: true"
23
+ end
24
+
25
+ it "CHECK ((start_date <= ('now'::text)::date))" do
26
+ @parser = par.new(lex.new StringIO.new "CHECK ((start_date <= ('now'::text)::date))")
27
+ @parser.parse.gen.should == "validate { errors.add(:start_date, \"Expected TODO\") unless start_date <= Time.now.to_date }"
28
+ end
29
+
30
+ it "CHECK ((start_date <= (('now'::text)::date + 365)))" do
31
+ @parser = par.new(lex.new StringIO.new "CHECK ((start_date <= (('now'::text)::date + 365)))")
32
+ @parser.parse.gen.should == 'validate { errors.add(:start_date, "Expected TODO") unless start_date <= Time.now.to_date + 365 }'
33
+ end
34
+
35
+ it "FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE RESTRICT" do
36
+ @parser = par.new(lex.new StringIO.new "FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE RESTRICT")
37
+ @parser.parse.gen.should == "belongs_to :employees, foreign_key: 'employee_id', class: 'Employees', primary_key: 'employee_id'"
38
+ end
39
+ end
@@ -0,0 +1,119 @@
1
+ require 'rspec'
2
+ require_relative '../lib/reactive_record'
3
+
4
+ include ReactiveRecord
5
+
6
+ describe 'ReactiveRecord' do
7
+ before :all do
8
+ # db setup
9
+ @dbname = "reactive_record_test_#{Process.pid}"
10
+
11
+ # N.B. all reactive_record methods are read only and so I believe
12
+ # that it is valid to share a db connection. Nothing reactive record
13
+ # does should mutate any global state
14
+ system "createdb #{@dbname}"
15
+ @db = PG.connect dbname: @dbname
16
+ @db.exec File.read('spec/seed/database.sql')
17
+ end
18
+
19
+ after :all do
20
+ # db teardown
21
+ @db.close
22
+ system "dropdb #{@dbname}"
23
+ end
24
+
25
+ context '#model_definition' do
26
+ it 'generates an employee model def' do
27
+ model_definition(@db, 'employees').should ==
28
+ <<-EOS.gsub(/^ {10}/, '')
29
+ class Employees < ActiveRecord::Base
30
+ set_table_name 'employees'
31
+ set_primary_key :id
32
+ validate :id, :name, :email, :start_date, presence: true
33
+ validate :email, uniqueness: true
34
+ validate { errors.add(:email, "Expected TODO") unless email =~ /.*@example.com/ }
35
+ validate { errors.add(:start_date, "Expected TODO") unless start_date >= Date.parse(\"2009-04-13\") }
36
+ validate { errors.add(:start_date, "Expected TODO") unless start_date <= Time.now.to_date + 365 }
37
+ end
38
+ EOS
39
+ end
40
+
41
+ it 'generates a project model def' do
42
+ model_definition(@db, 'projects').should ==
43
+ <<-EOS.gsub(/^ {10}/, '')
44
+ class Projects < ActiveRecord::Base
45
+ set_table_name 'projects'
46
+ set_primary_key :id
47
+ validate :id, :name, presence: true
48
+ validate :name, uniqueness: true
49
+ end
50
+ EOS
51
+ end
52
+ end
53
+
54
+ context '#constraints' do
55
+ it 'gathers all constraints on the employees table' do
56
+ constraints(@db, 'employees').to_a.should == [
57
+ {"table_name"=>"employees", "constraint_name"=>"company_email", "constraint_src"=>"CHECK (((email)::text ~~ '%@example.com'::text))"},
58
+ {"table_name"=>"employees", "constraint_name"=>"employees_email_key", "constraint_src"=>"UNIQUE (email)"},
59
+ {"table_name"=>"employees", "constraint_name"=>"employees_pkey", "constraint_src"=>"PRIMARY KEY (id)"},
60
+ {"table_name"=>"employees", "constraint_name"=>"founding_date", "constraint_src"=>"CHECK ((start_date >= '2009-04-13'::date))"},
61
+ {"table_name"=>"employees", "constraint_name"=>"future_start_date", "constraint_src"=>"CHECK ((start_date <= (('now'::text)::date + 365)))"}
62
+ ]
63
+ end
64
+ it 'gathers all constraints on the projects table' do
65
+ constraints(@db, 'projects').to_a.should == [
66
+ {"table_name"=>"projects", "constraint_name"=>"projects_name_key", "constraint_src"=>"UNIQUE (name)"},
67
+ {"table_name"=>"projects", "constraint_name"=>"projects_pkey", "constraint_src"=>"PRIMARY KEY (id)"}
68
+ ]
69
+ end
70
+ it 'gathers all constraints on the employees_projects table' do
71
+ constraints(@db, 'employees_projects').to_a.should == [
72
+ {
73
+ "table_name"=>"employees_projects",
74
+ "constraint_name"=>"employees_projects_employee_id_fkey",
75
+ "constraint_src"=>"FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE RESTRICT"
76
+ },
77
+ {
78
+ "table_name"=>"employees_projects",
79
+ "constraint_name"=>"employees_projects_pkey",
80
+ "constraint_src"=>"PRIMARY KEY (employee_id, project_id)"
81
+ },
82
+ {
83
+ "table_name"=>"employees_projects",
84
+ "constraint_name"=>"employees_projects_project_id_fkey",
85
+ "constraint_src"=>"FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE"
86
+ }
87
+ ]
88
+ end
89
+ end
90
+
91
+ context '#generate_constraints' do
92
+ it 'generates ruby code for employees constraints' do
93
+ generate_constraints(@db, 'employees').should == [
94
+ "validate { errors.add(:email, \"Expected TODO\") unless email =~ /.*@example.com/ }",
95
+ "validate { errors.add(:start_date, \"Expected TODO\") unless start_date >= Date.parse(\"2009-04-13\") }",
96
+ "validate { errors.add(:start_date, \"Expected TODO\") unless start_date <= Time.now.to_date + 365 }",
97
+ ]
98
+ end
99
+ end
100
+
101
+ context '#unique_columns' do
102
+ it 'returns email for employees' do
103
+ unique_columns(@db, 'employees').should == ['email']
104
+ end
105
+ end
106
+
107
+ context '#cols_with_contype' do
108
+ it 'identifies UNIQUE columns in database' do
109
+ cols_with_contype(@db, 'employees', 'u').to_a.should == [{"column_name"=>"email", "conname"=>"employees_email_key"},
110
+ {"column_name"=>"name", "conname"=>"projects_name_key"}]
111
+ end
112
+ end
113
+
114
+ context '#non_nullable_columns' do
115
+ it 'identifies NOT NULL columns in employees' do
116
+ non_nullable_columns(@db, 'employees').should == ['id', 'name', 'email', 'start_date']
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,67 @@
1
+ -- create tables
2
+
3
+ CREATE TABLE employees (
4
+ id SERIAL,
5
+ name VARCHAR(255) NOT NULL,
6
+ email VARCHAR(255) NOT NULL UNIQUE,
7
+ start_date DATE NOT NULL,
8
+
9
+ PRIMARY KEY (id),
10
+
11
+ CONSTRAINT founding_date CHECK (start_date >= '2009-04-13'),
12
+ CONSTRAINT future_start_date CHECK (start_date <= CURRENT_DATE + 365),
13
+ CONSTRAINT company_email CHECK (email LIKE '%@example.com')
14
+ );
15
+
16
+ CREATE TABLE projects (
17
+ id INTEGER NOT NULL,
18
+ name VARCHAR(255) NOT NULL UNIQUE,
19
+
20
+ PRIMARY KEY (id)
21
+ );
22
+
23
+ CREATE TABLE employees_projects (
24
+ employee_id INTEGER REFERENCES employees ON DELETE RESTRICT,
25
+ project_id INTEGER REFERENCES projects ON DELETE CASCADE,
26
+
27
+ PRIMARY KEY (employee_id, project_id)
28
+ );
29
+
30
+ -- seed data
31
+
32
+ INSERT INTO employees (id, name, email, start_date)
33
+ VALUES
34
+ (1, 'Alfred Arnold', 'alfred@example.com', '2009-04-13'),
35
+ (2, 'Benedict Burton', 'benedict@example.com', '2011-09-01'),
36
+ (3, 'Cat Cams', 'cat@example.com', '2009-04-13'),
37
+ (4, 'Duane Drummond', 'duane@example.com', '2009-04-13'),
38
+ (5, 'Elizabeth Eggers', 'elizabeth@example.com', '2009-04-13'),
39
+ (6, 'Fred Fitzgerald', 'fred@example.com', '2012-01-01'),
40
+ (7, 'Greg Gruber', 'greg@example.com', '2013-01-07'),
41
+ (8, 'Horatio Helms', 'horatio@example.com', '2012-01-01'),
42
+ (9, 'Ian Ives', 'ian@example.com', '2009-04-13'),
43
+ (10, 'Jan Jarvis', 'jan@example.com', '2013-01-28'),
44
+ (11, 'Kevin Kelvin', 'kevin@example.com', '2009-04-13'),
45
+ (12, 'Lucy Lemieux', 'lucy@example.com', '2009-04-13');
46
+
47
+ INSERT INTO projects (id, name)
48
+ VALUES
49
+ (1, 'Yoyodyne Inc.'),
50
+ (2, 'Global Omni Mega Corp.'),
51
+ (3, 'Murray''s Widgets Ltd.'),
52
+ (4, 'Spatula City'),
53
+ (5, 'Aperture Laboratories');
54
+
55
+ INSERT INTO employees_projects (employee_id, project_id)
56
+ VALUES
57
+ (1,1),
58
+ (2,1),
59
+ (3,2),
60
+ (4,2),
61
+ (5,3),
62
+ (6,3),
63
+ (7,4),
64
+ (8,4),
65
+ (9,5),
66
+ (10,5),
67
+ (11,5);
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: reactive_record
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Joe Nelson
8
+ - Chris Wilson
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-10-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: pg
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ! '>='
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ! '>='
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: activesupport
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ! '>='
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ! '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rspec
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ! '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ! '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: racc
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ description: Generate ActiveRecord models from a pre-existing Postgres db
71
+ email:
72
+ - christopher.j.wilson@gmail.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - .gitignore
78
+ - .rspec
79
+ - Gemfile
80
+ - LICENSE.txt
81
+ - README.md
82
+ - Rakefile
83
+ - lib/code_generator.rb
84
+ - lib/generators/reactive_record/install_generator.rb
85
+ - lib/lexer.rb
86
+ - lib/parser.y
87
+ - lib/reactive_record.rb
88
+ - lib/reactive_record/version.rb
89
+ - reactive_record.gemspec
90
+ - spec/lexer_spec.rb
91
+ - spec/parser_spec.rb
92
+ - spec/reactive_record_spec.rb
93
+ - spec/seed/database.sql
94
+ homepage: https://github.com/twopoint718/reactive_record
95
+ licenses:
96
+ - MIT
97
+ metadata: {}
98
+ post_install_message:
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ! '>='
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ! '>='
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ requirements: []
113
+ rubyforge_project:
114
+ rubygems_version: 2.1.5
115
+ signing_key:
116
+ specification_version: 4
117
+ summary: Use the schema you always wanted.
118
+ test_files:
119
+ - spec/lexer_spec.rb
120
+ - spec/parser_spec.rb
121
+ - spec/reactive_record_spec.rb
122
+ - spec/seed/database.sql