railspp 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 12d4c74ed0ac4e2d6342bcdd8319d96e078c162a76aa422de2fab510e1bce423
4
+ data.tar.gz: c121223b43c6c248fa8066d8311a40526979729282f546764bbb36f51d9d03cd
5
+ SHA512:
6
+ metadata.gz: a99075325a3cbeaa265eca334eb94b7827c572b129d5f1e49de13034535e5af35f450f795b0369a31924dfa743d7e343eaa1de093dae8706db44ae5f068fe5e3
7
+ data.tar.gz: beb144dee9e25d7876ca01f66a9e966f304b27de13f1de50f935d44c51ca3ab04af085983d8c19f284b000479f9652a1ad90ace189c8dd7ed89f86a93d9f2b81
data/README.md ADDED
@@ -0,0 +1,168 @@
1
+ # Rails++
2
+
3
+ Autogenerate your CRUD operations with Swagger like API documentation
4
+
5
+ ## Requirements
6
+
7
+ Rails version >= 5
8
+ Ruby version >= 2.5
9
+
10
+ ## Installation
11
+
12
+ In your gem file
13
+
14
+ ```ruby
15
+ gem 'railspp'
16
+ ```
17
+
18
+ In your terminal
19
+
20
+ ```bash
21
+ gem isntall railspp
22
+ ```
23
+
24
+ ## CLI Commands
25
+
26
+ Your command CLI command bin path is `railspp`
27
+
28
+ <!-- ![CLI DOCUMENTATION](./docs/command_list.png) -->
29
+
30
+ ## Getting Started
31
+
32
+ For existing project use Git and keep all the
33
+ changes from the initialize method while keeping your changes as well.
34
+
35
+ Example:
36
+ Create a new project and then initialize.
37
+
38
+ ```bash
39
+ rails new (project-name)
40
+ cd (project-name)
41
+ # Install dependency in gem file and globally
42
+ railspp init
43
+ ```
44
+
45
+ Run the initialize command to get started:
46
+
47
+ ```bash
48
+ railspp init
49
+ ```
50
+
51
+ Run the model command to generate a migration, model, and completed controller:
52
+
53
+ This controller is overwritable when you declare the same method name in it's controller class.
54
+
55
+ ```bash
56
+ railspp model (model-name)
57
+ ```
58
+
59
+ Update your migration and model file. Then declare your routes with `resources`.
60
+ The controller has all api resources completed by default. These will fail if your migration and
61
+ model are not completed. And your routes are not defined.
62
+
63
+ Steps to do after:
64
+
65
+ - Update model file
66
+ - Update migration file
67
+ - Define your routes with resources
68
+
69
+ Run the make_test command to make unit tests:
70
+
71
+ ```bash
72
+ railspp make_test (resource-route-name)
73
+ ```
74
+
75
+ After your tests are completed. Update your update and create request bodies
76
+ based on the controller's `params.permit`. Add the keys available to your request body.
77
+ If you have special headers like "Authorization" add those to the public
78
+ variable `@headers` in that file.
79
+
80
+ Steps to do after:
81
+
82
+ - Update request body based on the controller's permitted keys
83
+ - Update your headers for all the requests
84
+
85
+ ```bash
86
+
87
+ ```
88
+
89
+ ## API Documentation
90
+
91
+ There are Swagger-like API Documentation that requires no configuration.
92
+ This was created on your initialize command. This docs with regenerate the javascript
93
+ on API route changes.
94
+
95
+ The API documentation lives on your web route '/documentation'
96
+ Run the server and listen to url:
97
+
98
+ `http://localhost:3000/documentation`
99
+
100
+ For more information on the API docs check out the details here: [Autogenerated API Documentation Docs](./docs/API_DOCUMENTATION.md)
101
+
102
+ ## Gloabl Controller
103
+
104
+ All of your CRUD operations were created in the GlobalController.
105
+ The GlobalController gets your model based on the class name of the controller.
106
+ Do not change the class name of the generated controller.
107
+
108
+ All methods are overwritable but have the functionality complete by default.
109
+ Restrict the routes you want accessible through the apiResource declaration.
110
+ Update the tests for ignoring the routes that do not exist.
111
+
112
+ By Default these methods are created:
113
+
114
+ - index
115
+ - show
116
+ - store
117
+ - update
118
+ - destroy
119
+
120
+ There are many querystrings available in the index method.
121
+ These contain options like:
122
+
123
+ - include=(associated model names comma separated)
124
+ - where=(key:value rows comma separated and key value pairs are semi-colon separated)
125
+ - order=(key:(DESC||ASC) rows comma separated and key DESC/ASC pairs semi-colon separated)
126
+ - limit=(amount returned the default is 25)
127
+ - offset=(number_to_skip in rows)
128
+ - page=(which page the default is 1)
129
+
130
+ For more information about the Global Controller check out the details here: [Global Controller Docs](./docs/GLOBAL_CONTROLLER.md)
131
+
132
+ ## Unit tests
133
+
134
+ By default the tests check the format of the response and the status code.
135
+ You must provide request params for your store method and update method based
136
+ on the params options in your model. They are at the top of the file with TODOS.
137
+ There are tests for the querystrings. That you can update. The include
138
+ querystring test must be updated based on your schema.
139
+
140
+ The tests cover all querystrings with the same successful data format as default index currently.
141
+
142
+ Test cover:
143
+
144
+ Index:
145
+
146
+ - status_code = 200
147
+ - json_format = { data: (array), count: (integer) }
148
+
149
+ Show:
150
+
151
+ - status_code = 200
152
+ - json_format = { id: (integer) }
153
+
154
+ Create:
155
+
156
+ - status_code = 201
157
+ - json_format = { id: (integer) }
158
+
159
+ Update:
160
+
161
+ - status_code = 200
162
+ - json_format = { id: (integer) }
163
+
164
+ Destroy:
165
+
166
+ - status_code = 204
167
+
168
+ For more information about the Unit Tests check out the details here: [Generated Unit Test Docs](./docs/RAILSUNIT.md)
data/bin/railspp ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'railspp'
4
+
5
+ if RailsPlusPlus.check_directory
6
+ RailsPlusPlus.run_command(ARGV)
7
+ else
8
+ puts RailsPlusPlus.error_string
9
+ end
@@ -0,0 +1,67 @@
1
+ require_relative '../utils/strings.rb'
2
+
3
+
4
+ class InitializeCommand < MoreUtils
5
+ class << self
6
+
7
+ def run *args
8
+ lookup = flag_lookup(args)
9
+
10
+ # Add Initializers
11
+ cors_template = get_file_str("#{this_dir}/../templates/rack_cors_initializer.txt")
12
+ write_file("#{root}/config/initializers/rack_cors.rb", cors_template)
13
+
14
+ adi_template = get_file_str("#{this_dir}/../templates/api_documentation_initializer.txt")
15
+ write_file("#{root}/config/initializers/api_documentation_js.rb", adi_template)
16
+
17
+ # Add Controllers
18
+ dc_template = get_file_str("#{this_dir}/../templates/documentation_controller.txt")
19
+ write_file("#{root}/app/controllers/documentation_controller.rb", dc_template)
20
+
21
+ gc_template = get_file_str("#{this_dir}/../templates/global_controller.txt")
22
+ write_file("#{root}/app/controllers/global_controller.rb", gc_template)
23
+
24
+ # Add Concerns
25
+ ehc_template = get_file_str("#{this_dir}/../templates/exception_handler.txt")
26
+ write_file("#{root}/app/controllers/concerns/exception_handler.rb", ehc_template)
27
+
28
+ resp_template = get_file_str("#{this_dir}/../templates/response.txt")
29
+ write_file("#{root}/app/controllers/concerns/response.rb", resp_template)
30
+
31
+ # Add Service
32
+ system("mkdir -p #{root}/app/services")
33
+ aps_template = get_file_str("#{this_dir}/../templates/api_documentation_service.txt")
34
+ write_file("#{root}/app/services/api_documentation_service.rb", aps_template)
35
+
36
+ # Add Views
37
+ system("mkdir -p #{root}/app/views/documentation")
38
+ dihtml_template = get_file_str("#{this_dir}/../templates/documentation.index.erb.txt")
39
+ write_file("#{root}/app/views/documentation/index.html.erb", dihtml_template)
40
+
41
+ dlhtml_template = get_file_str("#{this_dir}/../templates/documentation.layout.erb.txt")
42
+ write_file("#{root}/app/views/layouts/documentation.html.erb", dlhtml_template)
43
+
44
+ # Update Routes
45
+ unless lookup[:"skip-routes"]
46
+ routes_template = get_file_str("#{this_dir}/../templates/routes_documentation.txt")
47
+ routes_file = get_file_str("#{root}/config/routes.rb")
48
+ routes_arr = routes_file.split("\n")
49
+ last_end_line = last_end_index(routes_arr)
50
+
51
+ new_routes = routes_arr.slice(0, last_end_line).join("\n") + "\n#{routes_template}\nend\n"
52
+ write_file("#{root}/config/routes.rb", new_routes)
53
+ end
54
+
55
+ puts "Your project has been initialized."
56
+ end
57
+
58
+ def last_end_index arr
59
+ arr.each_with_index.inject(0) do |acc, (e, i)|
60
+ acc = i if /(end)/.match(e)
61
+ acc
62
+ end
63
+ end
64
+
65
+ end
66
+
67
+ end
@@ -0,0 +1,12 @@
1
+ require_relative '../utils/strings.rb'
2
+
3
+
4
+ class MakeTestCommand < MoreUtils
5
+ class << self
6
+
7
+ def run *args
8
+
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,36 @@
1
+ require_relative '../utils/strings.rb'
2
+
3
+
4
+ class ModelCommand < MoreUtils
5
+ class << self
6
+
7
+ def run *args
8
+ lookup = flag_lookup(args)
9
+ arguments = get_args(args)
10
+
11
+ if arguments.length < 2
12
+ puts "Enter a valid model generation command."
13
+ return
14
+ end
15
+
16
+ model_name = arguments[0].camelcase
17
+ others = arguments[1..-1]
18
+
19
+ system("rails generate model #{model_name} #{others.join(' ')}")
20
+
21
+ api_version_path = lookup[:"api-version"] || 'api/v1'
22
+ system("mkdir -p #{root}/app/controllers/#{api_version_path}")
23
+
24
+ controller_prefix = api_version_path.split('/').map { |e| e.downcase.capitalize }.join('::')
25
+ controller_name = controller_prefix + '::' + model_name
26
+
27
+ controller_temp = get_file_str("#{this_dir}/../templates/controller.txt")
28
+ controller_regex = '{{ CONTROLLER_NAME }}'
29
+ controller_str = controller_temp.gsub(controller_regex, controller_name)
30
+ write_file("#{root}/app/controllers/#{api_version_path}/#{model_name.underscore}_controller.rb", controller_str)
31
+
32
+ puts "#{model_name} model, migration, and controller has been generated."
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,30 @@
1
+ require_relative '../utils/strings.rb'
2
+
3
+ class DocumentationHelpCommand < MoreUtils
4
+ class << self
5
+
6
+ def run descriptions
7
+ puts "#{ascii_art}
8
+ Rails Plus Plus Version: #{gem_version}
9
+
10
+ Rails Plus Plus: Command Line Interface to make your life easier.
11
+ => The Rails Plus Plus command is 'railspp'. To blast this project into the fifth dimension.
12
+ => Use '--help' on any of the commands listed below for more details.
13
+
14
+ List of commands:
15
+ #{descriptions}
16
+ "
17
+ end
18
+
19
+ def ascii_art
20
+ "______ _ __
21
+ | ___ \\ (_) | _ _
22
+ | |_/ /__ _ _| |___ _| |_ _| |_
23
+ | // _` | | / __|_ _|_ _|
24
+ | |\\ \\ (_| | | \\__ \\ |_| |_|
25
+ \\_| \\_\\__,_|_|_|___/
26
+ "
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,21 @@
1
+ class InitializeHelpCommand
2
+ class << self
3
+
4
+ def run *args
5
+ puts "Options:
6
+
7
+ You have the availability to skip portions of the
8
+ initialize command.
9
+
10
+ If you would like to skip the initial route nmespace use:
11
+
12
+ --skip-routes
13
+
14
+ Run to initialize your project:
15
+ 'railspp initialize'
16
+ "
17
+ end
18
+
19
+ end
20
+ end
21
+
@@ -0,0 +1,13 @@
1
+ class MakeTestHelpCommand
2
+ class << self
3
+
4
+ def run *args
5
+ puts "No Options available.
6
+
7
+ Run to generate railspp tests your generated controllers:
8
+ 'railspp make_test (resource-prefix)'
9
+ "
10
+ end
11
+
12
+ end
13
+ end
data/lib/help/model.rb ADDED
@@ -0,0 +1,30 @@
1
+ class ModelHelpCommand
2
+ class << self
3
+
4
+ def run
5
+ puts "Options:
6
+ You have the abliity to enter an api-version through namespaces.
7
+ You can generate your controller in your nmespace directory.
8
+ By default the value is 'api/v1'
9
+
10
+ Enter your namespace directory-path from controllers directory without
11
+ a starting '/' in this option.
12
+
13
+ --api-version=(namespace-directory-path-from-controllers-directory)
14
+
15
+ Example:
16
+
17
+ --api-version=api/v2
18
+
19
+ Same command as:
20
+ rails generate model <model-name>
21
+
22
+ With your generated controller
23
+
24
+ Run to generate your model, migration, and controller:
25
+ 'railspp model (model-name) (models-options) (your-api-version-flag)'
26
+ "
27
+ end
28
+
29
+ end
30
+ end
data/lib/railspp.rb ADDED
@@ -0,0 +1,78 @@
1
+ require_relative './commands/initialize.rb'
2
+ require_relative './commands/make_test.rb'
3
+ require_relative './commands/model.rb'
4
+ require_relative './help/documentation.rb'
5
+ require_relative './help/initialize.rb'
6
+ require_relative './help/make_test.rb'
7
+ require_relative './help/model.rb'
8
+ require_relative './utils/strings.rb'
9
+
10
+
11
+ class RailsPlusPlus < MoreUtils
12
+ class << self
13
+
14
+ def error_string
15
+ 'ERROR: Must make railspp commands in the rails root directory'
16
+ end
17
+
18
+ def check_directory
19
+ File.file?(root + '/bin/rails')
20
+ end
21
+
22
+ def run_command arguments
23
+
24
+ is_help = arguments.select { |e| e == '--help' || e == '-h' }.length > 0
25
+ command_name = arguments[0]
26
+ passable_args = arguments[1..-1]
27
+ lookup = is_help ? command_name_help_lookup : command_name_lookup
28
+ command_exists = !command_name.nil? && !lookup[command_name.to_sym].nil?
29
+
30
+ if command_exists
31
+ command_class = lookup[command_name.to_sym]
32
+ command_class.run(*passable_args)
33
+ else
34
+ DocumentationHelpCommand.run(descriptions.join("\n"))
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def command_name_lookup
41
+ {
42
+ i: InitializeCommand,
43
+ init: InitializeCommand,
44
+ initialize: InitializeCommand,
45
+ m: ModelCommand,
46
+ model: ModelCommand,
47
+ make_test: MakeTestCommand,
48
+ mt: MakeTestCommand,
49
+ }
50
+ end
51
+
52
+ def command_name_help_lookup
53
+ {
54
+ i: InitializeHelpCommand,
55
+ init: InitializeHelpCommand,
56
+ initialize: InitializeHelpCommand,
57
+ m: ModelHelpCommand,
58
+ model: ModelHelpCommand,
59
+ make_test: MakeTestHelpCommand,
60
+ mt: MakeTestHelpCommand,
61
+ }
62
+ end
63
+
64
+ def descriptions
65
+ [
66
+ '- i => Initialize your project',
67
+ '- init => Initialize your project',
68
+ '- initialize => Initialize your project',
69
+ '- m => Generate your CRUD model, controller, and migration',
70
+ '- model => Generate your CRUD model, controller, and migration',
71
+ '- make_test => Generate your resource unit test',
72
+ '- mt => Generate your resource unit test'
73
+ ]
74
+ end
75
+
76
+ end
77
+ end
78
+
@@ -0,0 +1,4 @@
1
+ Rails.application.config.after_initialize do
2
+ Rails.application.reload_routes!
3
+ APIDocumentationService.generate_js_file()
4
+ end
@@ -0,0 +1,206 @@
1
+ class APIDocumentationService
2
+ class << self
3
+ def generate_js_file
4
+ all_routes = get_api_routes
5
+ documentation_js_str = all_routes.map { |e| handle_js_for_route(e) }.join("\n")
6
+
7
+ File.open(output_path, "w+") { |f| f.write(documentation_js_str) }
8
+ end
9
+
10
+ def namespaces
11
+ # Blacklist namespaces
12
+ [:api]
13
+ end
14
+
15
+ def docs_namespaces
16
+ namespaces.each_with_object({}) do |item, acc|
17
+ acc[item] = true
18
+ acc
19
+ end
20
+ end
21
+
22
+ def get_api_routes
23
+ get_all_routes.select do |e|
24
+ path_array = e[:route].split('/')
25
+ path_array.length > 1 && docs_namespaces[path_array[1].to_sym]
26
+ end
27
+ end
28
+
29
+ def camelize_route route
30
+ camel_route = route[:route].split('/').reject { |e| e == '' }.map { |e| /\:/.match(e) ? e.gsub!(':', '') + 'Param' : e }.map { |e| e.capitalize }.join('')
31
+ route[:method].downcase + camel_route
32
+ end
33
+
34
+ private
35
+
36
+ def output_path
37
+ __dir__ + '/../../app/assets/javascripts/documentation.js'
38
+ end
39
+
40
+ def get_all_routes
41
+ Rails.application.routes.routes.map { |e| { route: e.path.spec.to_s.split('(')[0], method: e.verb } }.map { |e| e[:route] && e[:method] ? e.merge(camel_cased: camelize_route(e)) : e }.select { |e| e[:route] }
42
+ end
43
+
44
+ def handle_js_for_route route
45
+ camel_cased = route[:camel_cased]
46
+ javascript_string = ''
47
+ javascript_string += "window.#{camel_cased} = function() {
48
+ var allData = {
49
+ method: '#{route[:method]}',
50
+ route: '#{route[:route]}',
51
+ };
52
+ var paramList = document.getElementById('#{camel_cased}ParamsForm') && document.getElementById('#{camel_cased}ParamsForm').elements ? document.getElementById('#{camel_cased}ParamsForm').elements : [];
53
+ var headerList = document.getElementById('#{camel_cased}HeadersForm') && document.getElementById('#{camel_cased}HeadersForm').elements ? document.getElementById('#{camel_cased}HeadersForm').elements : [];
54
+
55
+ var headerObject = {};
56
+ var tempHeaderKey = null;
57
+ for (var i = 0; i < headerList.length; i++) {
58
+ var eleHeader = headerList[i].value;
59
+ if (i % 2 !== 0 && tempHeaderKey) {
60
+ headerObject[tempHeaderKey] = eleHeader;
61
+ tempHeaderKey = null;
62
+ } else {
63
+ tempHeaderKey = eleHeader;
64
+ }
65
+ }
66
+
67
+ var hasHeaders = Object.keys(headerObject).length > 0;
68
+ var headers = { headers: headerObject };
69
+
70
+ var paramObject = {};
71
+ var tempParamKey = null;
72
+ for (var i = 0; i < paramList.length; i++) {
73
+ var eleParam = paramList[i].value;
74
+ if (i % 2 !== 0) {
75
+ paramObject[':' + tempParamKey] = eleParam;
76
+ tempParamKey = null;
77
+ } else {
78
+ tempParamKey = eleParam;
79
+ }
80
+ }
81
+
82
+ var routeName = allData.route.split('/').map(function(e) { return paramObject[e] ? paramObject[e] : e; }).join('/');
83
+
84
+ if (allData.method !== 'GET') {
85
+ var bodyDataType = document.getElementById('#{camel_cased}DataType') && document.getElementById('#{camel_cased}DataType').value ? document.getElementById('#{camel_cased}DataType').value : false;
86
+ var formBoolean = bodyDataType === 'Form Data';
87
+
88
+ var bodyElements = [];
89
+ var bodyRawElements = document.getElementById('#{camel_cased}BodyForm') && document.getElementById('#{camel_cased}BodyForm').elements ? document.getElementById('#{camel_cased}BodyForm').elements : [];
90
+ for (var i = 0; i < bodyRawElements.length; i++) {
91
+ var eleParam = bodyRawElements[i].files ? bodyRawElements[i].files[0] : (bodyRawElements[i].value || null);
92
+ bodyElements.push(eleParam);
93
+ }
94
+ bodyElements = bodyElements.filter(function(e) {
95
+ return !!e;
96
+ });
97
+
98
+ var bodyObject = bodyDataType === 'Form Data' ? new FormData() : {};
99
+ var tempBodyKey = null;
100
+ bodyElements.forEach(function(e, i) {
101
+ if (i % 2 !== 0) {
102
+ if (formBoolean) {
103
+ bodyObject.append(tempBodyKey, e);
104
+ tempBodyKey = null;
105
+ } else {
106
+ bodyObject[tempBodyKey] = e;
107
+ tempBodyKey = null;
108
+ }
109
+ } else {
110
+ if (formBoolean) {
111
+ tempBodyKey = e;
112
+ } else {
113
+ bodyObject[e] = null;
114
+ tempBodyKey = e;
115
+ }
116
+ }
117
+ });
118
+ }
119
+
120
+ var qsElements = [];
121
+ var qsRawElements = document.getElementById('#{camel_cased}QSForm') && document.getElementById('#{camel_cased}QSForm').elements ? document.getElementById('#{camel_cased}QSForm').elements : [];
122
+ for (var i = 0; i < qsRawElements.length; i++) {
123
+ var eleParam = qsRawElements[i].value || null;
124
+ qsElements.push(eleParam);
125
+ }
126
+
127
+ qsElements = qsElements.filter(function(e) { return !!e; });
128
+
129
+ var qsObject = {};
130
+ var tempQSKey = null;
131
+ qsElements.forEach(function(e, i) {
132
+ if (i % 2 !== 0) {
133
+ qsObject[tempQSKey] = e;
134
+ tempQSKey = null;
135
+ } else {
136
+ qsObject[e] = null;
137
+ tempQSKey = e;
138
+ }
139
+ });
140
+
141
+ var qsLength = Object.keys(qsObject).length;
142
+ var querystring = qsLength > 0 ? '?' : '';
143
+
144
+ var qsCount = 0;
145
+ var qsArray = [];
146
+ if (querystring === '?') {
147
+ for (var qs in qsObject) {
148
+ if (qs && qsObject[qs]) {
149
+ qsArray.push(qs + '=' + qsObject[qs]);
150
+ }
151
+ }
152
+ }
153
+
154
+ querystring += qsArray.join('&');
155
+
156
+ var args = allData.method === 'GET' || allData.method === 'DELETE' ? [routeName + querystring, headers] : [routeName + querystring, bodyObject, headers];
157
+ if (!hasHeaders) args.pop();
158
+ var resultElement = document.getElementById('#{camel_cased}-results');
159
+
160
+ axios[allData.method.toLowerCase()](...args)
161
+ .then(function(resp) {
162
+ if (resp.status <= 300) {
163
+ resultElement.innerText = JSON.stringify(resp.data, null, 4);
164
+ } else {
165
+ resultElement.innerText = JSON.stringify(resp.data, null, 4);
166
+ }
167
+ })
168
+ .catch(function(err) {
169
+ var error_ajax = err && err.response && err.response.data ? err.response.data : err;
170
+ resultElement.innerText = JSON.stringify(error_ajax, null, 4);
171
+ });
172
+ };
173
+ "
174
+
175
+ javascript_string += "window.#{camel_cased}NewBody = function() {
176
+ var ele = document.getElementById('#{camel_cased}BodyForm');
177
+ ele.innerHTML += '<div class=\"d-flex f-row\"><input class=\"w-100 m-1 form-control\" type=\"text\" placeholder=\"Enter key\"><input class=\"w-100 m-1 form-control\" type=\"text\" placeholder=\"Enter value\"></div>';
178
+ };
179
+
180
+ "
181
+
182
+ javascript_string += "window.#{camel_cased}NewBodyFile = function() {
183
+ var ele = document.getElementById('#{camel_cased}BodyForm');
184
+ ele.innerHTML += '<div class=\"d-flex f-row\"><input class=\"w-100 m-1 form-control\" type=\"text\" placeholder=\"Enter key\"><input class=\"w-100 m-1 form-control\" type=\"file\" multiple accept=\"*/*\" placeholder=\"Enter value\"></div>';
185
+ };
186
+
187
+ "
188
+
189
+ javascript_string += "window.#{camel_cased}NewQS = function() {
190
+ var ele = document.getElementById('#{camel_cased}QSForm');
191
+ ele.innerHTML += '<div class=\"d-flex f-row\"><input class=\"w-100 m-1 form-control\" type=\"text\" placeholder=\"Enter key\"><input class=\"w-100 m-1 form-control\" type=\"text\" placeholder=\"Enter value\"></div>';
192
+ };
193
+ "
194
+
195
+ javascript_string += "window.#{camel_cased}NewHeader = function() {
196
+ var ele = document.getElementById('#{camel_cased}HeadersForm');
197
+ ele.innerHTML += '<div class=\"d-flex f-row\"><input class=\"w-100 m-1 form-control\" type=\"text\" placeholder=\"Enter key\"><input class=\"w-100 m-1 form-control\" type=\"text\" placeholder=\"Enter value\"></div>';
198
+ };
199
+
200
+ ";
201
+
202
+ javascript_string
203
+ end
204
+
205
+ end
206
+ end
@@ -0,0 +1,2 @@
1
+ class {{ CONTROLLER_NAME }}Controller < GlobalController
2
+ end
@@ -0,0 +1,129 @@
1
+ <div class="header p-3 bg-dark card-shadow d-flex flex-row justify-content-between">
2
+ <div class="d-flex flex-row">
3
+ <a href='/' class="h-100">
4
+ <img class="d-flex flex-column align-items-center justify-content-center" src="https://s3-us-west-1.amazonaws.com/manoftech/ruby.png" alt="" height='50' width='50' />
5
+ </a>
6
+ <h1 class="h1 h-100 align-items-center text-white ml-3">API Documentation</h1>
7
+ </div>
8
+ <a class="d-flex align-items-center cursor-pointer">
9
+ <h2 class="h5 text-white" onclick="goBack()">Go Back</h2>
10
+ </a>
11
+ </div>
12
+
13
+ <div class="card-body p-3 w-100 p-1">
14
+ <% @all_routes.each do |data| %>
15
+ <div class="card card-outline-secondary w-100 flex-respond-row card-shadow">
16
+ <div class="col-xs-12 col-sm-12 col-md-12 col-lg-6 col-xl-6 p-1 card-shadow">
17
+ <div class='card-header <%= "#{data[:route_header]}" %>'>
18
+ <span class="mr-1"><%= data[:method] %>:</span>
19
+ <span><%= data[:route] %></span>
20
+ </div>
21
+ <div class="card-body p-2 w-100" style="max-height: 550px; overflow: auto;">
22
+ <% if data[:middleware] && data[:middleware].length > 0 %>
23
+ <h2 class="h5">Middleware Used:</h2>
24
+ <ul>
25
+ <% data[:middleware].each do |ware| %>
26
+ <li class="h6"><%= "#{ware}" %></li>
27
+ <% end %>
28
+ </ul>
29
+ <% end %>
30
+
31
+ <h2 class="h5">Description:
32
+ <ul>
33
+ <% data[:description].each do |ware| %>
34
+ <li class="h6"><%= "#{ware}" %></li>
35
+ <% end %>
36
+ </ul>
37
+ </h2>
38
+
39
+ <div class="d-flex f-row w-100">
40
+ <h2 class="w-100 h6">Headers:</h2>
41
+ <button class="<%= "#{data[:submit_button_color]} rounded-circle" %>" onclick="<%= "#{data[:submit_button_color]}NewHeader()" %>">+</button>
42
+ </div>
43
+ <div class="d-flex f-row w-100">
44
+ <form id="<%= data[:camel_cased] %>HeadersForm" class="w-100">
45
+ <div class="d-flex f-row">
46
+ <input type="text" class="w-100 m-1 d-flex f-row form-control" placeholder="Enter key">
47
+ <input type="text" class="w-100 m-1 form-control" placeholder="Enter value">
48
+ </div>
49
+ </form>
50
+ </div>
51
+
52
+ <% if data[:allow_params] %>
53
+ <div class="d-flex f-row w-100">
54
+ <h2 class="w-100 h6">Params:</h2>
55
+ </div>
56
+ <div class="d-flex f-row w-100">
57
+ <form id="<%= "#{data[:camel_cased]}ParamsForm" %>" class="w-100">
58
+ <%= data[:params].each do |param| %>
59
+ <div class="d-flex f-row">
60
+ <input id="<%= "#{data[:camel_cased]}-#{param}" %>" type="text" class="w-100 m-1 d-flex f-row form-control" value="<%= "#{param}" %>">
61
+ <input id="<%= "#{data[:camel_cased]}-#{param}-value" %>" type="text" class="w-100 m-1 form-control" placeholder="Enter value">
62
+ </div>
63
+ <% end %>
64
+ </form>
65
+ </div>
66
+ <% else %>
67
+ <span></span>
68
+ <% end %>
69
+ <% if data[:allow_body] %>
70
+ <div class="d-flex f-row w-100">
71
+ <h2 class="w-100 h6">Body:</h2>
72
+ <button class="<%= "#{data[:submit_button_color]} rounded mr-2" %>" onclick="<%= "#{data[:camel_cased]}NewBodyFile()" %>">Add File</button>
73
+ <button class="<%= "#{data[:submit_button_color]} rounded-circle" %>" onclick="<%= "#{data[:camel_cased]}NewBody()" %>">+</button>
74
+ </div>
75
+ <div class="d-flex f-row w-100">
76
+ <form id='<%= "#{data[:camel_cased]}BodyForm" %>'class='w-100'>
77
+ <div class='d-flex f-row'>
78
+ <input type='text' class='w-100 m-1 form-control' placeholder='Enter key'>
79
+ <input type='text' class='w-100 m-1 form-control' placeholder='Enter value'>
80
+ </div>
81
+ </form>
82
+ </div>
83
+ <% else %>
84
+ <span></span>
85
+ <% end %>
86
+ <div class="d-flex f-row w-100">
87
+ <h2 class="w-100 h6">Querystrings: </h2>
88
+ <button class="<%= "#{data[:submit_button_color]} rounded-circle" %>" onclick="<%= "#{data[:camel_cased]}NewQS()" %>">+</button>
89
+ </div>
90
+ <div class="d-flex f-row w-100">
91
+ <form id='<%= "#{data[:camel_cased]}QSForm" %>' class="w-100">
92
+ <div class="d-flex f-row">
93
+ <input type="text" class="w-100 m-1 form-control" placeholder="Enter key">
94
+ <input type="text" class="w-100 m-1 form-control" placeholder="Enter value">
95
+ </div>
96
+ </form>
97
+ </div>
98
+ <% if data[:allow_body] %>
99
+ <h2 class="h6">Body Data Type:</h2>
100
+ <select id="<%= "#{data[:camel_cased]}DataType" %>" class='form-control'>
101
+ <option value=''></option>
102
+ <option value='JSON'>JSON</option>
103
+ <option value='Form Data'>Form Data</option>
104
+ </select>
105
+ <% else %>
106
+ <span></span>
107
+ <% end %>
108
+ <div class="w-100 mt-2">
109
+ <button class="<%= "#{data[:submit_button_color]} w-100" %>" onclick="<%= "#{data[:camel_cased]}()" %>">Submit</button>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ <div class="col-xs-12 col-sm-12 col-md-12 col-lg-6 col-xl-6 p-1 card-shadow p-1">
114
+ <div class="card-header">
115
+ <h2 class="h5">Sample Data</h2>
116
+ </div>
117
+ <div class='card-body' style="max-height: 550px; overflow: auto;">
118
+ <code style="overflow: auto; display: block;">
119
+ <span id="<%= "#{data[:camel_cased]}-results" %>" style="white-space: pre;"></span>
120
+ </code>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ <% end %>
125
+ </div>
126
+
127
+
128
+ <script>function goBack() { window.history.back(); }</script>
129
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Automatic API Docs</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+ <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
8
+ <style>.flex-respond-row {display: flex !important;flex-direction: column !important;}@media (min-width: 992px) {.flex-respond-row {display: flex !important;flex-direction: row !important;}}.card-shadow {box-shadow: 0 7px 14px 0 rgba(50,50,93,.1), 0 3px 6px 0 rgba(0,0,0,.07);}.cursor-pointer{ cursor: pointer !important; }</style>
9
+ </head>
10
+
11
+ <body style="width: 100%; overflow-x: hidden;">
12
+ <%= yield %>
13
+ <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
14
+ </body>
15
+ </html>
@@ -0,0 +1,52 @@
1
+ class DocumentationController < ApplicationController
2
+
3
+ def index
4
+ @all_routes = get_api_array
5
+ render template: 'documentation/index', layout: 'documentation'
6
+ end
7
+
8
+ private
9
+
10
+ def get_api_array
11
+ get_api_routes.map { |e| get_api_object(e) }
12
+ end
13
+
14
+ def get_api_object route_hash
15
+ {
16
+ method: route_hash[:method],
17
+ route: route_hash[:route],
18
+ params: get_params(route_hash[:route]),
19
+ middleware: ['Not Available'],
20
+ route_header: "text-white bg-#{get_method_color(route_hash[:method])}",
21
+ submit_button_color: "btn btn-outline-#{get_method_color(route_hash[:method])}",
22
+ camel_cased: camelize_route(route_hash),
23
+ allow_params: route_hash[:route].split(':').length > 1,
24
+ allow_body: route_hash[:method] != 'GET',
25
+ description: ['No Description available'],
26
+ }
27
+ end
28
+
29
+ def get_method_color method
30
+ lookup = {
31
+ GET: 'success',
32
+ POST: 'info',
33
+ PATCH: 'warning',
34
+ PUT: 'warning',
35
+ DELETE: 'danger',
36
+ }
37
+ lookup[method.to_sym] || 'light'
38
+ end
39
+
40
+ def get_params route
41
+ route.split('/').select { |e| /\:/.match(e) }.map { |e| e.split(':')[1] }
42
+ end
43
+
44
+ def camelize_route route
45
+ APIDocumentationService.camelize_route(route)
46
+ end
47
+
48
+ def get_api_routes
49
+ APIDocumentationService.get_api_routes
50
+ end
51
+
52
+ end
@@ -0,0 +1,19 @@
1
+ module ExceptionHandler
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ rescue_from ActiveRecord::RecordNotFound do |e|
6
+ render_json({ error: e.message }, :not_found)
7
+ end
8
+ rescue_from ActiveRecord::RecordInvalid do |e|
9
+ render_json({ error: e.message }, :unprocessable_entity)
10
+ end
11
+ rescue_from ActiveRecord::ActiveRecordError do |e|
12
+ render_json({ error: e.message }, :bad_request)
13
+ end
14
+ rescue_from ActionController::InvalidAuthenticityToken do |e|
15
+ render_json({ error: e.message }, :forbidden)
16
+ end
17
+ end
18
+
19
+ end
@@ -0,0 +1,190 @@
1
+ class GlobalController < ApplicationController
2
+
3
+ include Response
4
+ include ExceptionHandler
5
+
6
+ def default_per_page
7
+ 25
8
+ end
9
+
10
+ def model
11
+ self.class.to_s.split('::').select { |e| /Controller/.match(e) }.first.chomp('Controller').singularize.constantize
12
+ end
13
+
14
+ def index
15
+ resp = index_response
16
+ past_pages = resp[:last_page] < resp[:current_page]
17
+ error = { error: 'Past the amount of pages available' }
18
+ render_json(past_pages ? error : index_response, past_pages ? 400 : 200)
19
+ end
20
+
21
+ def show
22
+ data = model.find(params[:id] || 0)
23
+ render_json(data, 200)
24
+ end
25
+
26
+ def create
27
+ data = model.create(create_params)
28
+ render_json(data, 201)
29
+ end
30
+
31
+ def update
32
+ data = model.where(params[:id] || 0).update(update_params).first
33
+ render_json(data, 200)
34
+ end
35
+
36
+ def destroy
37
+ model.destroy(params[:id] || 0)
38
+ render_json(nil, 204)
39
+ end
40
+
41
+ def bulk_csv_create
42
+ data = model.create(csv_to_json)
43
+ render_json(data, 201)
44
+ end
45
+
46
+ def bulk_create
47
+ data = model.create(bulk_create_params)
48
+ render_json(data, 201)
49
+ end
50
+
51
+ def bulk_update
52
+ data = model.where(id: bulk_update_params[:id]).update(bulk_update_params)
53
+ render_json(data, 201)
54
+ end
55
+
56
+ def bulk_destory
57
+ model.where(id: bulk_destroy_params)
58
+ render_json(nil, 204)
59
+ end
60
+
61
+ private
62
+
63
+ def csv_to_json param_key = :file, separated_by = ','
64
+ lines = params[param_key].split("\n")
65
+ keys = lines[0].split(separated_by)
66
+ rows = lines.slice(1).map { |e| e.split(separated_by) }
67
+ rows.map { |e| e.each_with_index.inject({}) { |acc, (e, i)| acc.merge("#{keys[i]}": e) } }
68
+ end
69
+
70
+ def create_params
71
+ bl = array_to_hash(black_list_create)
72
+ params.permit(*get_model_key.reject { |e| bl[e.to_sym] })
73
+ end
74
+
75
+ def update_params
76
+ bl = array_to_hash(black_list_update)
77
+ params.permit(*get_model_key.reject { |e| bl[e.to_sym] })
78
+ end
79
+
80
+ def bulk_create_params
81
+ params.permit(bulk: get_model_key)
82
+ end
83
+
84
+ def bulk_update_params
85
+ params.permit(bulk: get_model_key)
86
+ end
87
+
88
+ def bulk_destroy_params
89
+ params.permit(bulk: [:id])
90
+ end
91
+
92
+ def black_list_create
93
+ [:id, :created_at, :updated_at]
94
+ end
95
+
96
+ def black_list_update
97
+ [:id, :created_at, :updated_at]
98
+ end
99
+
100
+ def array_to_hash array
101
+ array.each_with_object({}) do |e, acc|
102
+ acc[e] = true
103
+ acc
104
+ end
105
+ end
106
+
107
+ def handle_pagination
108
+ limit = (params[:limit] || default_per_page).to_i
109
+ offset = (params[:offset] || 0).to_i
110
+ page = (params[:page] || 1).to_i
111
+
112
+ data = model.limit(limit).offset(offset).offset((page - 1) * limit)
113
+ data = data.where(get_where) if params[:where]
114
+ data = data.includes(*get_includes) if params[:include]
115
+ data = data.order(get_order) if params[:order]
116
+ data = data.all
117
+ data
118
+ end
119
+
120
+ def handle_pagination_count
121
+ data = model
122
+ data = data.where(get_where) if params[:where]
123
+ data.count
124
+ end
125
+
126
+ def index_response
127
+ page = (params[:page] || 1).to_i
128
+ per_page = (params[:limit] || default_per_page).to_i
129
+ total = handle_pagination_count
130
+ path = request.original_url
131
+ path_path = add_pg_qs(path)
132
+ last = (total / per_page.to_f).ceil
133
+ nxt = (page + 1) > last ? nil : (page + 1)
134
+ prev = (page - 1) == 0 ? nil : (page - 1)
135
+ last = last == 0 ? 1 : last
136
+ data = handle_pagination
137
+ data_first = data.first
138
+ data_last = data.last
139
+
140
+ {
141
+ data: data,
142
+ to: data_first ? data_first.id : nil,
143
+ from: data_last ? data_last.id : nil,
144
+ total: total,
145
+ path: path,
146
+ current_page: page,
147
+ per_page: per_page,
148
+ last_page: last,
149
+ first_page_url: path_path + '1',
150
+ last_page_url: path_path + last.to_s,
151
+ next_page_url: nxt ? (path_path + nxt.to_s) : nil,
152
+ prev_page_url: prev ? (path_path + prev.to_s) : nil,
153
+ }
154
+ end
155
+
156
+ def add_pg_qs str
157
+ has_qs = /\?/.match(str)
158
+ return "#{str}?page=" unless has_qs
159
+ before_qs, after = str.split('?')
160
+ after_qs = after.split('&').reject { |e| /page\=/.match(e) }
161
+ after_qs.push('page=')
162
+ "#{before_qs}?#{after_qs.join('&')}"
163
+ end
164
+
165
+ def get_includes
166
+ comma_separated(params[:include]).map(&:to_sym)
167
+ end
168
+
169
+ def get_order
170
+ params[:order].split(',').flat_map { |e| e.split(':') }.join(' ')
171
+ end
172
+
173
+ def get_where
174
+ arrays = params[:where].split(',').map { |e| e.split(':') }
175
+ arrays.inject({}) do |acc, item|
176
+ key, val = item
177
+ acc[key] = val
178
+ acc
179
+ end
180
+ end
181
+
182
+ def comma_separated key
183
+ params[:key].split(',')
184
+ end
185
+
186
+ def get_model_key
187
+ model.columns.map { |e| e.name.to_sym }
188
+ end
189
+
190
+ end
@@ -0,0 +1,6 @@
1
+ Rails.application.config.middleware.insert_before 0, Rack::Cors do
2
+ allow do
3
+ origins '*'
4
+ resource '*', headers: :any, methods: [:get, :post, :patch, :put, :delete, :options]
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ module Response
2
+
3
+ def render_json data, status
4
+ render json: data, status: status
5
+ end
6
+
7
+ end
@@ -0,0 +1,10 @@
1
+ resources :documentation, only: :index
2
+
3
+ namespace :api do
4
+ # Add API Routes here
5
+
6
+ # For API version 1
7
+ namespace :v1 do
8
+
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ resources :documentation, only: :index
2
+
3
+ namespace :api do
4
+ # Add API Routes here
5
+
6
+ # For API version 1
7
+ namespace :v1 do
8
+
9
+ end
10
+ end
@@ -0,0 +1,61 @@
1
+ class String
2
+
3
+ def underscore
4
+ self.gsub(/::/, '/').
5
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
6
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
7
+ tr("-", "_").
8
+ downcase
9
+ end
10
+
11
+ def camelcase
12
+ self.split(/(?=[A-Z])|(_)/)
13
+ .reject { |e| e == '_' }
14
+ .map(&:capitalize)
15
+ .join('')
16
+ end
17
+
18
+ end
19
+
20
+ class MoreUtils
21
+ class << self
22
+
23
+ def gem_version
24
+ "0.0.3"
25
+ end
26
+
27
+ def get_file_str path
28
+ File.open(path, 'r:UTF-8', &:read)
29
+ end
30
+
31
+ def write_file path, str
32
+ File.write(path, str)
33
+ end
34
+
35
+ def this_dir
36
+ __dir__
37
+ end
38
+
39
+ def root
40
+ Dir.pwd
41
+ end
42
+
43
+ def get_flags arr
44
+ arr.select { |e| /--/.match(e) }
45
+ end
46
+
47
+ def get_args arr
48
+ arr.reject { |e| /--/.match(e) }
49
+ end
50
+
51
+ def flag_lookup arr
52
+ arr.each_with_object({}) do |e, acc|
53
+ e = e.gsub('--', '')
54
+ key, val = e.split('=')
55
+ acc[key.to_sym] = val
56
+ acc
57
+ end
58
+ end
59
+
60
+ end
61
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: railspp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ platform: ruby
6
+ authors:
7
+ - Layne Faler
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-01-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rack-cors
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: faker
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.9'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.9'
55
+ description: Scaffolding your CRUD operations
56
+ email: laynefaler@gmail.com
57
+ executables:
58
+ - railspp
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - README.md
63
+ - bin/railspp
64
+ - lib/commands/initialize.rb
65
+ - lib/commands/make_test.rb
66
+ - lib/commands/model.rb
67
+ - lib/help/documentation.rb
68
+ - lib/help/initialize.rb
69
+ - lib/help/make_test.rb
70
+ - lib/help/model.rb
71
+ - lib/railspp.rb
72
+ - lib/templates/api_documentation_initializer.txt
73
+ - lib/templates/api_documentation_service.txt
74
+ - lib/templates/controller.txt
75
+ - lib/templates/documentation.index.erb.txt
76
+ - lib/templates/documentation.layout.erb.txt
77
+ - lib/templates/documentation_controller.txt
78
+ - lib/templates/exception_handler.txt
79
+ - lib/templates/global_controller.txt
80
+ - lib/templates/rack_cors_initializer.txt
81
+ - lib/templates/response.txt
82
+ - lib/templates/routes_documentation.txt
83
+ - lib/templates/routes_namespace.txt
84
+ - lib/utils/strings.rb
85
+ homepage:
86
+ licenses: []
87
+ metadata: {}
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '2.5'
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubygems_version: 3.0.1
104
+ signing_key:
105
+ specification_version: 4
106
+ summary: Scaffold your CRUD operations
107
+ test_files: []