Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
10d71ba
added initial webclient API that mirrors the Webserver API and uses t…
jurgenvinju May 11, 2026
645d334
cleanup and added POST method
jurgenvinju May 11, 2026
61cadea
added the other methods
jurgenvinju May 11, 2026
6cce6de
added progress bar
jurgenvinju May 11, 2026
17723ff
fixed post
jurgenvinju May 12, 2026
b9c1e6e
constructor typo
jurgenvinju May 12, 2026
8c9c0da
fix post bug
jurgenvinju May 12, 2026
ddf4bfa
added path to other requests kinds but GET and POST
jurgenvinju May 12, 2026
6b37e74
Merge branch 'main' into feat/webclient
jurgenvinju May 19, 2026
53ae8a0
started rewrite of Server and Client interface to canonically treat a…
jurgenvinju May 19, 2026
cd8bdda
big cleanup of Webclient, but Webserver is broken now and I still hav…
jurgenvinju May 21, 2026
8c51344
debugging with @davylandman
jurgenvinju May 21, 2026
748dcc7
linked up the Subscription API as well to complete the stream
jurgenvinju May 22, 2026
ae4eefe
improving error handling of common mistakes in the client
jurgenvinju May 22, 2026
9f7b1b5
factored out Writer-based suppliers
jurgenvinju May 22, 2026
06b20ad
added asserts to diagnose possible race
jurgenvinju May 22, 2026
e1f8367
error handling for bad URLs
jurgenvinju May 22, 2026
76abafe
removed dead use of parameter
jurgenvinju May 22, 2026
bee60e8
this seems to have fixed the race
jurgenvinju May 22, 2026
345585e
comments
jurgenvinju May 22, 2026
548aac6
deal with null messages of IOException generally
jurgenvinju May 22, 2026
db4c552
fixed off-by-one in download progress
jurgenvinju May 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/org/rascalmpl/exceptions/RuntimeExceptionFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@ public static Throw io(String msg) {
}

private static String mapIOException(IOException ex) {

var msg = ex.getMessage();
if (ex instanceof FileSystemException) {
// nio exceptions lack proper messages, they are encoded in the class name
Expand All @@ -466,12 +467,19 @@ private static String mapIOException(IOException ex) {
return "Not a directory: " + msg;
}
}

if (ex instanceof FileNotFoundException) {
// not all paths throw a proper message, they often only have the path name
return "No such file: " + msg;
}

if (msg == null || msg.length() == 0) {
// the class name has the information about what is going on
msg = ex.getClass().getSimpleName();
}

// otherwise fallback to the message
return msg;
return msg + (ex.getCause() != null ? (", due to: " + ex.getCause().getMessage()) : "");
}

