surikat 0.2.2

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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.idea/.rakeTasks +7 -0
  4. data/.idea/inspectionProfiles/Project_Default.xml +16 -0
  5. data/.idea/misc.xml +7 -0
  6. data/.idea/modules.xml +8 -0
  7. data/.idea/surikat.iml +50 -0
  8. data/.idea/vcs.xml +6 -0
  9. data/.idea/workspace.xml +744 -0
  10. data/.rspec +2 -0
  11. data/.travis.yml +5 -0
  12. data/Gemfile +6 -0
  13. data/LICENSE.txt +21 -0
  14. data/README.md +399 -0
  15. data/Rakefile +6 -0
  16. data/bin/console +11 -0
  17. data/bin/setup +8 -0
  18. data/exe/surikat +234 -0
  19. data/lib/surikat.rb +421 -0
  20. data/lib/surikat/base_model.rb +35 -0
  21. data/lib/surikat/base_queries.rb +10 -0
  22. data/lib/surikat/base_type.rb +3 -0
  23. data/lib/surikat/configurations.rb +22 -0
  24. data/lib/surikat/new_app.rb +108 -0
  25. data/lib/surikat/routes.rb +67 -0
  26. data/lib/surikat/scaffold.rb +503 -0
  27. data/lib/surikat/session.rb +35 -0
  28. data/lib/surikat/session_manager.rb +92 -0
  29. data/lib/surikat/templates/.rspec.tmpl +1 -0
  30. data/lib/surikat/templates/.standalone_migrations.tmpl +6 -0
  31. data/lib/surikat/templates/Gemfile.tmpl +31 -0
  32. data/lib/surikat/templates/Rakefile.tmpl +2 -0
  33. data/lib/surikat/templates/aaa_queries.rb.tmpl +124 -0
  34. data/lib/surikat/templates/aaa_spec.rb.tmpl +151 -0
  35. data/lib/surikat/templates/application.yml.tmpl +14 -0
  36. data/lib/surikat/templates/base_aaa_model.rb.tmpl +28 -0
  37. data/lib/surikat/templates/base_model.rb.tmpl +6 -0
  38. data/lib/surikat/templates/base_spec.rb.tmpl +148 -0
  39. data/lib/surikat/templates/config.ru.tmpl +61 -0
  40. data/lib/surikat/templates/console.tmpl +14 -0
  41. data/lib/surikat/templates/crud_queries.rb.tmpl +105 -0
  42. data/lib/surikat/templates/database.yml.tmpl +26 -0
  43. data/lib/surikat/templates/hello_queries.rb.tmpl +19 -0
  44. data/lib/surikat/templates/hello_spec.rb.tmpl +39 -0
  45. data/lib/surikat/templates/routes.yml.tmpl +15 -0
  46. data/lib/surikat/templates/spec_helper.rb.tmpl +11 -0
  47. data/lib/surikat/templates/test_helper.rb.tmpl +30 -0
  48. data/lib/surikat/types.rb +45 -0
  49. data/lib/surikat/version.rb +3 -0
  50. data/lib/surikat/yaml_configurator.rb +18 -0
  51. data/surikat.gemspec +47 -0
  52. metadata +199 -0
