Skip to content

Commit 54ce845

Browse files
committed
Add package "restful"
1 parent 1a1d13f commit 54ce845

7 files changed

Lines changed: 586 additions & 0 deletions

File tree

scripts/restful/README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Restful (REST)
2+
3+
A Minecraft scripting api port of REST APIs are used to access and manipulate data using a common set of stateless operations.
4+
5+
This package is experimental.
6+
7+
## Example
8+
9+
```js
10+
import { world } from "@minecraft/server";
11+
import { RequestMethod, REST } from "./index";
12+
13+
const rest = new REST('demo'); // id is demo, lower case
14+
15+
(async () => {
16+
await rest.request('/players', { method: RequestMethod.POST }); // create a route
17+
18+
// save data for all players into a table
19+
for (const player of world.getAllPlayers()) {
20+
await rest.request('/players', {
21+
method: RequestMethod.PUT,
22+
key: player.name,
23+
value: player.id
24+
});
25+
};
26+
})().catch(console.error);
27+
28+
world.events.chat.subscribe((event) => {
29+
/**
30+
* Get player id from REST
31+
*/
32+
const playerId = rest.request('/players', {
33+
method: RequestMethod.GET,
34+
key: event.sender.name
35+
});
36+
event.sender.tell('Player ID: ' + playerId);
37+
})
38+
```

scripts/restful/encoding.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// base64 character set, plus padding character (=)
2+
const b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
3+
// Regular expression to check formal correctness of base64 encoded strings
4+
b64re = /^(?:[A-Za-z\d+\/]{4})*?(?:[A-Za-z\d+\/]{2}(?:==)?|[A-Za-z\d+\/]{3}=?)?$/;
5+
function btoa(string) {
6+
string = String(string);
7+
var bitmap, a, b, c, result = "", i = 0, rest = string.length % 3; // To determine the final padding
8+
for (; i < string.length;) {
9+
if ((a = string.charCodeAt(i++)) > 255
10+
|| (b = string.charCodeAt(i++)) > 255
11+
|| (c = string.charCodeAt(i++)) > 255)
12+
throw new TypeError("Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range.");
13+
bitmap = (a << 16) | (b << 8) | c;
14+
result += b64.charAt(bitmap >> 18 & 63) + b64.charAt(bitmap >> 12 & 63)
15+
+ b64.charAt(bitmap >> 6 & 63) + b64.charAt(bitmap & 63);
16+
}
17+
// If there's need of padding, replace the last 'A's with equal signs
18+
return rest ? result.slice(0, rest - 3) + "===".substring(rest) : result;
19+
}
20+
;
21+
function atob(string) {
22+
// atob can work with strings with whitespaces, even inside the encoded part,
23+
// but only \t, \n, \f, \r and ' ', which can be stripped.
24+
string = String(string).replace(/[\t\n\f\r ]+/g, "");
25+
if (!b64re.test(string))
26+
throw new TypeError("Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.");
27+
// Adding the padding if missing, for semplicity
28+
string += "==".slice(2 - (string.length & 3));
29+
var bitmap, result = "", r1, r2, i = 0;
30+
for (; i < string.length;) {
31+
bitmap = b64.indexOf(string.charAt(i++)) << 18 | b64.indexOf(string.charAt(i++)) << 12
32+
| (r1 = b64.indexOf(string.charAt(i++))) << 6 | (r2 = b64.indexOf(string.charAt(i++)));
33+
result += r1 === 64 ? String.fromCharCode(bitmap >> 16 & 255)
34+
: r2 === 64 ? String.fromCharCode(bitmap >> 16 & 255, bitmap >> 8 & 255)
35+
: String.fromCharCode(bitmap >> 16 & 255, bitmap >> 8 & 255, bitmap & 255);
36+
}
37+
return result;
38+
}
39+
;
40+
function utf8_to_b64(str) {
41+
return btoa(unescape(encodeURIComponent(str)));
42+
}
43+
function b64_to_utf8(str) {
44+
return decodeURIComponent(escape(atob(str)));
45+
}
46+
;
47+
export { utf8_to_b64, b64_to_utf8 };

