apiotics-test 0.1.49
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +260 -0
- data/Rakefile +33 -0
- data/lib/apiotics.rb +58 -0
- data/lib/apiotics/client.rb +20 -0
- data/lib/apiotics/configuration.rb +23 -0
- data/lib/apiotics/extract.rb +107 -0
- data/lib/apiotics/hardware.rb +109 -0
- data/lib/apiotics/insert.rb +115 -0
- data/lib/apiotics/parse.rb +20 -0
- data/lib/apiotics/portal.rb +217 -0
- data/lib/apiotics/railtie.rb +14 -0
- data/lib/apiotics/server.rb +238 -0
- data/lib/apiotics/version.rb +3 -0
- data/lib/generators/apiotics/channel/USAGE +10 -0
- data/lib/generators/apiotics/channel/channel_generator.rb +35 -0
- data/lib/generators/apiotics/channel/templates/apiotics_channel.rb.erb +9 -0
- data/lib/generators/apiotics/channel/templates/apiotics_channel_client.coffee.erb +16 -0
- data/lib/generators/apiotics/channel/templates/apiotics_channel_initializer.rb.erb +3 -0
- data/lib/generators/apiotics/controller/USAGE +8 -0
- data/lib/generators/apiotics/controller/controller_generator.rb +25 -0
- data/lib/generators/apiotics/controller/templates/apiotics_scaffold.rb.erb +60 -0
- data/lib/generators/apiotics/create_model/USAGE +10 -0
- data/lib/generators/apiotics/create_model/create_model_generator.rb +76 -0
- data/lib/generators/apiotics/create_model/templates/apiotics_logs_model.rb.erb +7 -0
- data/lib/generators/apiotics/create_model/templates/apiotics_model.rb.erb +49 -0
- data/lib/generators/apiotics/create_model/templates/apiotics_module.rb.erb +14 -0
- data/lib/generators/apiotics/create_model/templates/apiotics_module_model.rb.erb +8 -0
- data/lib/generators/apiotics/create_model/templates/create_module_model_table.rb.erb +9 -0
- data/lib/generators/apiotics/create_table/USAGE +9 -0
- data/lib/generators/apiotics/create_table/create_table_generator.rb +62 -0
- data/lib/generators/apiotics/create_table/templates/create_logs_table.rb.erb +14 -0
- data/lib/generators/apiotics/create_table/templates/create_table.rb.erb +16 -0
- data/lib/generators/apiotics/initializer/USAGE +8 -0
- data/lib/generators/apiotics/initializer/initializer_generator.rb +29 -0
- data/lib/generators/apiotics/initializer/templates/apiotics.rb.erb +10 -0
- data/lib/generators/apiotics/initializer/templates/apiotics_module.rb.erb +6 -0
- data/lib/generators/apiotics/initializer/templates/apiotics_parents.rb.erb +3 -0
- data/lib/generators/apiotics/initializer/templates/apiotics_settings.rb.erb +9 -0
- data/lib/generators/apiotics/initializer/templates/apiotics_targets.rb.erb +4 -0
- data/lib/generators/apiotics/initializer/templates/setting.rb.erb +3 -0
- data/lib/generators/apiotics/install/USAGE +11 -0
- data/lib/generators/apiotics/install/install_generator.rb +11 -0
- data/lib/generators/apiotics/migration/USAGE +10 -0
- data/lib/generators/apiotics/migration/migration_generator.rb +54 -0
- data/lib/generators/apiotics/migration/templates/create_logs_table.rb.erb +14 -0
- data/lib/generators/apiotics/migration/templates/migrate_table.rb.erb +12 -0
- data/lib/generators/apiotics/model/USAGE +11 -0
- data/lib/generators/apiotics/model/model_generator.rb +58 -0
- data/lib/generators/apiotics/scaffold/scaffold_generator.rb +23 -0
- data/lib/generators/apiotics/scaffold/templates/USAGE +0 -0
- data/lib/generators/apiotics/script/USAGE +8 -0
- data/lib/generators/apiotics/script/script_generator.rb +45 -0
- data/lib/generators/apiotics/script/templates/comm_server.rake +19 -0
- data/lib/generators/apiotics/script/templates/dev_comm_server.rake +19 -0
- data/lib/generators/apiotics/script/templates/dev_server.rb +8 -0
- data/lib/generators/apiotics/script/templates/dev_server_control.rb +7 -0
- data/lib/generators/apiotics/script/templates/install_firmware.rake +10 -0
- data/lib/generators/apiotics/script/templates/publish_script.rake +6 -0
- data/lib/generators/apiotics/script/templates/script.rb.erb +12 -0
- data/lib/generators/apiotics/script/templates/server.rb +8 -0
- data/lib/generators/apiotics/script/templates/server_control.rb +7 -0
- data/lib/generators/apiotics/script/templates/test_comm_server.rake +19 -0
- data/lib/generators/apiotics/script/templates/test_server.rb +8 -0
- data/lib/generators/apiotics/script/templates/test_server_control.rb +7 -0
- data/lib/generators/apiotics/view/USAGE +12 -0
- data/lib/generators/apiotics/view/templates/default.css.erb +18 -0
- data/lib/generators/apiotics/view/templates/edit.html.erb +6 -0
- data/lib/generators/apiotics/view/templates/form.html.erb +52 -0
- data/lib/generators/apiotics/view/templates/index.html.erb +66 -0
- data/lib/generators/apiotics/view/templates/show.html.erb +56 -0
- data/lib/generators/apiotics/view/view_generator.rb +26 -0
- data/lib/tasks/simbiotes_tasks.rake +4 -0
- metadata +256 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'apiotics'
|
2
|
+
require 'rails'
|
3
|
+
|
4
|
+
module Apiotics
|
5
|
+
class Railtie < Rails::Railtie
|
6
|
+
generators do
|
7
|
+
require 'lib/generators/apiotics/model'
|
8
|
+
require 'lib/generators/apiotics/create_table'
|
9
|
+
require 'lib/generators/apiotics/create_model'
|
10
|
+
require 'lib/generators/apiotics/initializer'
|
11
|
+
require 'lib/generators/apiotics/migration'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
|
2
|
+
# This class creates a local server listening on port 8001, and connects to a remote server on port 8000. Messages sent to the local server are transmitted
|
3
|
+
# to the remote server. Messages received from the remote server are forwarded to the appropriate logic.
|
4
|
+
|
5
|
+
require "socket"
|
6
|
+
require 'openssl'
|
7
|
+
require 'json'
|
8
|
+
|
9
|
+
module Apiotics
|
10
|
+
class Server
|
11
|
+
def initialize
|
12
|
+
@error_msg = nil
|
13
|
+
server_details = Server.lookup
|
14
|
+
rgs = ApioticsSetting.find_by(key: "server")
|
15
|
+
rgs_port = ApioticsSetting.find_by(key: "port")
|
16
|
+
if rgs == nil
|
17
|
+
if server_details["status"] == "ok"
|
18
|
+
rgs = server_details["ip"]
|
19
|
+
c = ApioticsSetting.new
|
20
|
+
c.key = "server"
|
21
|
+
c.value = rgs
|
22
|
+
c.save
|
23
|
+
rgs_port = server_details["port"]
|
24
|
+
c = ApioticsSetting.new
|
25
|
+
c.key = "port"
|
26
|
+
c.value = rgs_port
|
27
|
+
c.save
|
28
|
+
else
|
29
|
+
@error_msg = server_details["status_msg"]
|
30
|
+
end
|
31
|
+
else
|
32
|
+
if server_details["status"] == "ok"
|
33
|
+
rgs.value = server_details["ip"]
|
34
|
+
rgs.save
|
35
|
+
rgs = rgs.value
|
36
|
+
rgs_port.value = server_details["port"]
|
37
|
+
rgs_port.save
|
38
|
+
rgs_port = rgs_port.value
|
39
|
+
else
|
40
|
+
@error_msg = server_details["status_msg"]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
if Apiotics.configuration.tls == true
|
44
|
+
socket = TCPSocket.new(rgs, rgs_port)
|
45
|
+
context = OpenSSL::SSL::SSLContext.new
|
46
|
+
context.key = Server.key
|
47
|
+
context.cert = Server.cert
|
48
|
+
if Apiotics.configuration.verify_peer == true
|
49
|
+
ca_tempfile = Tempfile.new
|
50
|
+
ca_tempfile.write Server.ca_cert.to_pem
|
51
|
+
ca_tempfile.rewind
|
52
|
+
context.ca_file = ca_tempfile.path
|
53
|
+
context.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
54
|
+
end
|
55
|
+
server = OpenSSL::SSL::SSLSocket.new socket, context
|
56
|
+
server.sync_close = true
|
57
|
+
server.connect
|
58
|
+
if Apiotics.configuration.verify_peer == true
|
59
|
+
ca_tempfile.close(true)
|
60
|
+
end
|
61
|
+
else
|
62
|
+
server = TCPSocket.open(rgs, rgs_port)
|
63
|
+
end
|
64
|
+
@server = server
|
65
|
+
@localport = Apiotics.configuration.local_port
|
66
|
+
listen_remote
|
67
|
+
listen_local
|
68
|
+
end
|
69
|
+
|
70
|
+
def send(msg)
|
71
|
+
puts "Message sent: #{msg}"
|
72
|
+
@server.puts( msg )
|
73
|
+
end
|
74
|
+
|
75
|
+
def close
|
76
|
+
@server.close
|
77
|
+
end
|
78
|
+
|
79
|
+
def listen_remote
|
80
|
+
begin
|
81
|
+
Thread.new do
|
82
|
+
loop do
|
83
|
+
msg = @server.gets
|
84
|
+
puts msg
|
85
|
+
msg_hash = Apiotics::Parse.message(msg)
|
86
|
+
r = Apiotics::Insert.new(msg_hash)
|
87
|
+
puts "Message received": msg_hash
|
88
|
+
if r.valid == true
|
89
|
+
if r.action == "set-request-ack" || r.action == "set-complete" || r.action == "get-ack"
|
90
|
+
r.save
|
91
|
+
unless Apiotics.configuration.local_logging == false
|
92
|
+
r.save_log
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
rescue => e
|
99
|
+
puts e
|
100
|
+
listen_remote
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
def listen_local
|
106
|
+
begin
|
107
|
+
server = TCPServer.open(@localport)
|
108
|
+
if Apiotics.configuration.handshake == true
|
109
|
+
self.send('{"action":"connect"}')
|
110
|
+
end
|
111
|
+
loop do
|
112
|
+
Thread.fork(server.accept) do |client|
|
113
|
+
s = client.gets
|
114
|
+
if @error_msg != nil
|
115
|
+
string = '{"error":"' + error_msg + '"}'
|
116
|
+
client.puts(string)
|
117
|
+
end
|
118
|
+
#puts s
|
119
|
+
self.send(s)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
rescue => e
|
123
|
+
puts e
|
124
|
+
listen_local
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.lookup
|
129
|
+
if Apiotics.configuration.tls == true
|
130
|
+
socket = TCPSocket.new(Apiotics.configuration.server, Apiotics.configuration.server_port)
|
131
|
+
context = OpenSSL::SSL::SSLContext.new
|
132
|
+
context.key = Server.key
|
133
|
+
context.cert = Server.cert
|
134
|
+
if Apiotics.configuration.verify_peer == true
|
135
|
+
ca_tempfile = Tempfile.new
|
136
|
+
ca_tempfile.write Server.ca_cert.to_pem
|
137
|
+
ca_tempfile.rewind
|
138
|
+
context.ca_file = ca_tempfile.path
|
139
|
+
context.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
140
|
+
end
|
141
|
+
server = OpenSSL::SSL::SSLSocket.new socket, context
|
142
|
+
server.sync_close = true
|
143
|
+
server.connect
|
144
|
+
else
|
145
|
+
server = TCPSocket.open(Apiotics.configuration.server, Apiotics.configuration.server_port)
|
146
|
+
end
|
147
|
+
server.puts('{"action":"lookup"}')
|
148
|
+
msg = server.gets
|
149
|
+
hash = JSON.parse(msg)
|
150
|
+
server.close
|
151
|
+
if Apiotics.configuration.verify_peer == true
|
152
|
+
ca_tempfile.close(true)
|
153
|
+
end
|
154
|
+
return hash
|
155
|
+
end
|
156
|
+
|
157
|
+
def self.key
|
158
|
+
key = ApioticsSetting.find_by(key: "key")
|
159
|
+
if key == nil
|
160
|
+
key = Server.generate_key
|
161
|
+
else
|
162
|
+
key = key.value
|
163
|
+
end
|
164
|
+
pass_phrase = ApioticsSetting.find_by(key: "key_pass_phrase").value
|
165
|
+
key = OpenSSL::PKey::RSA.new key, pass_phrase
|
166
|
+
return key
|
167
|
+
end
|
168
|
+
|
169
|
+
def self.cert
|
170
|
+
cert = ApioticsSetting.find_by(key: "cert")
|
171
|
+
if cert == nil
|
172
|
+
key = Server.key
|
173
|
+
public_key = key.public_key
|
174
|
+
cert = Server.generate_cert(key, public_key)
|
175
|
+
c = ApioticsSetting.new
|
176
|
+
c.key = "cert"
|
177
|
+
c.value = cert
|
178
|
+
c.save
|
179
|
+
else
|
180
|
+
cert = cert.value
|
181
|
+
end
|
182
|
+
cert = OpenSSL::X509::Certificate.new cert
|
183
|
+
return cert
|
184
|
+
end
|
185
|
+
|
186
|
+
def self.generate_key
|
187
|
+
key = OpenSSL::PKey::RSA.new 2048
|
188
|
+
pass_phrase = 'simbiotes'
|
189
|
+
cipher = OpenSSL::Cipher.new 'AES-128-CBC'
|
190
|
+
s = ApioticsSetting.new
|
191
|
+
s.key = "key_cipher"
|
192
|
+
s.value = "OpenSSL::Cipher.new 'AES-128-CBC'"
|
193
|
+
s.save
|
194
|
+
s = ApioticsSetting.new
|
195
|
+
s.key = "key_pass_phrase"
|
196
|
+
s.value = 'simbiotes'
|
197
|
+
s.save
|
198
|
+
s = ApioticsSetting.new
|
199
|
+
s.key = "public_key"
|
200
|
+
s.value = key.public_key.to_pem
|
201
|
+
s.save
|
202
|
+
s = ApioticsSetting.new
|
203
|
+
s.key = "key"
|
204
|
+
s.value = key.export(cipher, pass_phrase)
|
205
|
+
s.save
|
206
|
+
return s.value
|
207
|
+
end
|
208
|
+
|
209
|
+
def self.generate_cert(key, public_key)
|
210
|
+
csr = OpenSSL::X509::Request.new
|
211
|
+
csr.version = 0
|
212
|
+
csr.subject = OpenSSL::X509::Name.parse "CN=simbiotes.com/O=#{Apiotics.configuration.public_key}/OU=#{Apiotics.configuration.private_key}"
|
213
|
+
csr.public_key = key.public_key
|
214
|
+
csr.sign key, OpenSSL::Digest::SHA1.new
|
215
|
+
cert = Apiotics::Portal.generate_certificate(csr)
|
216
|
+
return cert
|
217
|
+
end
|
218
|
+
|
219
|
+
def self.ca_cert
|
220
|
+
ca_cert = ApioticsSetting.find_by(key: "ca_cert")
|
221
|
+
if ca_cert == nil
|
222
|
+
ca_cert = Apiotics::Portal.ca_certificate
|
223
|
+
c = ApioticsSetting.new
|
224
|
+
c.key = "ca_cert"
|
225
|
+
c.value = ca_cert
|
226
|
+
c.save
|
227
|
+
else
|
228
|
+
ca_cert = ca_cert.value
|
229
|
+
end
|
230
|
+
ca_cert = OpenSSL::X509::Certificate.new ca_cert
|
231
|
+
return ca_cert
|
232
|
+
end
|
233
|
+
|
234
|
+
def do_at_exit
|
235
|
+
end
|
236
|
+
|
237
|
+
end
|
238
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
Description:
|
2
|
+
This generator creates channel files with the right code for Apiotics interactions.
|
3
|
+
|
4
|
+
Example:
|
5
|
+
rails generate apiotics:channel Worker Driver
|
6
|
+
rails generate apiotics:channel Worker Script
|
7
|
+
|
8
|
+
This will create:
|
9
|
+
app/channel/worker/driver_channel.rb
|
10
|
+
app/assets/javascripts/channels/worker_driver.coffee
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Apiotics
|
2
|
+
class ChannelGenerator < Rails::Generators::Base
|
3
|
+
source_root File.expand_path('../templates', __FILE__)
|
4
|
+
argument :parent, :type => :string
|
5
|
+
|
6
|
+
def copy_channel_files
|
7
|
+
template "apiotics_channel.rb.erb", "app/channels/#{module_file_name}_channel.rb"
|
8
|
+
template "apiotics_channel_client.coffee.erb", "app/assets/javascripts/channels/#{module_file_name}.coffee"
|
9
|
+
template "apiotics_channel_initializer.rb.erb", "config/initializers/apiotics_channel.rb"
|
10
|
+
route "mount ActionCable.server => '/cable'"
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def file_name
|
16
|
+
name.underscore
|
17
|
+
end
|
18
|
+
|
19
|
+
def module_file_name
|
20
|
+
parent.underscore
|
21
|
+
end
|
22
|
+
|
23
|
+
def class_name
|
24
|
+
name.classify
|
25
|
+
end
|
26
|
+
|
27
|
+
def module_name
|
28
|
+
parent.classify
|
29
|
+
end
|
30
|
+
|
31
|
+
def table_prefix
|
32
|
+
parent.underscore + "_"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
$(document).ready ->
|
2
|
+
App.alert = App.cable.subscriptions.create('<%= "#{module_name}" + "Channel" %>',
|
3
|
+
connected: ->
|
4
|
+
# Called when the subscription is ready for use on the server
|
5
|
+
return
|
6
|
+
disconnected: ->
|
7
|
+
# Called when the subscription has been terminated by the server
|
8
|
+
return
|
9
|
+
received: (data) ->
|
10
|
+
console.log data
|
11
|
+
tr = $("#" + data["apiotics_instance"])
|
12
|
+
td = tr.children("." + data["worker_name"] + "-" + data["model_name"] + "-" + data["interface"])
|
13
|
+
td.text(data["value"])
|
14
|
+
# Called when there's incoming data on the websocket for this channel
|
15
|
+
return
|
16
|
+
)
|
@@ -0,0 +1,8 @@
|
|
1
|
+
Description:
|
2
|
+
Creates a model and associated database table with the attributes specified. If using data from the portal (recommended) Worker should be the name of a worker defined in your Hive, and Model should be the name of a Driver or Script in that Worker.
|
3
|
+
|
4
|
+
Example:
|
5
|
+
rails generate apiotics:scaffold Worker
|
6
|
+
|
7
|
+
This will create:
|
8
|
+
app/controllers/worker.rb
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Apiotics
|
2
|
+
class ControllerGenerator < Rails::Generators::Base
|
3
|
+
source_root File.expand_path('../templates', __FILE__)
|
4
|
+
argument :parent, :type => :string
|
5
|
+
|
6
|
+
def copy_scaffold_files
|
7
|
+
@models = Apiotics.configuration.targets[module_name.to_s]
|
8
|
+
template "apiotics_scaffold.rb.erb", "app/controllers/#{module_file_name}/#{module_file_name.pluralize}_controller.rb"
|
9
|
+
generate "apiotics:view", "#{parent}"
|
10
|
+
route "scope module: :#{module_file_name} do \n\t\tresources :#{module_file_name}s\n\tend"
|
11
|
+
generate "simple_form:install"
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def module_file_name
|
17
|
+
parent.underscore
|
18
|
+
end
|
19
|
+
|
20
|
+
def module_name
|
21
|
+
parent.classify
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module <%= module_name %>
|
2
|
+
class <%= module_name.pluralize + "Controller"%> < ApplicationController
|
3
|
+
before_action <%= ":set_" + module_file_name %>, only: [:show, :edit, :update, :destroy]
|
4
|
+
|
5
|
+
# GET /alert/leds
|
6
|
+
def index
|
7
|
+
@<%= module_file_name + "s" %> = <%= "::" + module_name + "::" + module_name + ".all" %>
|
8
|
+
end
|
9
|
+
|
10
|
+
# GET /alert/leds/1
|
11
|
+
def show
|
12
|
+
end
|
13
|
+
|
14
|
+
# GET /alert/leds/new
|
15
|
+
def new
|
16
|
+
redirect_to <%= module_file_name %>s_url, notice: 'You are not authorized to do that.'
|
17
|
+
end
|
18
|
+
|
19
|
+
# GET /alert/leds/1/edit
|
20
|
+
def edit
|
21
|
+
end
|
22
|
+
|
23
|
+
# POST /alert/leds
|
24
|
+
def create
|
25
|
+
@<%= module_file_name %> = <%= "::" + module_name + "::" + module_name + ".new(#{module_file_name}_params)" %>
|
26
|
+
|
27
|
+
if @<%= module_file_name %>.save
|
28
|
+
redirect_to <%= module_file_name %>_path(@<%= module_file_name %>), notice: '<%= module_name %> was successfully created.'
|
29
|
+
else
|
30
|
+
render :new
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# PATCH/PUT /alert/leds/1
|
35
|
+
def update
|
36
|
+
if @<%= module_file_name %>.update(<%= module_file_name %>_params)
|
37
|
+
redirect_to <%= module_file_name %>_path(@<%= module_file_name %>), notice: '<%= module_name %> was successfully updated.'
|
38
|
+
else
|
39
|
+
render :edit
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# DELETE /alert/leds/1
|
44
|
+
def destroy
|
45
|
+
@<%= module_file_name %>.destroy
|
46
|
+
redirect_to <%= module_file_name %>s_url, notice: '<%= module_file_name %> was successfully destroyed.'
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
# Use callbacks to share common setup or constraints between actions.
|
51
|
+
def set_<%= module_file_name %>
|
52
|
+
@<%= module_file_name %> = <%= "::" + module_name + "::" + module_name + ".find(params[:id])" %>
|
53
|
+
end
|
54
|
+
|
55
|
+
# Only allow a trusted parameter "white list" through.
|
56
|
+
def <%= module_file_name %>_params
|
57
|
+
params.fetch(:<%= module_file_name %>_<%= module_file_name %>, {}).permit(:id, :apiotics_instance<% Apiotics.configuration.targets[module_name].keys.each do |key| %>, :<%= key.underscore.downcase %>_attributes => <%= Apiotics.configuration.targets[module_name][key] + ["id"] %><% end %>)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
Description:
|
2
|
+
This generator creates model files with the right code for Apiotics interactions.
|
3
|
+
|
4
|
+
Example:
|
5
|
+
rails generate apiotics:model Worker Driver
|
6
|
+
rails generate apiotics:model Worker Script
|
7
|
+
|
8
|
+
This will create:
|
9
|
+
app/models/worker.rb
|
10
|
+
app/models/worker/driver.rb
|