@@ -0,0 +1,35 @@
1
+ require 'active_record'
2
+ require 'ransack'
3
+
4
+ #require File.join(Gem::Specification.find_by_name("surikat").full_gem_path, "lib/surikat/yaml_configurator")
5
+
6
+ module Surikat
7
+
8
+ class BaseModel < ActiveRecord::Base
9
+ ActiveRecord::Base.establish_connection(Surikat.config.db)
10
+
11
+ self.abstract_class = true
12
+
13
+ # Used when running tests
14
+ def self.create_random
15
+ create random_params
16
+ end
17
+
18
+ def self.random_params
19
+ params = {}
20
+ columns.each do |col|
21
+ next if ['id', 'created_at', 'updated_at'].include?(col.name)
22
+ params[col.name] = case col.type.to_s
23
+ when 'string'
24
+ "Some String #{SecureRandom.hex(4)}"
25
+ when 'float', 'integer'
26
+ rand(100)
27
+ when 'boolean'
28
+ [true, false].sample
29
+ end
30
+ end
31
+ params
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,10 @@
1
+ module Surikat
2
+ class BaseQueries
3
+ def initialize(arguments, session)
4
+ @arguments, @session = arguments, session
5
+ end
6
+
7
+ attr_reader :arguments
8
+ attr_reader :session
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ class BaseType
2
+
3
+ end
@@ -0,0 +1,22 @@
1
+ module ActiveRecordMigrations
2
+ class Configurations
3
+ include Singleton
4
+
5
+ attr_accessor :yaml_config, :database_configuration, :environment, :db_dir, :migrations_paths, :schema_format, :seed_loader
6
+
7
+ def initialize
8
+ @yaml_config = 'config/database.yml'
9
+ @environment = ENV['db'] || Rails.env
10
+ @db_dir = 'db'
11
+ @migrations_paths = ['db/migrate']
12
+ @schema_format = :ruby # or :sql
13
+ @seed_loader = Rails.application
14
+ end
15
+
16
+ alias configure instance_eval
17
+
18
+ def database_configuration
19
+ @database_configuration ||= YAML.load(ERB.new(File.read @yaml_config).result)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,108 @@
1
+ module NewApp
2
+
3
+ # Generate a new Surikat app with a Hello World query in it.
4
+ def new_app(arguments)
5
+ app_name = arguments.first
6
+
7
+ if app_name.nil?
8
+ puts "Usage: surikat new app_name\nCreates a new, empty Surikat app"
9
+ return
10
+ end
11
+
12
+ # Create app directory structure
13
+ create_dirs app_name
14
+
15
+ # Create initial files
16
+ {
17
+ 'Gemfile' => {path: app_name},
18
+ 'Rakefile' => {path: app_name},
19
+ '.standalone_migrations' => {path: app_name},
20
+ '.rspec' => {path: app_name},
21
+ 'config.ru' => {path: app_name, vars: {app_name_capitalized: app_name.capitalize}},
22
+ 'hello_queries.rb' => {path: "#{app_name}/app/queries"},
23
+ 'console' => {path: "#{app_name}/bin"},
24
+ 'routes.yml' => {path: "#{app_name}/config"},
25
+ 'database.yml' => {path: "#{app_name}/config"},
26
+ 'application.yml' => {path: "#{app_name}/config"},
27
+ 'spec_helper.rb' => {path: "#{app_name}/spec"},
28
+ 'test_helper.rb' => {path: "#{app_name}/spec"},
29
+ 'hello_spec.rb' => {path: "#{app_name}/spec"}
30
+ }.each do |name, details|
31
+ print "Create #{name}... "
32
+ copy_template name, details
33
+ puts 'ok'
34
+ end
35
+
36
+ # Post-templating...
37
+ FileUtils.chmod '+x', "#{app_name}/bin/console"
38
+
39
+ `cd #{app_name} && rake db:migrate`
40
+
41
+ # Show help :)
42
+ show_help app_name
43
+ end
44
+
45
+ def create_dirs app_name
46
+ dirs = [nil, '/bin', '/app/models', '/app/queries', '/log', '/tmp/pids', '/config/initializers', '/db', '/spec']
47
+ dirs.each do |dir|
48
+ print "Create directory #{app_name}#{dir}... "
49
+ FileUtils.mkdir_p "#{app_name}#{dir}"
50
+ puts "ok"
51
+ end
52
+ end
53
+
54
+ def show_help app_name
55
+ puts <<-EOT
56
+
57
+ =================================================================
58
+ Done. What next?
59
+
60
+ cd #{app_name}
61
+ bundle install
62
+ rspec
63
+ passenger start
64
+
65
+ =================================================================
66
+ Then...
67
+ Got GraphiQL?
68
+ Open GraphiQL
69
+ Enter the URL: http://localhost:3000/
70
+ Enter the query: {Hello}
71
+
72
+ No GraphQL client yet? Simply try from any browser, or with curl:
73
+ http://localhost:3000/?query=%7BHello%7B
74
+
75
+ And then...?
76
+ Generate your own scaffold:
77
+ surikat generate scaffold Author name:string year_of_birth:integer is_any_good:boolean
78
+
79
+ Generate examples of queries for a specific query:
80
+ surikat exemplify Author get
81
+
82
+ Generate your own models:
83
+ surikat generate model Book title:string author_id:integer
84
+
85
+ Generate a User model with full support for authentication, authorization and access:
86
+ surikat generate aaa
87
+
88
+ List existing routes or GraphQL types:
89
+ surikat list routes|types
90
+
91
+ Create a migration:
92
+ rake db:new_migration name=Bookstore address:string name:string
93
+ or
94
+ surikat generate migration AddNumberToBookstore no:integer
95
+
96
+ Run migrations:
97
+ rake db:migrate
98
+
99
+ (More about migrations: https://github.com/thuss/standalone-migrations )
100
+
101
+ Run generated tests:
102
+ rspec
103
+ (or, more interesting, rspec -f d)
104
+
105
+ EOT
106
+ end
107
+
108
+ end
@@ -0,0 +1,67 @@
1
+ # This is an internal utility class used to manage the GraphQL @routes, which are stored in an object dump called 'routes'.
2
+ # The routes map connections between names of queries and mutations, and methods in the _queries modules.
3
+ #
4
+ # A route has the following format:
5
+ # { query_name => {'module' => name of query module,
6
+ # 'method' => name of method in that module,
7
+ # 'output_type' => type to cast over the result of the method},
8
+ # 'arguments' => hash of argument names and GraphQL types
9
+ # }
10
+ #
11
+ # Examples:
12
+ # 'Author' => {module: 'AuthorQueries', method: 'get', output_type: 'Author', 'arguments' => {'id' => 'ID'}},
13
+ # 'Authors' => {module: 'AuthorQueries', method: 'all', output_type: '[Author]', 'arguments' => {}}
14
+ class Routes
15
+
16
+ require 'oj'
17
+
18
+ def initialize
19
+ @filename = "#{FileUtils.pwd}/config/routes.yml"
20
+ @routes = load
21
+ end
22
+
23
+ def all
24
+ @routes
25
+ end
26
+
27
+ def merge_query route
28
+ @routes['queries'] ||= {}
29
+ @routes['queries'].merge!(route)
30
+ save
31
+ end
32
+
33
+ def merge_mutation route
34
+ @routes['mutations'] ||= {}
35
+ @routes['mutations'].merge!(route)
36
+ save
37
+ end
38
+
39
+ def delete_query query_name
40
+ raise "Query #{query_name} not found" if @routes['queries'].nil? || !(@routes['queries']&.keys&.include?(query_name))
41
+ @routes['queries'].delete query_name
42
+ save
43
+ end
44
+
45
+ def delete_mutation mutation_name
46
+ raise "Mutation #{mutation_name} not found" if @routes['mutations'].nil? || !(@routes['mutations']&.keys&.include?(mutation_name))
47
+ @routes['mutations'].delete mutation_name
48
+ save
49
+ end
50
+
51
+ def clear
52
+ @routes = {}
53
+ save
54
+ end
55
+
56
+ private
57
+ def load
58
+ return {} unless File.exists?(@filename)
59
+ YAML.load_file @filename
60
+ end
61
+
62
+ def save
63
+ File.open(@filename, 'w') { |file| file.write(@routes.to_yaml)}
64
+ end
65
+
66
+
67
+ end
@@ -0,0 +1,503 @@
1
+ module Scaffold
2
+
3
+ require 'fileutils'
4
+
5
+ def generate_routes(model_name, is_aaa: false)
6
+ print "Generating query and migration routes... "
7
+
8
+ routes = Routes.new
9
+
10
+ class_name = ActiveSupport::Inflector.camelize(model_name)
11
+
12
+ begin
13
+ # get one query
14
+ routes.merge_query({class_name => {
15
+ 'class' => "#{class_name}Queries",
16
+ 'method' => 'get',
17
+ 'output_type' => "#{class_name}",
18
+ 'arguments' => {'id' => 'ID'}
19
+ }})
20
+
21
+ # get all query
22
+ class_name_plural = ActiveSupport::Inflector.pluralize(class_name)
23
+ routes.merge_query({class_name_plural => {
24
+ 'class' => "#{class_name}Queries",
25
+ 'method' => 'all',
26
+ 'output_type' => "[#{class_name}]",
27
+ 'arguments' => {'q' => 'String'}
28
+ }})
29
+
30
+ # create mutation
31
+ routes.merge_mutation({class_name => {
32
+ 'class' => "#{class_name}Queries",
33
+ 'method' => 'create',
34
+ 'output_type' => "#{class_name}",
35
+ 'arguments' => {class_name => "#{class_name}Input"}
36
+ }})
37
+
38
+ # update mutation
39
+ routes.merge_mutation({"Update#{class_name}" => {
40
+ 'class' => "#{class_name}Queries",
41
+ 'method' => 'update',
42
+ 'output_type' => "Boolean",
43
+ 'arguments' => {class_name => "#{class_name}Input"}
44
+ }})
45
+
46
+ # delete mutation
47
+ routes.merge_mutation({"Delete#{class_name}" => {
48
+ 'class' => "#{class_name}Queries",
49
+ 'method' => 'delete',
50
+ 'output_type' => "Boolean",
51
+ 'arguments' => {'id' => 'ID'}
52
+ }})
53
+
54
+ if is_aaa # add extra routes for AAA
55
+ routes.merge_query({'Authenticate' => {
56
+ "class" => "AAAQueries",
57
+ "method" => "authenticate",
58
+ "output_type" => "Boolean",
59
+ "arguments" => {
60
+ "email" => "String",
61
+ "password" => "String"
62
+ }
63
+ }})
64
+
65
+ routes.merge_query({'Logout' => {
66
+ "class" => "AAAQueries",
67
+ "method" => "logout",
68
+ "output_type" => "Boolean",
69
+ "permitted_roles" => "any"
70
+ }})
71
+
72
+ routes.merge_query({'CurrentUser' => {
73
+ "class" => "AAAQueries",
74
+ "method" => "current_user",
75
+ "output_type" => "User",
76
+ "permitted_roles" => "any"
77
+ }})
78
+
79
+ routes.merge_query({'LoginAs' => {
80
+ "class" => "AAAQueries",
81
+ "method" => "login_as",
82
+ "output_type" => "Boolean",
83
+ "permitted_roles" => ["superadmin"],
84
+ "arguments" => {
85
+ "user_id" => "ID"
86
+ }
87
+ }})
88
+
89
+ routes.merge_query({'BackFromLoginAs' => {
90
+ "class" => "AAAQueries",
91
+ "method" => "back_from_login_as",
92
+ "output_type" => "Boolean",
93
+ "permitted_roles" => "any"
94
+ }})
95
+
96
+ routes.merge_query({'DemoOne' => {
97
+ "class" => "AAAQueries",
98
+ "method" => "demo_one",
99
+ "output_type" => "String",
100
+ "permitted_roles" => "any"
101
+ }})
102
+
103
+ routes.merge_query({'DemoTwo' => {
104
+ "class" => "AAAQueries",
105
+ "method" => "demo_two",
106
+ "output_type" => "String",
107
+ "permitted_roles" => ["hotdog", "hamburger"]
108
+ }})
109
+
110
+ routes.merge_query({'DemoThree' => {
111
+ "class" => "AAAQueries",
112
+ "method" => "demo_three",
113
+ "output_type" => "String",
114
+ "permitted_roles" => ["worker"]
115
+ }})
116
+ end # if is_aaa
117
+ rescue Exception => e
118
+ puts "fail: #{e.message}, #{e.backtrace.first}"
119
+ false
120
+ else
121
+ puts "ok"
122
+ true
123
+ end
124
+ end
125
+
126
+ def destroy_routes(model_name)
127
+ class_name = ActiveSupport::Inflector.camelize(model_name)
128
+ class_name_plural = ActiveSupport::Inflector.pluralize(class_name)
129
+
130
+ routes = Routes.new
131
+
132
+ print "Destroying query and mutation routes... "
133
+
134
+ begin
135
+ routes.delete_query class_name
136
+ routes.delete_query class_name_plural
137
+ routes.delete_mutation class_name
138
+ routes.delete_mutation "Update#{class_name}"
139
+ routes.delete_mutation "Delete#{class_name}"
140
+ rescue Exception => e
141
+ puts "fail: #{e.message}, #{e.backtrace.first}"
142
+ false
143
+ else
144
+ puts "ok"
145
+ true
146
+ end
147
+ end
148
+
149
+ def generate_queries(model_name)
150
+ var_name = ActiveSupport::Inflector.singularize(model_name.underscore)
151
+ class_name = ActiveSupport::Inflector.camelize(model_name)
152
+ class_name_plural = ActiveSupport::Inflector.pluralize(class_name)
153
+ file_name = ActiveSupport::Inflector.singularize(model_name.underscore) + '_queries.rb'
154
+ file_path = "queries/#{file_name}"
155
+ absolute_path = FileUtils.pwd + '/app/'
156
+ file_absolute_path = absolute_path + file_path
157
+
158
+ FileUtils.mkdir_p absolute_path + 'models'
159
+
160
+ print "Generating query file #{file_path}... "
161
+
162
+ if File.exists?(file_absolute_path)
163
+ puts "exists"
164
+ return true
165
+ end
166
+
167
+ all_types = Types.new.all
168
+
169
+ input_type_detailed = all_types["#{class_name}Input"]['arguments'].map do |field, type|
170
+ " '#{field}' => #{type}"
171
+ end.join("\n")
172
+
173
+ input_type_detailed_no_id = all_types["#{class_name}Input"]['arguments'].map do |field, type|
174
+ next if field == 'id'
175
+ " '#{field}' => #{type}"
176
+ end.compact.join("\n")
177
+
178
+ fields = all_types[class_name]['fields'].keys
179
+
180
+ examples = {
181
+ get: "{\n #{class_name}(id: 123) {\n" + fields.map {|f| " #{f}"}.join("\n") + "\n }\n}",
182
+ list: "{\n #{class_name_plural}(q: \"id_lt=100\") {\n" + fields.map {|f| " #{f}"}.join("\n") + "\n }\n}",
183
+ create: "mutation #{class_name}($#{var_name}: #{class_name}Input) {\n #{class_name}(#{var_name}: $#{var_name}) {\n" + fields.map {|f| " #{f}"}.join("\n") + "\n }\n}",
184
+ update: "mutation Update#{class_name}($#{var_name}: #{class_name}Input) {\n Update#{class_name}(#{var_name}: $#{var_name}) {\n" + fields.map {|f| " #{f}"}.join("\n") + "\n }\n}",
185
+ delete: "mutation Delete#{class_name}($id: ID) {\n Delete#{class_name}(id: $id)\n}"
186
+ }
187
+
188
+ update_vars = {var_name => {}}
189
+ all_types["#{class_name}Input"]['arguments'].each {|a, t| update_vars[var_name][a] = random_values(t)}
190
+
191
+ create_vars = {var_name => {}}
192
+ all_types["#{class_name}Input"]['arguments'].each {|a, t| create_vars[var_name][a] = random_values(t)}
193
+ create_vars[var_name].delete 'id'
194
+
195
+ vars = {
196
+ examples: examples,
197
+ time: Time.now.to_s,
198
+ class_name_downcase: class_name.underscore,
199
+ class_name: class_name,
200
+ class_name_plural: class_name_plural,
201
+ input_type_detailed: input_type_detailed,
202
+ pretty_update_vars: JSON.pretty_generate(update_vars),
203
+ pretty_create_vars: JSON.pretty_generate(create_vars),
204
+ pretty_random_id: JSON.pretty_generate({'id' => random_values('ID')}),
205
+ input_type_detailed_no_id: input_type_detailed_no_id,
206
+ examples_get: examples[:get],
207
+ examples_list: examples[:list],
208
+ examples_create: examples[:create],
209
+ examples_update: examples[:update],
210
+ examples_delete: examples[:delete]
211
+ }
212
+
213
+ copy_template 'crud_queries.rb', {new_name: file_name, path: 'app/queries', vars: vars}
214
+
215
+ puts "ok"
216
+ true
217
+ end
218
+
219
+ def generate_aaa_queries
220
+ file_name = 'aaa_queries.rb'
221
+ file_path = "app/queries/#{file_name}"
222
+ absolute_path = FileUtils.pwd + '/app/queries/'
223
+ file_absolute_path = absolute_path + file_path
224
+
225
+ vars = {
226
+ time: Time.now.to_s
227
+ }
228
+
229
+ print "Creating AAA queries file: #{file_name}... "
230
+
231
+ if File.exists?(file_absolute_path)
232
+ puts "exists"
233
+ return true
234
+ end
235
+
236
+ copy_template file_name, {path: 'app/queries', vars: vars}
237
+
238
+ puts "ok"
239
+ true
240
+ end
241
+
242
+ def destroy_queries(model_name)
243
+ file_name = ActiveSupport::Inflector.singularize(model_name.underscore) + '_queries.rb'
244
+ file_path = "queries/#{file_name}"
245
+
246
+ print "Deleting queries file: #{file_path}... "
247
+
248
+ begin
249
+ File.unlink FileUtils.pwd + '/app/' + file_path
250
+ rescue Exception => e
251
+ puts "fail: #{e.message}"
252
+ false
253
+ else
254
+ puts "ok"
255
+ true
256
+ end
257
+ end
258
+
259
+ def generate_types(model_name, arguments, is_aaa: false)
260
+ type_name = ActiveSupport::Inflector.camelize(model_name)
261
+ types = Types.new
262
+
263
+ fields = {}
264
+ (arguments + ['id:ID']).each do |arg|
265
+ field_name, field_type = arg.split(':').map(&:strip)
266
+
267
+ next if field_name.in?(%w(password hashed_password)) && is_aaa # never expose the hashed password
268
+
269
+ field_type = 'ID' if field_name[-3, 3] == '_id'
270
+
271
+ graphql_type = case field_type
272
+ when 'ID'
273
+ 'ID'
274
+ when 'integer'
275
+ 'Int'
276
+ when 'float'
277
+ 'Float'
278
+ when 'string', 'text'
279
+ 'String'
280
+ when 'boolean'
281
+ 'Boolean'
282
+ else
283
+ 'String'
284
+ end
285
+
286
+ fields[field_name] = graphql_type
287
+ end
288
+
289
+ print "Generating output type... "
290
+
291
+ if types.all.keys.include?(type_name)
292
+ puts "exists"
293
+ else
294
+ begin
295
+ types.merge type_name => {
296
+ 'type' => 'Output',
297
+ 'fields' => fields.clone
298
+ }
299
+ rescue Exception => e
300
+ puts "fail: #{e.message}, #{e.backtrace.first}"
301
+ return false
302
+ else
303
+ puts "ok"
304
+ end
305
+ end
306
+
307
+ print "Generating input type... "
308
+
309
+ if types.all.keys.include?("#{type_name}Input")
310
+ puts "exists"
311
+ true
312
+ else
313
+ input_fields = fields.clone
314
+
315
+ if is_aaa
316
+ input_fields.merge!({'password' => 'String'})
317
+ input_fields.delete('hashed_password')
318
+ end
319
+
320
+ begin
321
+ types.merge "#{type_name}Input" => {
322
+ 'type' => 'Input',
323
+ 'arguments' => input_fields
324
+ }
325
+ rescue Exception => e
326
+ puts "fail: #{e.message}, #{e.backtrace.first}"
327
+ false
328
+ else
329
+ puts "ok"
330
+ true
331
+ end
332
+ end
333
+ end
334
+
335
+ def destroy_types(model_name)
336
+ print "Deleting types... "
337
+
338
+ begin
339
+ type_name = ActiveSupport::Inflector.camelize(model_name)
340
+ types = Types.new
341
+
342
+ types.delete(type_name)
343
+ types.delete("#{type_name}Input")
344
+ rescue Exception => e
345
+ puts "fail: #{e.message}, #{e.backtrace.first}"
346
+ false
347
+ else
348
+ puts "ok"
349
+ true
350
+ end
351
+ end
352
+
353
+ def make_create_migration(migration_name, arguments)
354
+ print "Creating migration #{migration_name}... "
355
+
356
+ arguments << 'created_at:datetime'
357
+ arguments << 'updated_at:datetime'
358
+
359
+ begin
360
+ StandaloneMigrations::Generator.migration migration_name, arguments
361
+ rescue Exception => e
362
+ puts " error: #{e.message}"
363
+ return false
364
+ else
365
+ puts "ok"
366
+ true
367
+ end
368
+ end
369
+
370
+ def generate_model(model_name, arguments, is_aaa: false)
371
+ class_name = ActiveSupport::Inflector.camelize(model_name)
372
+ file_name = ActiveSupport::Inflector.singularize(model_name.underscore) + '.rb'
373
+ file_path = "models/#{file_name}"
374
+ absolute_path = FileUtils.pwd + '/app/'
375
+ file_absolute_path = absolute_path + file_path
376
+
377
+ return false unless make_create_migration("create_#{model_name}", arguments)
378
+
379
+ FileUtils.mkdir_p absolute_path + 'models'
380
+
381
+ print "Creating model file: #{file_name}... "
382
+
383
+ if File.exists?(file_absolute_path)
384
+ puts "exists"
385
+ return true
386
+ end
387
+
388
+ template = is_aaa ? 'base_aaa_model.rb' : 'base_model.rb'
389
+
390
+ copy_template template, {new_name: file_name, path: 'app/models', vars: {class_name: class_name}}
391
+
392
+ puts "ok"
393
+ return true
394
+ end
395
+
396
+ def generate_tests(model_name, arguments, is_aaa: false)
397
+
398
+ class_name = ActiveSupport::Inflector.camelize(model_name)
399
+ class_name_plural = ActiveSupport::Inflector.pluralize(class_name)
400
+ model_name_plural = ActiveSupport::Inflector.pluralize(model_name.underscore)
401
+ file_name = ActiveSupport::Inflector.singularize(model_name.underscore) + '_spec.rb'
402
+ file_path = "spec/#{file_name}"
403
+ absolute_path = FileUtils.pwd + '/app/'
404
+ file_absolute_path = absolute_path + file_path
405
+ columns = (arguments.to_a.map {|a| a.split(':').first} + %w(id created_at updated_at)).uniq
406
+
407
+ columns -= ['hashed_password'] if is_aaa
408
+
409
+ print "Creating rspec tests: #{file_name}... "
410
+
411
+ if File.exists?(file_absolute_path)
412
+ puts "exists"
413
+ return true
414
+ end
415
+
416
+ copy_template 'base_spec.rb', {
417
+ new_name: file_name,
418
+ path: 'spec',
419
+ vars: {class_name: class_name, class_name_plural: class_name_plural,
420
+ model_name: model_name.underscore, model_name_plural: model_name_plural,
421
+ columns_new_line: columns.join("\n "),
422
+ columns_space: columns.map {|c| "'#{c}'"}.join(', ')
423
+ }
424
+ }
425
+
426
+ puts "ok"
427
+
428
+ if is_aaa
429
+ print "Creating AAA rspec tests: spec/aaa_spec.rb... "
430
+ copy_template 'aaa_spec.rb', {path: 'spec'}
431
+ puts "ok"
432
+ end
433
+
434
+ true
435
+ end
436
+
437
+ def generate_scaffold arguments
438
+ model_name = arguments.shift
439
+ if model_name.to_s.empty?
440
+ puts "Syntax: surikat generate scaffold model_name field1:type1 field2:type2..."
441
+ return
442
+ end
443
+
444
+ unless model_name =~ /^[\p{L}_][\p{L}\p{N}@$#_]{0,127}$/
445
+ puts "'#{model_name}' does not appear to be a valid."
446
+ return
447
+ end
448
+
449
+ valid_types = %w(integer float string boolean date datetime decimal binary bigint primary_key references string text time timestamp)
450
+
451
+ arguments.each do |arg|
452
+ field_name, field_type = arg.split(':')
453
+ unless field_name =~ /^[\p{L}_][\p{L}\p{N}@$#_]{0,127}$/
454
+ puts "'#{field_name} does not appear to be valid'"
455
+ return
456
+ end
457
+ unless valid_types.include?(field_type)
458
+ puts "'#{field_type}' does not appear to be valid. Valid field types are: #{valid_types.join(', ')}"
459
+ end
460
+ end
461
+
462
+ if generate_model(model_name, arguments) &&
463
+ generate_types(model_name, arguments) &&
464
+ generate_queries(model_name) &&
465
+ generate_routes(model_name) &&
466
+ generate_tests(model_name, arguments)
467
+ puts "Done."
468
+ else
469
+ puts "Partially done."
470
+ end
471
+
472
+ end
473
+
474
+ def generate_aaa
475
+ model_name = 'user'
476
+ arguments = ['email:string', 'hashed_password:string', 'roleids:string']
477
+
478
+ if generate_model(model_name, arguments, is_aaa: true) &&
479
+ generate_types(model_name, arguments, is_aaa: true) &&
480
+ generate_queries(model_name) &&
481
+ generate_aaa_queries &&
482
+ generate_routes(model_name, is_aaa: true)
483
+ generate_tests(model_name, arguments, is_aaa: true)
484
+
485
+ puts "Done."
486
+ else
487
+ puts "Partially done."
488
+ end
489
+ end
490
+
491
+ def copy_template name, details
492
+ destination_path = details[:path]
493
+ vars = details[:vars] || {}
494
+ file = "#{__dir__}/templates/#{name}.tmpl"
495
+ dest = "#{destination_path}/#{details[:new_name] || name}"
496
+
497
+ text = File.open(file).read
498
+
499
+ File.open(dest, 'w') {|f| f.write(text % vars)}
500
+ end
501
+
502
+
503
+ end