scripts/restful/encoding.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// base64 character set, plus padding character (=)
2+
const b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
3+
// Regular expression to check formal correctness of base64 encoded strings
4+
b64re = /^(?:[A-Za-z\d+\/]{4})*?(?:[A-Za-z\d+\/]{2}(?:==)?|[A-Za-z\d+\/]{3}=?)?$/;
5+
6+
function btoa (string: string) {
7+
string = String(string);
8+
var bitmap: number, a: number, b: number, c: number,
9+
result = "", i = 0,
10+
rest = string.length % 3; // To determine the final padding
11+
12+
for (; i < string.length;) {
13+
if ((a = string.charCodeAt(i++)) > 255
14+
|| (b = string.charCodeAt(i++)) > 255
15+
|| (c = string.charCodeAt(i++)) > 255)
16+
throw new TypeError("Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range.");
17+
18+
bitmap = (a << 16) | (b << 8) | c;
19+
result += b64.charAt(bitmap >> 18 & 63) + b64.charAt(bitmap >> 12 & 63)
20+
+ b64.charAt(bitmap >> 6 & 63) + b64.charAt(bitmap & 63);
21+
}
22+
23+
// If there's need of padding, replace the last 'A's with equal signs
24+
return rest ? result.slice(0, rest - 3) + "===".substring(rest) : result;
25+
};
26+
27+
function atob(string: string) {
28+
// atob can work with strings with whitespaces, even inside the encoded part,
29+
// but only \t, \n, \f, \r and ' ', which can be stripped.
30+
string = String(string).replace(/[\t\n\f\r ]+/g, "");
31+
if (!b64re.test(string))
32+
throw new TypeError("Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.");
33+
34+
// Adding the padding if missing, for semplicity
35+
string += "==".slice(2 - (string.length & 3));
36+
var bitmap: number, result = "", r1: number, r2: number, i = 0;
37+
for (; i < string.length;) {
38+
bitmap = b64.indexOf(string.charAt(i++)) << 18 | b64.indexOf(string.charAt(i++)) << 12
39+
| (r1 = b64.indexOf(string.charAt(i++))) << 6 | (r2 = b64.indexOf(string.charAt(i++)));
40+
41+
result += r1 === 64 ? String.fromCharCode(bitmap >> 16 & 255)
42+
: r2 === 64 ? String.fromCharCode(bitmap >> 16 & 255, bitmap >> 8 & 255)
43+
: String.fromCharCode(bitmap >> 16 & 255, bitmap >> 8 & 255, bitmap & 255);
44+
}
45+
return result;
46+
};
47+
48+
function utf8_to_b64(str: string) {
49+
return btoa(unescape(encodeURIComponent(str)));
50+
}
51+
52+
function b64_to_utf8(str: string) {
53+
return decodeURIComponent(escape(atob(str)));
54+
};
55+
56+
export { utf8_to_b64, b64_to_utf8 };

