apiotics 0.1.22
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 +57 -0
- data/lib/apiotics/client.rb +20 -0
- data/lib/apiotics/configuration.rb +22 -0
- data/lib/apiotics/extract.rb +87 -0
- data/lib/apiotics/insert.rb +105 -0
- data/lib/apiotics/parse.rb +20 -0
- data/lib/apiotics/portal.rb +156 -0
- data/lib/apiotics/railtie.rb +14 -0
- data/lib/apiotics/server.rb +236 -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 +24 -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 +10 -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 +8 -0
- data/lib/generators/apiotics/create_table/USAGE +9 -0
- data/lib/generators/apiotics/create_table/create_table_generator.rb +52 -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 +27 -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_settings.rb.erb +9 -0
- data/lib/generators/apiotics/initializer/templates/apiotics_targets.rb.erb +3 -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/script/USAGE +8 -0
- data/lib/generators/apiotics/script/script_generator.rb +44 -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/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 +36 -0
- data/lib/generators/apiotics/view/templates/index.html.erb +57 -0
- data/lib/generators/apiotics/view/templates/show.html.erb +47 -0
- data/lib/generators/apiotics/view/view_generator.rb +26 -0
- data/lib/tasks/simbiotes_tasks.rake +4 -0
- metadata +237 -0
@@ -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,24 @@
|
|
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
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def module_file_name
|
16
|
+
parent.underscore
|
17
|
+
end
|
18
|
+
|
19
|
+
def module_name
|
20
|
+
parent.classify
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
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
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Apiotics
|
2
|
+
class CreateModelGenerator < Rails::Generators::Base
|
3
|
+
source_root File.expand_path('../templates', __FILE__)
|
4
|
+
argument :parent, :type => :string
|
5
|
+
argument :name, :type => :string
|
6
|
+
argument :portal, :type => :string, :required => false
|
7
|
+
|
8
|
+
def copy_model_file
|
9
|
+
if portal == "true"
|
10
|
+
@c = Apiotics.get_attributes(parent, name)
|
11
|
+
end
|
12
|
+
@targets = Apiotics::Portal.parse_all_interfaces
|
13
|
+
template "apiotics_module.rb.erb", "app/models/#{module_file_name}.rb"
|
14
|
+
unless File.exist?("app/models/#{module_file_name}/#{module_file_name}.rb")
|
15
|
+
template "create_module_model_table.rb.erb", "db/migrate/#{date_string}_create_#{plural_name}.rb"
|
16
|
+
end
|
17
|
+
template "apiotics_module_model.rb.erb", "app/models/#{module_file_name}/#{module_file_name}.rb"
|
18
|
+
template "apiotics_model.rb.erb", "app/models/#{module_file_name}/#{file_name}.rb"
|
19
|
+
unless Apiotics.configuration.local_logging == false
|
20
|
+
if portal == "true"
|
21
|
+
@c[:attributes].each do |k,v|
|
22
|
+
@k = k
|
23
|
+
@v = v
|
24
|
+
template "apiotics_logs_model.rb.erb", "app/models/#{module_file_name}/#{file_name}_#{@k.underscore.downcase.gsub(" ","_")}_log.rb"
|
25
|
+
end
|
26
|
+
else
|
27
|
+
@k = name
|
28
|
+
template "apiotics_logs_model.rb.erb", "app/models/#{module_file_name}/#{file_name}_#{@k.underscore.downcase.gsub(" ","_")}_log.rb"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def file_name
|
36
|
+
name.underscore
|
37
|
+
end
|
38
|
+
|
39
|
+
def module_file_name
|
40
|
+
parent.underscore
|
41
|
+
end
|
42
|
+
|
43
|
+
def class_name
|
44
|
+
name.classify
|
45
|
+
end
|
46
|
+
|
47
|
+
def module_name
|
48
|
+
parent.classify
|
49
|
+
end
|
50
|
+
|
51
|
+
def table_prefix
|
52
|
+
parent.underscore + "_"
|
53
|
+
end
|
54
|
+
|
55
|
+
def plural_name
|
56
|
+
parent.underscore.pluralize
|
57
|
+
end
|
58
|
+
|
59
|
+
def plural_table_name
|
60
|
+
parent.underscore + "_" + parent.underscore.pluralize
|
61
|
+
end
|
62
|
+
|
63
|
+
def plural_class_name
|
64
|
+
parent.classify + name.classify.pluralize
|
65
|
+
end
|
66
|
+
|
67
|
+
def plural_module_name
|
68
|
+
parent.classify.pluralize
|
69
|
+
end
|
70
|
+
|
71
|
+
def date_string
|
72
|
+
date_string = DateTime.now.strftime("%Y%m%d%H%M%S")
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module <%= module_name %>
|
2
|
+
class <%= class_name %> < ApplicationRecord
|
3
|
+
<% unless @c == nil %># The method(s) that correspond to device interfaces are: <% @c[:attributes].each do |k,v| %><%=k.downcase.gsub(" ", "_")%> <% end %><% end %>
|
4
|
+
<%unless @c == nil %><% unless Apiotics.configuration.local_logging == false %><% @c[:attributes].each do |k,v| %>
|
5
|
+
has_many :<%= class_name.underscore.downcase.gsub(" ", "_") + "_#{k.underscore.downcase.gsub(" ", "_")}_logs"%>, dependent: :destroy
|
6
|
+
<% end %><% end %><% end %>
|
7
|
+
belongs_to :<%= module_name.underscore.downcase.gsub(" ", "_")%>
|
8
|
+
attr_accessor :skip_extract
|
9
|
+
after_commit :extract, unless: :skip_extract
|
10
|
+
after_commit :channel_push
|
11
|
+
<% unless @c == nil %><% @c[:attributes].each do |k,v| %><% unless v[:values] == "" %>validates :<%= k.downcase.gsub(" ", "_").underscore %>, inclusion: { in: %w(<%= v[:values].join(" ") %>,
|
12
|
+
message: "%{value} is not a valid <%= k %>" }<% end %><% unless v[:range] == "" %>
|
13
|
+
validates :<%= k.gsub(" ", "_").underscore %>, inclusion: { in: <%= v[:range] %>,
|
14
|
+
message: "%{value} is not within the range <%= v[:range] %>" }<% end %><% if v[:type] == "enum" %>
|
15
|
+
enum <%= k.gsub(" ", "_").underscore %>: <%= v[:values] %><% end %><% end %><% end %>
|
16
|
+
|
17
|
+
|
18
|
+
def sync
|
19
|
+
Apiotics.sync(self)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def extract
|
25
|
+
Apiotics::Extract.send(self)
|
26
|
+
end
|
27
|
+
|
28
|
+
def channel_push
|
29
|
+
if Apiotics.configuration.push == true
|
30
|
+
interfaces = Hash.new
|
31
|
+
puts self.previous_changes
|
32
|
+
self.previous_changes.each do |k,v|
|
33
|
+
if Apiotics::Extract.is_target(self, k)
|
34
|
+
interfaces[k] = v[1].to_s
|
35
|
+
end
|
36
|
+
end
|
37
|
+
interfaces.each do |k,v|
|
38
|
+
ActionCable.server.broadcast "#{self.class.parent.to_s.underscore.downcase.gsub(" ", "_")}_channel",
|
39
|
+
apiotics_instance: self.<%= module_name.underscore.downcase.gsub(" ", "_")%>.apiotics_instance,
|
40
|
+
worker_name: self.class.parent.to_s.underscore.downcase.gsub(" ", "_"),
|
41
|
+
model_name: self.class.name.demodulize.to_s.underscore.downcase.gsub(" ", "_"),
|
42
|
+
interface: k,
|
43
|
+
value: v
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
module <%= module_name %>
|
2
|
+
class <%= module_name %> < ApplicationRecord
|
3
|
+
<% @targets[module_name].keys.each do |key| %>
|
4
|
+
has_one :<%= key.underscore.downcase.gsub(" ", "_")%>, dependent: :destroy
|
5
|
+
accepts_nested_attributes_for :<%= key.underscore.downcase.gsub(" ", "_")%>
|
6
|
+
<% end %>
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
Description:
|
2
|
+
Creates a database table with attributes that Apiotics requires.
|
3
|
+
|
4
|
+
Example:
|
5
|
+
rails generate apiotics:create_table Table attribute:type attribute:type ... attribute:type
|
6
|
+
|
7
|
+
This will create:
|
8
|
+
db/migrate/{datetime_stamp}_create_table.rb
|
9
|
+
db/migrate/{datetime_stamp}_create_table_logs.rb
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Apiotics
|
2
|
+
class CreateTableGenerator < Rails::Generators::Base
|
3
|
+
source_root File.expand_path('../templates', __FILE__)
|
4
|
+
argument :parent, :type => :string
|
5
|
+
argument :name, :type => :string
|
6
|
+
argument :attributes, :type => :hash, :required => false
|
7
|
+
|
8
|
+
def copy_create_table_files
|
9
|
+
template "create_table.rb.erb", "db/migrate/#{date_string}_create_#{plural_name}.rb"
|
10
|
+
unless Apiotics.configuration.local_logging == false
|
11
|
+
sleep 1
|
12
|
+
attributes.each do |k,v|
|
13
|
+
@k = k
|
14
|
+
@v = v
|
15
|
+
template "create_logs_table.rb.erb", "db/migrate/#{date_string}_create_#{table_name}_#{@k}_logs.rb"
|
16
|
+
sleep 1
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def date_string
|
24
|
+
date_string = DateTime.now.strftime("%Y%m%d%H%M%S")
|
25
|
+
end
|
26
|
+
|
27
|
+
def plural_name
|
28
|
+
parent.underscore + "_" + name.underscore.pluralize
|
29
|
+
end
|
30
|
+
|
31
|
+
def table_name
|
32
|
+
parent.underscore + "_" + name.underscore
|
33
|
+
end
|
34
|
+
|
35
|
+
def class_name
|
36
|
+
name.classify
|
37
|
+
end
|
38
|
+
|
39
|
+
def plural_class_name
|
40
|
+
parent.classify + name.classify.pluralize
|
41
|
+
end
|
42
|
+
|
43
|
+
def logs_class_name(k)
|
44
|
+
parent.classify + name.classify + k.classify + "Logs"
|
45
|
+
end
|
46
|
+
|
47
|
+
def logs_table_name(k)
|
48
|
+
parent.underscore + "_" + name.underscore + "_" + k.underscore + "_" + "logs"
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class Create<%= logs_class_name(@k) %> < ActiveRecord::Migration[5.1]
|
2
|
+
def change
|
3
|
+
create_table :<%= logs_table_name(@k) %> do |t|
|
4
|
+
t.<%= @v %> :<%= @k %>
|
5
|
+
t.boolean :<%= @k %>_ack
|
6
|
+
t.boolean :<%= @k %>_complete
|
7
|
+
t.string :<%= @k %>_timestamp
|
8
|
+
t.string :<%= @k %>_status
|
9
|
+
t.string :<%= @k %>_action
|
10
|
+
t.integer :<%= name.underscore.downcase %>_id
|
11
|
+
t.timestamps
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class Create<%= plural_class_name %> < ActiveRecord::Migration[5.1]
|
2
|
+
def change
|
3
|
+
create_table :<%= plural_name %> do |t|
|
4
|
+
<% attributes.each do |k,v| %>
|
5
|
+
t.<%= v %> :<%= k %>
|
6
|
+
t.boolean :<%= k %>_ack
|
7
|
+
t.boolean :<%= k %>_complete
|
8
|
+
t.string :<%= k %>_timestamp
|
9
|
+
t.string :<%= k %>_status
|
10
|
+
t.string :<%= k %>_action
|
11
|
+
<% end %>
|
12
|
+
t.integer :<%= parent.underscore.downcase.gsub(" ", "_") + "_id" %>
|
13
|
+
t.timestamps
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|