railspp 0.0.3

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,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: []