reactive_record 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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