ruby_robot 0.1.9
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/.rspec +1 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +133 -0
- data/Guardfile +77 -0
- data/LICENSE.txt +21 -0
- data/README.md +50 -0
- data/Rakefile +24 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/ruby_robot +34 -0
- data/exe/ruby_robot_grpc_client +53 -0
- data/exe/ruby_robot_grpc_server +46 -0
- data/exe/ruby_robot_web +19 -0
- data/json_schema/request.schema.json +37 -0
- data/json_schema/response.schema.json +24 -0
- data/lib/public/favicon-16x16.png +0 -0
- data/lib/public/favicon-32x32.png +0 -0
- data/lib/public/index.html +95 -0
- data/lib/public/swagger-ui-bundle.js +93 -0
- data/lib/public/swagger-ui-standalone-preset.js +13 -0
- data/lib/public/swagger-ui.css +3 -0
- data/lib/public/swagger-ui.js +8 -0
- data/lib/public/swagger.json +179 -0
- data/lib/ruby_robot.rb +16 -0
- data/lib/ruby_robot/cacerts.crt +92 -0
- data/lib/ruby_robot/construction_error.rb +4 -0
- data/lib/ruby_robot/grpc/robot_service.rb +102 -0
- data/lib/ruby_robot/grpc/ruby_robot_services_pb.rb +34 -0
- data/lib/ruby_robot/grpc_helper.rb +15 -0
- data/lib/ruby_robot/grpc_ruby/ruby_robot_pb.rb +36 -0
- data/lib/ruby_robot/grpc_shell.rb +85 -0
- data/lib/ruby_robot/netflix_tabletop.rb +14 -0
- data/lib/ruby_robot/placement_error.rb +4 -0
- data/lib/ruby_robot/robot.rb +71 -0
- data/lib/ruby_robot/ruby_robot.proto +53 -0
- data/lib/ruby_robot/schema_loader.rb +10 -0
- data/lib/ruby_robot/shell.rb +204 -0
- data/lib/ruby_robot/tabletop.rb +120 -0
- data/lib/ruby_robot/version.rb +3 -0
- data/lib/ruby_robot/webapp.rb +263 -0
- data/ruby_robot.gemspec +48 -0
- metadata +258 -0
@@ -0,0 +1,53 @@
|
|
1
|
+
//
|
2
|
+
// gRPC service definition for the RubyRobot.
|
3
|
+
//
|
4
|
+
syntax = "proto3";
|
5
|
+
|
6
|
+
import "google/protobuf/empty.proto";
|
7
|
+
|
8
|
+
option java_multiple_files = true;
|
9
|
+
option java_package = "net.avilla.netflix.studio.robot";
|
10
|
+
option java_outer_classname = "RobotProto";
|
11
|
+
option objc_class_prefix = "RBT";
|
12
|
+
|
13
|
+
package RubyRobot;
|
14
|
+
|
15
|
+
//
|
16
|
+
// Service definitions
|
17
|
+
//
|
18
|
+
service RubyRobot {
|
19
|
+
rpc Left(google.protobuf.Empty) returns (RubyRobotResponse) {}
|
20
|
+
rpc Move(google.protobuf.Empty) returns (RubyRobotResponse) {}
|
21
|
+
rpc Place(RubyRobotRequest) returns (RubyRobotResponse) {}
|
22
|
+
rpc Remove(google.protobuf.Empty) returns (google.protobuf.Empty) {}
|
23
|
+
rpc Report(google.protobuf.Empty) returns (RubyRobotResponse) {}
|
24
|
+
rpc Right(google.protobuf.Empty) returns (RubyRobotResponse) {}
|
25
|
+
}
|
26
|
+
|
27
|
+
//
|
28
|
+
// Message type definitions
|
29
|
+
//
|
30
|
+
message RubyRobotRequest {
|
31
|
+
int32 x = 1;
|
32
|
+
int32 y = 2;
|
33
|
+
enum Direction {
|
34
|
+
// Clockwise from NORTH
|
35
|
+
NORTH=0;
|
36
|
+
EAST =1;
|
37
|
+
SOUTH=2;
|
38
|
+
WEST =3;
|
39
|
+
}
|
40
|
+
Direction direction = 3;
|
41
|
+
}
|
42
|
+
|
43
|
+
message RubyRobotError {
|
44
|
+
int32 error = 1;
|
45
|
+
string message = 2;
|
46
|
+
}
|
47
|
+
|
48
|
+
message RubyRobotResponse {
|
49
|
+
oneof response {
|
50
|
+
RubyRobotRequest current_state = 1;
|
51
|
+
RubyRobotError error = 2;
|
52
|
+
}
|
53
|
+
}
|
@@ -0,0 +1,204 @@
|
|
1
|
+
#
|
2
|
+
# Behold: I stand on the shoulders of giants...rossmeissl@github.com
|
3
|
+
# is the _(wo?)man_
|
4
|
+
#
|
5
|
+
require 'bombshell'
|
6
|
+
require 'logger'
|
7
|
+
|
8
|
+
module RubyRobot
|
9
|
+
class Shell < ::Bombshell::Environment
|
10
|
+
include ::Bombshell::Shell
|
11
|
+
|
12
|
+
prompt_with 'ILoveNetflixStudio'
|
13
|
+
|
14
|
+
attr_reader :logger
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@logger = Logger.new(STDOUT)
|
18
|
+
@logger.formatter = proc { |severity, datetime, progname, msg|
|
19
|
+
"#{msg}\n"
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# Place a robot
|
25
|
+
#
|
26
|
+
def PLACE(x, y, direction)
|
27
|
+
# Save state in case place is called w/ invalid coords
|
28
|
+
orig_robot = @robot
|
29
|
+
orig_tabletop = @tabletop
|
30
|
+
# TODO: What happens when place is called > 1x per session?
|
31
|
+
# Answer under time crunch: just replace the Robot and Tabletop
|
32
|
+
@robot = Robot.new(direction)
|
33
|
+
@tabletop = NetflixTabletop.new
|
34
|
+
begin
|
35
|
+
@tabletop.place(@robot, x: x, y: y)
|
36
|
+
true
|
37
|
+
rescue
|
38
|
+
@robot = orig_robot
|
39
|
+
@tabletop = orig_tabletop
|
40
|
+
@logger.info $!
|
41
|
+
false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def MOVE
|
46
|
+
return if @robot.nil?
|
47
|
+
@robot.move
|
48
|
+
end
|
49
|
+
|
50
|
+
def LEFT
|
51
|
+
return if @robot.nil?
|
52
|
+
@robot.left
|
53
|
+
end
|
54
|
+
|
55
|
+
def RIGHT
|
56
|
+
return if @robot.nil?
|
57
|
+
@robot.right
|
58
|
+
end
|
59
|
+
|
60
|
+
def REPORT(to_stderr=true)
|
61
|
+
return nil if @robot.nil?
|
62
|
+
@logger.info(@robot.report) if to_stderr
|
63
|
+
@robot.report
|
64
|
+
end
|
65
|
+
|
66
|
+
# Exit Bombshell
|
67
|
+
alias :QUIT :quit
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
#
|
72
|
+
# Monkeypatch Irb to allow no-arg uppercase methods
|
73
|
+
#
|
74
|
+
module IRB
|
75
|
+
class Irb
|
76
|
+
# Evaluates input for this session.
|
77
|
+
def eval_input
|
78
|
+
@scanner.set_prompt do
|
79
|
+
|ltype, indent, continue, line_no|
|
80
|
+
if ltype
|
81
|
+
f = @context.prompt_s
|
82
|
+
elsif continue
|
83
|
+
f = @context.prompt_c
|
84
|
+
elsif indent > 0
|
85
|
+
f = @context.prompt_n
|
86
|
+
else
|
87
|
+
f = @context.prompt_i
|
88
|
+
end
|
89
|
+
f = "" unless f
|
90
|
+
if @context.prompting?
|
91
|
+
@context.io.prompt = p = prompt(f, ltype, indent, line_no)
|
92
|
+
else
|
93
|
+
@context.io.prompt = p = ""
|
94
|
+
end
|
95
|
+
if @context.auto_indent_mode
|
96
|
+
unless ltype
|
97
|
+
ind = prompt(@context.prompt_i, ltype, indent, line_no)[/.*\z/].size +
|
98
|
+
indent * 2 - p.size
|
99
|
+
ind += 2 if continue
|
100
|
+
@context.io.prompt = p + " " * ind if ind > 0
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
@scanner.set_input(@context.io) do
|
106
|
+
signal_status(:IN_INPUT) do
|
107
|
+
if l = @context.io.gets
|
108
|
+
#
|
109
|
+
# Begin monkeypatch: turn uppercase constants into
|
110
|
+
# method calls
|
111
|
+
#
|
112
|
+
if [:MOVE,:LEFT,:RIGHT,:REPORT,:REMOVE,:QUIT].include?(l.strip.to_sym)
|
113
|
+
l = "#{l.strip}()\n"
|
114
|
+
end
|
115
|
+
#
|
116
|
+
# End monkeypatch
|
117
|
+
#
|
118
|
+
print l if @context.verbose?
|
119
|
+
else
|
120
|
+
if @context.ignore_eof? and @context.io.readable_after_eof?
|
121
|
+
l = "\n"
|
122
|
+
if @context.verbose?
|
123
|
+
printf "Use \"exit\" to leave %s\n", @context.ap_name
|
124
|
+
end
|
125
|
+
else
|
126
|
+
print "\n"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
l
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
@scanner.each_top_level_statement do |line, line_no|
|
134
|
+
signal_status(:IN_EVAL) do
|
135
|
+
begin
|
136
|
+
line.untaint
|
137
|
+
@context.evaluate(line, line_no)
|
138
|
+
output_value if @context.echo?
|
139
|
+
exc = nil
|
140
|
+
rescue Interrupt => exc
|
141
|
+
rescue SystemExit, SignalException
|
142
|
+
raise
|
143
|
+
rescue Exception => exc
|
144
|
+
end
|
145
|
+
if exc
|
146
|
+
print exc.class, ": ", exc, "\n"
|
147
|
+
if exc.backtrace && exc.backtrace[0] =~ /irb(2)?(\/.*|-.*|\.rb)?:/ && exc.class.to_s !~ /^IRB/ &&
|
148
|
+
!(SyntaxError === exc)
|
149
|
+
irb_bug = true
|
150
|
+
else
|
151
|
+
irb_bug = false
|
152
|
+
end
|
153
|
+
|
154
|
+
messages = []
|
155
|
+
lasts = []
|
156
|
+
levels = 0
|
157
|
+
if exc.backtrace
|
158
|
+
for m in exc.backtrace
|
159
|
+
m = @context.workspace.filter_backtrace(m) unless irb_bug
|
160
|
+
if m
|
161
|
+
if messages.size < @context.back_trace_limit
|
162
|
+
messages.push "\tfrom "+m
|
163
|
+
else
|
164
|
+
lasts.push "\tfrom "+m
|
165
|
+
if lasts.size > @context.back_trace_limit
|
166
|
+
lasts.shift
|
167
|
+
levels += 1
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
print messages.join("\n"), "\n"
|
174
|
+
unless lasts.empty?
|
175
|
+
printf "... %d levels...\n", levels if levels > 0
|
176
|
+
print lasts.join("\n"), "\n"
|
177
|
+
end
|
178
|
+
print "Maybe IRB bug!\n" if irb_bug
|
179
|
+
end
|
180
|
+
if $SAFE > 2
|
181
|
+
abort "Error: irb does not work for $SAFE level higher than 2"
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
#
|
190
|
+
# Map these constants to symbols so the shell
|
191
|
+
# will pass them through
|
192
|
+
#
|
193
|
+
module Bombshell
|
194
|
+
module Shell
|
195
|
+
NORTH=:north
|
196
|
+
SOUTH=:south
|
197
|
+
EAST=:east
|
198
|
+
WEST=:west
|
199
|
+
end
|
200
|
+
end
|
201
|
+
# Tell shell not to show lower-case 'quit' method; it is aliased
|
202
|
+
# to #QUIT along w/ all the other upper-case methods.
|
203
|
+
::Bombshell::Shell::Commands::HIDE << :quit
|
204
|
+
::Bombshell::Shell::Commands::HIDE << :logger
|
@@ -0,0 +1,120 @@
|
|
1
|
+
#
|
2
|
+
# This class is a tabletop which is essentially a 2D
|
3
|
+
# array even though Ruby doesn't support 2D arrays
|
4
|
+
# like in other languages.
|
5
|
+
#
|
6
|
+
# As per the API instructions, (0,0) is considered
|
7
|
+
# to be the SOUTH WEST most corner.
|
8
|
+
#
|
9
|
+
module RubyRobot
|
10
|
+
class Tabletop
|
11
|
+
attr_reader :width, :height
|
12
|
+
|
13
|
+
def initialize(width, height)
|
14
|
+
# Store position of each piece
|
15
|
+
@width = width
|
16
|
+
@height = height
|
17
|
+
# Actually, this probably isn't necessary
|
18
|
+
# @playing_field = Array.new(@width) { Array.new(@height) }
|
19
|
+
# Hash with keys as the robot object and values are x/y coords
|
20
|
+
@robots = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def width_range
|
24
|
+
# Exclude width since positions are 0-based
|
25
|
+
(0...@width)
|
26
|
+
end
|
27
|
+
|
28
|
+
def height_range
|
29
|
+
# Exclude height since positions are 0-based
|
30
|
+
(0...@height)
|
31
|
+
end
|
32
|
+
|
33
|
+
def calculate_position(orig_position, direction_sym)
|
34
|
+
case direction_sym
|
35
|
+
# These are clockwise from north
|
36
|
+
when :north then orig_position[:y] += 1
|
37
|
+
when :east then orig_position[:x] += 1
|
38
|
+
when :south then orig_position[:y] -= 1
|
39
|
+
when :west then orig_position[:x] -= 1
|
40
|
+
end
|
41
|
+
orig_position
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# Return hash of x/y coords for a placed
|
46
|
+
# Robot, else raise PlacementError. Position state doesn't
|
47
|
+
# mean much to a Robot, by itself: only in
|
48
|
+
# relation to a Tabletop.
|
49
|
+
#
|
50
|
+
def position(robot)
|
51
|
+
raise PlacementError.new "Robot is not on this table" unless placed?(robot)
|
52
|
+
@robots[robot]
|
53
|
+
end
|
54
|
+
|
55
|
+
#
|
56
|
+
# Can the Robot move in the specified direction
|
57
|
+
# w/o falling off? Even though direction is a
|
58
|
+
# property of a specific Robot, pass it in here.
|
59
|
+
#
|
60
|
+
# Later this could be extended to support multiple
|
61
|
+
# Robots on a Tabletop. In that case, this and
|
62
|
+
# #move would need to be synchronized...
|
63
|
+
#
|
64
|
+
def move?(robot, direction_sym)
|
65
|
+
raise PlacementError.new "Robot is not on this table" unless placed?(robot)
|
66
|
+
possible_position = calculate_position(@robots[robot].clone, direction_sym)
|
67
|
+
|
68
|
+
# Is current_position on the board?
|
69
|
+
# Check in range (0..width).include?(x) and (0..height).include?(y)
|
70
|
+
return false if
|
71
|
+
!width_range.include?(possible_position[:x]) or
|
72
|
+
!height_range.include?(possible_position[:y])
|
73
|
+
true
|
74
|
+
end
|
75
|
+
|
76
|
+
#
|
77
|
+
# Move the robot in the specified direction.
|
78
|
+
#
|
79
|
+
def move(robot, direction_sym)
|
80
|
+
raise PlacementError.new "Robot is not on this table" unless placed?(robot)
|
81
|
+
new_position = calculate_position(@robots[robot].clone, direction_sym)
|
82
|
+
# Move the robot by placing it at its new location
|
83
|
+
place(robot, **new_position)
|
84
|
+
end
|
85
|
+
|
86
|
+
#
|
87
|
+
# Is this robot on this Tabletop?
|
88
|
+
#
|
89
|
+
def placed?(robot)
|
90
|
+
@robots.keys.include?(robot)
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# NYI: Implement for multiple Robots per Tabletop
|
95
|
+
#
|
96
|
+
def place?(robot, direction_sym)
|
97
|
+
true
|
98
|
+
end
|
99
|
+
|
100
|
+
#
|
101
|
+
# Place a robot on this board
|
102
|
+
#
|
103
|
+
def place(robot, x:0, y:0)
|
104
|
+
raise PlacementError.new "Coordinates (#{x},#{y}) are not on this board" if
|
105
|
+
!width_range.include?(x) || !height_range.include?(y)
|
106
|
+
# @playing_field[x][y] = robot
|
107
|
+
@robots[robot] = {x: x, y: y}
|
108
|
+
robot.place(self)
|
109
|
+
end
|
110
|
+
|
111
|
+
#
|
112
|
+
# Human-readable dump of Tabletop, with
|
113
|
+
# 2D array index {x:0,y:0} in the lower-left
|
114
|
+
# corner of the output.
|
115
|
+
#
|
116
|
+
def inspect
|
117
|
+
@playing_field
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,263 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'sinatra/swagger-exposer/swagger-exposer'
|
3
|
+
require 'ruby_robot'
|
4
|
+
require 'json-schema'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module RubyRobot
|
8
|
+
|
9
|
+
#
|
10
|
+
# SwaggerExposer turns out to have a few drawbacks; it doesn't support
|
11
|
+
# specifying enumerated string types.
|
12
|
+
#
|
13
|
+
USE_SWAGGER_EXPOSER=false
|
14
|
+
|
15
|
+
#
|
16
|
+
# Simple Sinatra webapp that supports:
|
17
|
+
#
|
18
|
+
# Run HTTP GET on /swagger.json to fetch a static JSON OpenAPI description
|
19
|
+
# for this webapp (suitable for use with swagger.io's GUI).
|
20
|
+
#
|
21
|
+
class Webapp < Sinatra::Base
|
22
|
+
include ::RubyRobot::SchemaLoader
|
23
|
+
|
24
|
+
set :public_folder, File.expand_path(File.join(File.dirname(__FILE__), '..', 'public'))
|
25
|
+
enable :static
|
26
|
+
|
27
|
+
# Tell sinatra to listen on all interfaces if it detects
|
28
|
+
# it is running on a docker container...otherwise it
|
29
|
+
# will just bind to the loopback interface which can't be
|
30
|
+
# exposed from the container.
|
31
|
+
set :bind, '0.0.0.0' if File.exist?('/.dockerenv')
|
32
|
+
|
33
|
+
REPORT_EXAMPLE_OBJ = {x:1,y:1,direction: :NORTH}
|
34
|
+
REPORT_EXAMPLE = REPORT_EXAMPLE_OBJ.to_json
|
35
|
+
|
36
|
+
ERR_PLACEMENT_MSG = 'Robot is not currently placed'
|
37
|
+
|
38
|
+
def request_schema
|
39
|
+
@@request_schema ||= schema = load_schema('request')
|
40
|
+
end
|
41
|
+
|
42
|
+
def response_schema
|
43
|
+
@@response_schema ||= schema = load_schema('response')
|
44
|
+
end
|
45
|
+
|
46
|
+
def max_error_message_length
|
47
|
+
# Grab from the schema
|
48
|
+
(response_schema['definitions']['Error']['properties']['message']['maxLength'] || 1024)
|
49
|
+
end
|
50
|
+
|
51
|
+
if USE_SWAGGER_EXPOSER
|
52
|
+
register Sinatra::SwaggerExposer
|
53
|
+
#
|
54
|
+
# Swagger general info
|
55
|
+
#
|
56
|
+
general_info(
|
57
|
+
{
|
58
|
+
version: ::RubyRobot::VERSION,
|
59
|
+
title: 'RubyRobot',
|
60
|
+
description: 'Web interface to RubyRobot API',
|
61
|
+
}
|
62
|
+
)
|
63
|
+
|
64
|
+
#
|
65
|
+
# Swagger types
|
66
|
+
#
|
67
|
+
type 'Error', {
|
68
|
+
:required => [:code, :message],
|
69
|
+
:properties => {
|
70
|
+
:code => {
|
71
|
+
:type => Integer,
|
72
|
+
:example => 400,
|
73
|
+
:description => 'The error code',
|
74
|
+
},
|
75
|
+
:message => {
|
76
|
+
:type => String,
|
77
|
+
:example => ERR_PLACEMENT_MSG,
|
78
|
+
:description => 'The error message',
|
79
|
+
}
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
type 'Report', {
|
84
|
+
required: [:x, :y, :direction],
|
85
|
+
properties: {
|
86
|
+
x: {
|
87
|
+
type: Integer,
|
88
|
+
example: 0
|
89
|
+
},
|
90
|
+
y: {
|
91
|
+
type: Integer,
|
92
|
+
example: 0
|
93
|
+
},
|
94
|
+
direction: {
|
95
|
+
type: String,
|
96
|
+
example: "NORTH"
|
97
|
+
}
|
98
|
+
}
|
99
|
+
}
|
100
|
+
end # if USE_SWAGGER_EXPOSER
|
101
|
+
|
102
|
+
#
|
103
|
+
# "Normal" sinatra/ruby code.
|
104
|
+
#
|
105
|
+
def robot
|
106
|
+
@@robot ||= proc {
|
107
|
+
rr = ::RubyRobot::Shell.new
|
108
|
+
# In the webapp, don't log the REPORT messages to stdout
|
109
|
+
rr.logger.formatter = proc { |severity, datetime, progname, msg| "" }
|
110
|
+
rr
|
111
|
+
}.call
|
112
|
+
end
|
113
|
+
|
114
|
+
def not_placed_error
|
115
|
+
[400, {code: 400, message: ERR_PLACEMENT_MSG}.to_json]
|
116
|
+
end
|
117
|
+
|
118
|
+
def formatted_report
|
119
|
+
r = robot.REPORT(false)
|
120
|
+
if !r.nil?
|
121
|
+
r[:direction] = r[:direction].upcase unless r[:direction].nil?
|
122
|
+
end
|
123
|
+
r
|
124
|
+
end
|
125
|
+
|
126
|
+
def position_report
|
127
|
+
# Pass along the report, but the direction needs to be upcased to
|
128
|
+
# comply w/ the JSON schema for the web API
|
129
|
+
[200, formatted_report.to_json]
|
130
|
+
end
|
131
|
+
|
132
|
+
if USE_SWAGGER_EXPOSER
|
133
|
+
endpoint_description 'Place the robot'
|
134
|
+
endpoint_parameter :body, "Robot placement specification object", :body, true, 'Report', {
|
135
|
+
example: REPORT_EXAMPLE_OBJ
|
136
|
+
}
|
137
|
+
endpoint_response 200, 'Report', 'Successful placement'
|
138
|
+
endpoint_response 400, 'Error', ERR_PLACEMENT_MSG
|
139
|
+
endpoint_tags 'Robot'
|
140
|
+
end # if USE_SWAGGER_EXPOSER
|
141
|
+
post '/place' do
|
142
|
+
content_type :json
|
143
|
+
request_params = nil
|
144
|
+
result = nil
|
145
|
+
begin
|
146
|
+
# Parse JSON args
|
147
|
+
request.body.rewind
|
148
|
+
request_params = JSON.parse(request.body.read)
|
149
|
+
# Validate input against JSON schema
|
150
|
+
json_schema_errors = JSON::Validator.fully_validate(request_schema, request_params)
|
151
|
+
body_json = {code: 400, message: "Bad request: #{json_schema_errors.join('; ')}"[0...max_error_message_length] }.to_json
|
152
|
+
body body_json
|
153
|
+
return [400, body_json] unless json_schema_errors.empty?
|
154
|
+
# Call robot#PLACE: inputs have already been validated by the JSON schema
|
155
|
+
robot.PLACE(request_params['x'], request_params['y'], (request_params['direction']))
|
156
|
+
body_json = formatted_report.to_json
|
157
|
+
body body_json
|
158
|
+
[200, body_json]
|
159
|
+
rescue
|
160
|
+
# TODO: log failing request details to enterprise logging...
|
161
|
+
# STDERR.puts $!
|
162
|
+
body_json = {code: 400, message: "Bad request (#{$!})"[0...max_error_message_length]}.to_json
|
163
|
+
body body_json
|
164
|
+
[400, body_json]
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
if USE_SWAGGER_EXPOSER
|
169
|
+
endpoint_description 'Move the robot'
|
170
|
+
endpoint_response 200, 'Report', REPORT_EXAMPLE
|
171
|
+
endpoint_response 400, 'Error', ERR_PLACEMENT_MSG
|
172
|
+
endpoint_tags 'Robot'
|
173
|
+
end # if USE_SWAGGER_EXPOSER
|
174
|
+
post '/move' do
|
175
|
+
content_type :json
|
176
|
+
if robot.REPORT.nil?
|
177
|
+
not_placed_error
|
178
|
+
else
|
179
|
+
robot.MOVE
|
180
|
+
position_report
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
if USE_SWAGGER_EXPOSER
|
185
|
+
endpoint_description 'Turn the robot left'
|
186
|
+
endpoint_response 200, 'Report', REPORT_EXAMPLE
|
187
|
+
endpoint_response 400, 'Error', ERR_PLACEMENT_MSG
|
188
|
+
endpoint_tags 'Robot'
|
189
|
+
end # if USE_SWAGGER_EXPOSER
|
190
|
+
post '/left' do
|
191
|
+
content_type :json
|
192
|
+
if robot.REPORT.nil?
|
193
|
+
not_placed_error
|
194
|
+
else
|
195
|
+
robot.LEFT
|
196
|
+
position_report
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
if USE_SWAGGER_EXPOSER
|
201
|
+
endpoint_description 'Turn the robot right'
|
202
|
+
endpoint_response 200, 'Report', REPORT_EXAMPLE
|
203
|
+
endpoint_response 400, 'Error', ERR_PLACEMENT_MSG
|
204
|
+
endpoint_tags 'Robot'
|
205
|
+
end # if USE_SWAGGER_EXPOSER
|
206
|
+
post '/right' do
|
207
|
+
content_type :json
|
208
|
+
if robot.REPORT.nil?
|
209
|
+
not_placed_error
|
210
|
+
else
|
211
|
+
robot.RIGHT
|
212
|
+
position_report
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
if USE_SWAGGER_EXPOSER
|
217
|
+
endpoint_description "Report the robot's position and orientation"
|
218
|
+
endpoint_response 200, 'Report', REPORT_EXAMPLE
|
219
|
+
endpoint_response 400, 'Error', ERR_PLACEMENT_MSG
|
220
|
+
endpoint_tags 'Robot'
|
221
|
+
end # if USE_SWAGGER_EXPOSER
|
222
|
+
get '/report' do
|
223
|
+
content_type :json
|
224
|
+
if robot.REPORT.nil?
|
225
|
+
not_placed_error
|
226
|
+
else
|
227
|
+
position_report
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
if !USE_SWAGGER_EXPOSER
|
232
|
+
get '/' do
|
233
|
+
redirect '/index.html'
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
post '/remove' do
|
238
|
+
@@robot = nil
|
239
|
+
[200]
|
240
|
+
end
|
241
|
+
|
242
|
+
#
|
243
|
+
# For now, just validate the response for POST /place to show
|
244
|
+
# responses can be validated within a Sinatra URL handler.
|
245
|
+
#
|
246
|
+
after '/place' do
|
247
|
+
# Validate response
|
248
|
+
begin
|
249
|
+
obj = JSON.parse(body.first)
|
250
|
+
unless JSON::Validator.validate(response_schema, obj)
|
251
|
+
# TODO: Enteprise logging
|
252
|
+
# Print out the failing constraints
|
253
|
+
STDERR.puts JSON::Validator.fully_validate(response_schema, obj)
|
254
|
+
STDERR.puts "Return value doesn't match response.schema.json: #{body.first}"
|
255
|
+
end
|
256
|
+
rescue
|
257
|
+
# TODO: Enterprise logging here...
|
258
|
+
STDERR.puts "Error (#{$!}) parsing JSON: '#{body}'"
|
259
|
+
end
|
260
|
+
# STDERR.puts "Failed validation" if JSON::Validator.validate(@response_schema, obj)
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|