public static Throw io(IOException ex) {
Expand Down
187 changes: 129 additions & 58 deletions src/org/rascalmpl/library/Content.rsc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
module Content

import lang::json::IO;
import IO;

@synopsis{Content wraps the HTTP Request/Response API to support interactive visualization types
on the terminal.}
Expand Down Expand Up @@ -39,6 +40,36 @@ data Content
| content(Response response, str title="*static content*", ViewColumn viewColumn = normalViewColumn(1))
;

@synopsis{A static map with default MIME interpretations for particular file extensions.}
public map[str extension, str mimeType] mimeTypes = (
"json" :"application/json",
"css" : "text/css",
"htm" : "text/html",
"html" : "text/html",
"xml" : "text/xml",
"java" : "text/x-java-source, text/java",
"txt" : "text/plain",
"asc" : "text/plain",
"ico" : "image/x-icon",
"gif" : "image/gif",
"jpg" : "image/jpeg",
"jpeg" : "image/jpeg",
"png" : "image/png",
"mp3" : "audio/mpeg",
"m3u" : "audio/mpeg-url",
"mp4" : "video/mp4",
"ogv" : "video/ogg",
"flv" : "video/x-flv",
"mov" : "video/quicktime",
"swf" : "application/x-shockwave-flash",
"js" : "application/javascript",
"pdf" : "application/pdf",
"doc" : "application/msword",
"ogg" : "application/x-ogg",
"zip" : "application/octet-stream",
"exe" : "application/octet-stream",
"class" : "application/octet-stream"
);

@synopsis{Directly serve a static html page}
Content html(str html) = content(response(html));
Expand All @@ -51,9 +82,6 @@ Content file(loc src) = content(response(src));
@synopsis{Directly serve the contents of a string as plain text}
Content plainText(str text) = content(plain(text));

alias Body = value (type[value] expected);


@synopsis{Request values represent what a browser is asking for, most importantly the URL path.}
@description{
A request value also contains the full HTTP headers, the URL parameters as a `map[str,str]`
Expand All @@ -71,22 +99,77 @@ data Request (map[str, str] headers = (), map[str, str] parameters = (), map[str
| head(str path)
;


@synopsis{A response encodes what is send back from the server to the browser client.}
@description{
The three kinds of responses, encode either content that is already a `str`,
some file which is streamed directly from its source location or a jsonResponse
which involves a handy, automatic, encoding of Rascal values into json values.
}
data Response
= response(Status status, str mimeType, map[str,str] header, str content)
| fileResponse(loc file, str mimeType, map[str,str] header)
| jsonResponse(Status status, map[str,str] header, value val, str dateTimeFormat = "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'", JSONFormatter[value] formatter = str (value _) { fail; },
bool explicitConstructorNames=false, bool explicitDataTypes=false, bool dateTimeAsInt=true, bool rationalsAsString=false)
data Response = response(Status, str mimeType, map[str, str] header, Body body);

@synopsis{Bodies can be sent or received, depending on the context (client or)}
@description{
* put and post requests, when received by a server, receive bodies.
* put and post requests, when fetched by a client, sent bodies.
* a response y a server sends a body.
* a response that was fetched by a client receives a body.

The ((BodyKind)) encodes what we expect from the sender when it
puts the value onto the socket, and what we expect from the receiver
when it reads the contents off the socket. This is where builtin
conversions (formatters, parsers and validators) are activated on
the bridge between Rascal and the HTTP protocol.
}
data Body
= send(BodyKind kind, value source)
| receive(&T (BodyKind kind, type[&T] expect) receiver)
;

@synopsis{The type's of ((Body)) that we are sending or expecting to receive}
@description{
This interface bridges Rascal data to the HTTP protocol. Typically large input
such as (composite) strings and JSON code is _streamed_ onto the HTTP socket.
}
data BodyKind
= text()
| json(JSONOptions options=jsonOptions())
| file(loc storage=|unknown:///|)
;

data JSONOptions
= jsonOptions(
str dateTimeFormat = "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'",
JSONFormatter[value] formatter = str (value _) { fail; },
bool explicitConstructorNames=false,
bool explicitDataTypes=false,
bool dateTimeAsInt=true,
bool rationalsAsString=false
);

@synopsis{Convenience function for construction a JSON response value}
Response jsonResponse(Status status, map[str,str] header, value val, JSONOptions options = jsonOptions())
= response(status, "application/json", header, send(json(options=options), val));

@synopsis{Convenience function for construction a text response value}
Response response(Status status, str mimeType, map[str,str] header, str content)
= response(status, mimeType, header, send(text(), content));

@synopsis{Convenience function for file response value}
Response fileResponse(loc source, str mimeType, map[str,str] header)
= exists(source)
? response(ok(), mimeType, header, send(file(), source))
: response(notFound(), "text/plain", (), send(text(), "<file> not found."))
;

@synopsis{Convenience function for construction a file response value with automatic mimetype}
Response fileResponse(loc source, map[str,str] header)
= exists(source)
? response(ok(), mimeTypes[source.extension]?"text/plain", header, send(file(), source))
: response(notFound(), "text/plain", (), send(text(), "<source> not found."))
;

@synopsis{Utility to quickly render a string as HTML content}
Response response(str content, map[str,str] header = ()) = response(ok(), "text/html", header, content);
Response response(str content, map[str,str] header = ()) = response(ok(), "text/html", header, send(text(), content));

@synopsis{Utility to quickly report an HTTP error with a user-defined message}
Response response(Status status, str explanation, map[str,str] header = ()) = response(status, "text/plain", header, explanation);
Expand All @@ -108,56 +191,44 @@ default Response response(value val, map[str,str] header = ()) = jsonResponse(o
@benefits{
Fast way of producing JSON strings for embedded DSLs on the Rascal side.
}
Response response(value val, JSONFormatter[value] formatter, map[str,str] header = ()) = jsonResponse(ok(), header, val, formatter=formatter);
Response response(value val, JSONFormatter[value] formatter, map[str,str] header = ()) = jsonResponse(ok(), header, val, options=jsonOptions(formatter=formatter));

@synopsis{Encoding of HTTP status}
data Status
= ok()
| created()
| accepted()
| noContent()
| partialContent()
| redirect()
| notModified()
| badRequest()
| unauthorized()
| forbidden()
| notFound()
| rangeNotSatisfiable()
| internalError()
;

data Status
= ok()
| notFound()
| accepted()
| badRequest()
| conflict()
| created()
| expectationFailed()
| forbidden()
| found()
| gone()
| internalError()
| lengthRequired()
| methodNotAllowed()
| multiStatus()
| notAcceptible()
| notImplemented()
| notModified()
| noContent()
| partialContent()
| payloadTooLarge()
| preconditionFailed()
| rangeNotSatisfiable()
| redirect()
| redirectSeeOther()
| requestTimeout()
| serviceUnavailable()
| switchProtocol()
| temporaryRedirect()
| tooManyRequests()
| unauthorized()
| unsupportedHTTPVersion()
| unsupportedMediaType()
;

@synopsis{A static map with default MIME interpretations for particular file extensions.}
public map[str extension, str mimeType] mimeTypes = (
"json" :"application/json",
"css" : "text/css",
"htm" : "text/html",
"html" : "text/html",
"xml" : "text/xml",
"java" : "text/x-java-source, text/java",
"txt" : "text/plain",
"asc" : "text/plain",
"ico" : "image/x-icon",
"gif" : "image/gif",
"jpg" : "image/jpeg",
"jpeg" : "image/jpeg",
"png" : "image/png",
"mp3" : "audio/mpeg",
"m3u" : "audio/mpeg-url",
"mp4" : "video/mp4",
"ogv" : "video/ogg",
"flv" : "video/x-flv",
"mov" : "video/quicktime",
"swf" : "application/x-shockwave-flash",
"js" : "application/javascript",
"pdf" : "application/pdf",
"doc" : "application/msword",
"ogg" : "application/x-ogg",
"zip" : "application/octet-stream",
"exe" : "application/octet-stream",
"class" : "application/octet-stream"
);

@synopsis{Hint the IDE where to open the next web view or editor}
@description{
Expand Down
Loading
Loading