scripts/restful/index.js

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { world } from '@minecraft/server';
2+
import { b64_to_utf8, utf8_to_b64 } from './encoding';
3+
;
4+
export class RESTError extends Error {
5+
}
6+
;
7+
;
8+
globalThis.KEY = 'jayly-restful';
9+
export class Table {
10+
/**
11+
* @internal
12+
*/
13+
toRawtext() {
14+
const rawData = JSON.stringify({ route: this.route, data: this.data });
15+
return utf8_to_b64(rawData);
16+
}
17+
;
18+
constructor(rawtext) {
19+
try {
20+
const { route, data } = JSON.parse(b64_to_utf8(rawtext));
21+
this.route = route;
22+
this.data = data;
23+
}
24+
catch (error) {
25+
const { route, data } = JSON.parse(rawtext);
26+
this.route = route;
27+
this.data = data;
28+
}
29+
}
30+
;
31+
}
32+
;
33+
export class Member {
34+
constructor(key, value) {
35+
this.key = key;
36+
this.value = value;
37+
}
38+
;
39+
}
40+
;
41+
/**
42+
* Request methods available with Rest API
43+
*/
44+
export var RequestMethod;
45+
(function (RequestMethod) {
46+
/**
47+
* POST requests are commonly used to create a new resource that is a
48+
* subordinate of the specified route.
49+
*/
50+
RequestMethod["POST"] = "POST";
51+
/**
52+
* GET requests are commonly used to retrieve information about a resource
53+
* at the specified route.
54+
*/
55+
RequestMethod["GET"] = "GET";
56+
/**
57+
* PUT requests are commonly used to update a single resource that already
58+
* exists in a resource collection.
59+
*/
60+
RequestMethod["PUT"] = "PUT";
61+
/**
62+
* PATCH requests are commonly used to update partial of an already
63+
* existed resource collection.
64+
*/
65+
RequestMethod["PATCH"] = "PATCH";
66+
/**
67+
* POST requests are commonly used to remove an existing resource that is a
68+
* subordinate of the specified route.
69+
*/
70+
RequestMethod["DELETE"] = "DELETE";
71+
})(RequestMethod || (RequestMethod = {}));
72+
;
73+
export class REST {
74+
constructor(id) {
75+
const regex = /^[a-z]+$/;
76+
if (!regex.test(id))
77+
throw new RESTError('Invalid id.');
78+
const objectiveId = id;
79+
this.scoreboard = world.scoreboard.getObjective(objectiveId) ?? world.scoreboard.addObjective(objectiveId, id);
80+
}
81+
;
82+
request(route, options) {
83+
const participant = this.scoreboard.getParticipants().find((value) => new Table(value.displayName).route === route);
84+
// delete route
85+
if (options.method === RequestMethod.DELETE) {
86+
if (!participant)
87+
throw new RESTError('Route not found.');
88+
const parsedOption = options;
89+
if (typeof parsedOption.key !== 'string')
90+
this.scoreboard.removeParticipant(participant);
91+
else {
92+
const table = new Table(participant.displayName);
93+
const memberIndex = table.data.findIndex((value) => value.key === parsedOption.key);
94+
if (memberIndex === -1)
95+
throw new RESTError('Member not found in route ' + route);
96+
// set value
97+
table.data.splice(memberIndex, 1);
98+
const encrypted = table.toRawtext();
99+
// version increment
100+
const version = participant.getScore(this.scoreboard);
101+
(async () => {
102+
await world.getDimension('overworld')
103+
.runCommandAsync(`scoreboard players set ${JSON.stringify(encrypted)} ${JSON.stringify(this.scoreboard.id)} ${version + 1}`);
104+
});
105+
}
106+
;
107+
}
108+
// get member value from route
109+
else if (options.method === RequestMethod.GET) {
110+
const parsedOption = options;
111+
if (!participant)
112+
throw new RESTError('Route not found.');
113+
const { data: members } = new Table(participant.displayName);
114+
const data = members.find((v) => v.key === parsedOption.key);
115+
if (!data)
116+
throw new RESTError('Member not found in route ' + route);
117+
return data.value;
118+
}
119+
// modify member value from route
120+
else if (options.method === RequestMethod.PATCH) {
121+
const parsedOption = options;
122+
if (!participant)
123+
throw new RESTError('Route not found.');
124+
const table = new Table(participant.displayName);
125+
const member = table.data.find((value) => value.key === parsedOption.key);
126+
if (!member)
127+
throw new RESTError('Member not found in route ' + route);
128+
// set value
129+
member.value = parsedOption.value;
130+
const encrypted = table.toRawtext();
131+
// version increment
132+
const version = participant.getScore(this.scoreboard);
133+
return new Promise((resolve, reject) => {
134+
world.getDimension('overworld')
135+
.runCommandAsync(`scoreboard players set ${JSON.stringify(encrypted)} ${JSON.stringify(this.scoreboard.id)} ${version + 1}`)
136+
.catch(reject);
137+
resolve(version);
138+
});
139+
}
140+
// create a new route
141+
else if (options.method === RequestMethod.POST) {
142+
if (!!participant)
143+
throw new RESTError('Cannot create new route. Route ' + JSON.stringify(route) + ' already exists.');
144+
const rawtext = JSON.stringify({ route, data: [] });
145+
const table = new Table(rawtext.toString());
146+
return new Promise((resolve, reject) => {
147+
world.getDimension('overworld')
148+
.runCommandAsync(`scoreboard players set ${JSON.stringify(table.toRawtext())} ${JSON.stringify(this.scoreboard.id)} 1`)
149+
.then(() => resolve())
150+
.catch(reject);
151+
});
152+
}
153+
// create a new member from route
154+
else if (options.method === RequestMethod.PUT) {
155+
const parsedOption = options;
156+
if (!participant)
157+
throw new RESTError('Route not found.');
158+
const table = new Table(participant.displayName);
159+
if (table.data.findIndex((value) => value.key === parsedOption.key) > -1)
160+
throw new RESTError('Member exist in route ' + route);
161+
// set value
162+
const member = new Member(parsedOption.key, parsedOption.value);
163+
table.data.push(member);
164+
const encrypted = table.toRawtext();
165+
// version increment
166+
return new Promise((resolve, reject) => {
167+
world.getDimension('overworld')
168+
.runCommandAsync(`scoreboard players set ${JSON.stringify(encrypted)} ${JSON.stringify(this.scoreboard.id)} 1`)
169+
.then(() => resolve())
170+
.catch(reject);
171+
});
172+
}
173+
else
174+
throw new RESTError('Request method ' + options.method + ' not acceptable.');
175+
}
176+
;
177+
}
178+
;

0 commit comments

Comments
 (0)