HTTP Library (#1220)

Add `Base.Net.Http` library
This commit is contained in:
Dmitry Bushev 2020-10-27 14:45:10 +03:00 committed by GitHub
parent c0de753d95
commit 11e4241921
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1776 additions and 23 deletions

View File

@ -40,6 +40,8 @@ jobs:
- uses: actions/checkout@v2
- name: Enable Developer Command Prompt (Windows)
uses: ilammy/msvc-dev-cmd@v1.4.1
- name: Setup Go
uses: actions/setup-go@v2
- name: Disable TCP/UDP Offloading (macOS)
if: runner.os == 'macOS'
shell: bash
@ -248,6 +250,12 @@ jobs:
cp manifest.yaml $ENGINE_DIST_DIR
# Test Distribution
- name: Prepare Engine Test Environment
shell: bash
run: |
go get -v github.com/ahmetb/go-httpbin/cmd/httpbin
$(go env GOPATH)/bin/httpbin -host :8080 &
- name: Test Engine Distribution (Unix)
shell: bash
if: runner.os != 'Windows'

View File

@ -75,6 +75,11 @@ type Math
Math.pi : Decimal
Math.pi = 3.141592653589793
## A pair of elements.
type Pair
type Pair first second
## Generic equality of arbitrary values.
Any.== : Any -> Boolean
Any.== that = if Meta.is_same_object this that then True else

View File

@ -0,0 +1,358 @@
from Base import all
import Base.Data.Json
import Base.Net.Proxy
import Base.Net.Uri
import Base.Net.Http.Form
import Base.Net.Http.Header
import Base.Net.Http.Method
import Base.Net.Http.Request
import Base.Net.Http.Request.Body as Request_Body
import Base.Net.Http.Response
import Base.Net.Http.Version
import Base.System.File
import Base.Time.Duration
import Base.Time.Time
polyglot java import java.time.Duration as Java_Duration
polyglot java import java.net.InetSocketAddress
polyglot java import java.net.ProxySelector
polyglot java import java.net.URI
polyglot java import java.net.http.HttpClient
polyglot java import java.net.http.HttpRequest
polyglot java import java.net.http.HttpResponse
polyglot java import org.enso.base.Http_Utils
type Http
type Http timeout follow_redirects proxy version
## Send an Options request.
> Example
Send an Options request.
Http.new.options "http://httpbin.org"
options : To_Uri -> Vector -> Response
options uri (headers = []) =
req = Request.options uri headers
this.request req
## Send a Get request.
> Example
Send a Get request.
Http.new.get "http://httpbin.org/get"
> Example
Send authenticated Get request (note the use of TLS).
Http.new.get "https://httpbin.org/basic-auth/user/pass" [Header.authorization_basic "user" "pass"]
> Example
Download a file.
out_file = File.new "/tmp/out.bin"
res = Http.new.get "http://httpbin.org/bytes/1024"
res.body.to_file out_file
get : To_Uri -> Vector -> Response
get uri (headers = []) =
req = Request.get uri headers
this.request req
## Send a Head request.
> Example
Send a Head request.
res = Http.new.head "http://httpbin.org"
IO.println res.headers
head : To_Uri -> Vector -> Response
head uri (headers = []) =
req = Request.head uri headers
this.request req
## Send a Post request.
> Example
Send a Post request with binary data.
body = Body.Bytes "Hello".utf_8
header_binary = Header.content_type "application/octet-stream"
Http.new.post "http://httpbin.org/post" body [header_binary]
post : To_Uri -> Request_Body -> Vector -> Respoonse
post uri body (headers = []) =
req = Request.post uri body headers
this.request req
## Send a Post request with the form. By default it will be encoded as
"application/x-www-form-urlencoded". To encode the form as
"multipart/form-data" add the appropriate header.
> Example
Send a Post request with form.
form = [Form.text_field "name" "John Doe", Form.file_field "license.txt" (Enso_Project.root / "LICENSE")]
Http.new.post_form "http://httpbin.org/post" form
> Example
Send a Post request with form encoded as "multipart/form-data".
form = [Form.text_field "name" "John Doe", Form.file_field "license.txt" (Enso_Project.root / "LICENSE")]
Http.new.post_form "http://httpbin.org/post" form [Header.multipart_form_data]
> Example
Configure HTTP client and send a Post request.
form = [Form.text_field "name" "John Doe", Form.file_field "license.txt" (Enso_Project.root / "LICENSE")]
http = Http.new (timeout = 30.seconds)
http.post_form "http://httpbin.org/post" form
post_form : To_Uri -> To_Form -> Vector -> Response
post_form uri parts (headers = []) =
new_headers = [Header.application_x_www_form_urlencoded]
req = Request.post uri (Request_Body.Form parts.to_form) new_headers . with_headers headers
this.request req
## Send a Post request with body with content-type "application/json".
> Example
Send a Post request with json data.
json = Json.parse <| '''
{"key":"val"}
Http.new.post_json "http://httpbin.org/post" json
post_json : To_Uri -> Json -> Vector -> Response
post_json uri body_json (headers = []) =
new_headers = [Header.application_json]
req = Request.post uri (Request_Body.Json body_json) headers . with_headers new_headers
this.request req
## Send a Put request.
> Example
Send a Put request with binary data.
body = Body.Bytes "contents".utf_8
header_binary = Header.content_type "application/octet-stream"
Http.new.put "http://httpbin.org/post" body [header_binary]
put : To_Uri -> Request_Body -> Vector -> Respoonse
put uri body (headers = []) =
req = Request.put uri body headers
this.request req
## Send a Put request with body with content-type "application/json".
> Example
Send a Put request with json data.
json = Json.parse <| '''
{"key":"val"}
Http.new.put_json "http://httpbin.org/put" json
put_json : To_Uri -> Json -> Vector -> Response
put_json uri body_json (headers = []) =
new_headers = [Header.application_json]
req = Request.put uri (Request_Body.Json body_json) headers . with_headers new_headers
this.request req
## Create a Delete request.
> Example
Send a Delete request.
Http.new.delete "http://httpbin.org/delete"
delete : To_Uri -> Vector -> Response
delete uri (headers = []) =
req = Request.delete uri headers
this.request req
## Create a request
> Example
Send a Get request with headers.
req = Request.new Method.Get "http://httpbin.org/get" . with_header "X-Trace-Id" "00000"
res = Http.new.request req
res.body
> Example
Open a connection and send a Post request with form.
req = Request.post "http://httpbin.org/post" . with_form [Form.text_field "key" "value"] . with_header "X-Trace-Id" "123456789"
res = http.new.request req
res.code
> Example
Send a Post request with urlencoded form data.
form = [Form.text_field "name" "John Doe", Form.file_field "license.txt" (Enso_Project.root / "LICENSE")]
req = Request.post "http://httpbin.org/post" . with_form form
Http.new.request req
> Example
Send a Post request with form encoded as "multipart/form-data".
form = [Form.text_field "name" "John Doe", Form.file_field "license.txt" (Enso_Project.root / "LICENSE")]
req = Request.post "http://httpbin.org/post" . with_form form . with_headers [Header.multipart_form_data]
Http.new.post req
> Example
Configure HTTP client and send a Post request with form.
form = [Form.text_field "name" "John Doe"]
req = Request.new Method.Post "http://httpbin.org/post" . with_form form
http = Http.new (timeout = 30.seconds) (proxy = Proxy.new "proxy.example.com:80")
http.request req
request : Request -> Response
request req =
body_publishers = Polyglot.get_member HttpRequest "BodyPublishers"
builder = HttpRequest.newBuilder []
# set uri
builder.uri [req.uri.internal_uri]
# prepare headers and body
req_with_body = case req.body of
Request_Body.Empty ->
Pair req (body_publishers.noBody [])
Request_Body.Text text ->
builder.header [Header.text_plain.name, Header.text_plain.value]
Pair req (body_publishers.ofString [text])
Request_Body.Json json ->
builder.header [Header.application_json.name, Header.application_json.value]
Pair req (body_publishers.ofString [json.to_text])
Request_Body.Form form ->
add_multipart form =
body_builder = Http_Utils.multipart_body_builder []
form.parts.map part-> case part.value of
Form.Part_Text text -> body_builder.add_part_text [part.key, text]
Form.Part_File file -> body_builder.add_part_file [part.key, file.path]
boundary = body_builder.get_boundary []
Pair (req.with_headers [Header.multipart_form_data boundary]) (body_builder.build [])
add_urlencoded form =
body_builder = Http_Utils.urlencoded_body_builder []
form.parts.map part-> case part.value of
Form.Part_Text text -> body_builder.add_part_text [part.key, text]
Form.Part_File file -> body_builder.add_part_file [part.key, file.path]
Pair req (body_builder.build [])
if req.headers.contains Header.multipart_form_data then add_multipart form else
add_urlencoded form
# method
req_http_method = case req.method of
Method.Options -> "OPTIONS"
Method.Get -> "GET"
Method.Post -> "POST"
Method.Put -> "PUT"
Method.Delete -> "DELETE"
Method.Trace -> "TRACE"
Method.Connect -> "CONNECT"
case req_with_body of
Pair req body ->
# set method and body
builder.method [req_http_method, body]
# set headers
req.headers.map h-> builder.header [h.name, h.value]
http_request = builder.build []
body_handler = Polyglot.get_member HttpResponse "BodyHandlers" . ofByteArray []
Response.response (this.internal_http_client.send [http_request, body_handler])
## PRIVATE
Build an HTTP client.
internal_http_client : HttpClient
internal_http_client =
builder = HttpClient.newBuilder []
# timeout
if this.timeout.is_date then Panic.throw (Time.time_error "Connection timeout does not support date intervals") else builder.connectTimeout [this.timeout.internal_duration]
# redirect
redirect = Polyglot.get_member HttpClient "Redirect"
redirect_policy = case this.follow_redirects of
True -> Polyglot.get_member redirect "ALWAYS"
False -> Polyglot.get_member redirect "NEVER"
builder.followRedirects [redirect_policy]
# proxy
case this.proxy of
Proxy.Proxy_Addr proxy_host proxy_port ->
proxy_selector = ProxySelector.of [InetSocketAddress.new [proxy_host, proxy_port].to_array]
Polyglot.invoke builder "proxy" [proxy_selector].to_array
Proxy.System ->
proxy_selector = ProxySelector.getDefault []
Polyglot.invoke builder "proxy" [proxy_selector].to_array
Proxy.None ->
Unit
# version
http_client_version = Polyglot.get_member HttpClient "Version"
case this.version of
Version.Http_1_1 ->
Polyglot.invoke builder "version" [http_client_version.valueOf ["HTTP_1_1"]].to_array
Version.Http_2 ->
Polyglot.invoke builder "version" [http_client_version.valueOf ["HTTP_2"]].to_array
# build http client
builder.build []
## Create a new instance of HTTP client.
> Example
Create an HTTP client with default settings.
Http.new
> Example
Create an HTTP client with extended timeout.
Http.new timeout=30.seconds
> Example
Create an HTTP client with extended timeout and proxy settings.
Http.new (timeout = 30.seconds) (proxy = Proxy.new "example.com" 8080)
new : Duration -> Boolean -> Proxy -> Http
new (timeout = 10.seconds) (follow_redirects = True) (proxy = Proxy.System) (version = Version.Http_1_1) =
Http timeout follow_redirects proxy version
## Send an Options request.
> Example
Send an Options request.
Http.options "http://httpbin.org"
options : To_Uri -> Vector -> Response
options uri (headers = []) = here.new.options uri headers
## Send a Get request.
> Example
Send a Get request.
Http.get "http://httpbin.org/get"
> Example
Send authenticated Get request (note the use of TLS).
Http.get "https://httpbin.org/basic-auth/user/pass" [Header.authorization_basic "user" "pass"]
> Example
Download a file.
out_file = File.new "/tmp/out.bin"
res = Http.get "http://httpbin.org/bytes/1024"
res.body.to_file out_file
get : To_Uri -> Vector -> Response
get uri (headers = []) = here.new.get uri headers
## Send a Head request.
> Example
Send a Head request.
res = Http.head "http://httpbin.org"
IO.println res.headers
head : To_Uri -> Vector -> Response
head uri (headers = []) = here.new.options uri headers
## Send a Post request.
> Example
Send a Post request with binary data.
body = Body.Bytes "Hello".utf_8
header_binary = Header.content_type "application/octet-stream"
Http.post "http://httpbin.org/post" body [header_binary]
post : To_Uri -> Request_Body -> Vector -> Respoonse
post uri body (headers = []) = here.new.post uri body headers
## Send a Post request with the form. By default it will be encoded as
"application/x-www-form-urlencoded". To encode the form as
"multipart/form-data" add the appropriate header.
> Example
Send a Post request with form.
form = [Form.text_field "name" "John Doe", Form.file_field "license.txt" (Enso_Project.root / "LICENSE")]
Http.post_form "http://httpbin.org/post" form
> Example
Send a Post request with form encoded as "multipart/form-data".
form = [Form.text_field "name" "John Doe", Form.file_field "license.txt" (Enso_Project.root / "LICENSE")]
Http.post_form "http://httpbin.org/post" form [Header.multipart_form_data]
post_form : To_Uri -> To_Form -> Vector -> Response
post_form uri parts (headers = []) = here.new.post_form uri parts headers
## Send a Post request with body with content-type "application/json".
> Example
Send a Post request with json data.
json = Json.parse <| '''
{"key":"val"}
Http.post_json "http://httpbin.org/post" json
post_json : To_Uri -> Json -> Vector -> Response
post_json uri body_json (headers = []) = here.new.post_json uri body_json headers

View File

@ -0,0 +1,47 @@
from Base import all
import Base.Vector
type To_Form
type To_Form internal_to_form
to_form : Form
to_form = this.internal_to_form
## Implement To_Form for vector
Vector.Vector.to_form = Form this
## The HTTP form containing a vector of parts.
type Form
type Form parts
## Implement To_Form.to_form
to_form : Form
to_form = this
## The key-value element of the form.
type Part
type Part key value
## The value of the form element.
type Part_Value
type Part_Text part_text
type Part_File part_file
## Create Form data from Parts
new : Vector -> Form
new parts = Form parts
# Helpers for creating different parts of the form.
## Create a text field of a Form.
text_field : Text -> Text -> Part
text_field key val = Part key (Part_Text val)
## Create a file field of a Form.
file_field : Text -> Text -> Part
file_field key file = Part key (Part_File file)

View File

@ -0,0 +1,68 @@
from Base import all
polyglot java import org.enso.base.Http_Utils
type Header
type Header name value
## Header equality.
== : Header -> Boolean
== that = (this.name.equals_ignore_case that.name) && this.value==that.value
## Create a new Header.
new : Text -> Text -> Header
new name value = Header name value
# Accept
## Create "Accept" header.
accept : Text -> Header
accept value = Header "Accept" value
## Header "Accept: */*".
accept_all : Header
accept_all = here.accept "*/*"
# Authorization
## Create "Authorization" header.
authorization : Text -> Header
authorization value = Header "Authorization" value
## Create HTTP basic auth header.
> Example
Create basic auth header.
Header.authorization_basic "user" "pass"
authorization_basic : Text -> Text -> Header
authorization_basic user pass =
here.authorization (Http_Utils.header_basic_auth [user, pass])
# Content-Type
## Create "Content-Type" header.
content_type : Text -> Header
content_type value = Header "Content-Type" value
## Header "Content-Type: application/json".
application_json : Header
application_json = here.content_type "application/json"
## Header "Content-Type: application/octet-stream".
application_octet_stream : Header
application_octet_stream = here.content_type "application/octet-stream"
## Header "Content-Type: application/x-www-form-urlencoded".
application_x_www_form_urlencoded : Header
application_x_www_form_urlencoded = here.content_type "application/x-www-form-urlencoded"
## Header "Content-Type: multipart/form-data".
multipart_form_data : Text -> Header
multipart_form_data (boundary = "") =
if boundary == "" then here.content_type "multipart/form-data" else
here.content_type ("multipart/form-data; boundary=" + boundary)
## Header "Content-Type: text/plain".
text_plain : Header
text_plain = here.content_type "text/plain"

View File

@ -0,0 +1,10 @@
type Method
type Options
type Get
type Head
type Post
type Put
type Delete
type Trace
type Connect

View File

@ -0,0 +1,79 @@
from Base import all
import Base.Vector
import Base.Net.Uri
import Base.Net.Http.Form
import Base.Net.Http.Header
import Base.Net.Http.Method
import Base.Net.Http.Request.Body as Request_Body
import Base.System.File
polyglot java import org.enso.base.Text_Utils
type Request
type Request method uri headers body
## Set header.
with_header : Text -> Text -> Request
with_header key val =
new_header = Header.new key val
update_header p h = case p of
Pair acc True -> Pair (acc + [h]) True
Pair acc False ->
if Text_Utils.equals_ignore_case [h.name, key] then Pair (acc + [new_header]) True else Pair (acc + [h]) False
new_headers = case this.headers.fold (Pair [] False) update_header of
Pair acc True -> acc
Pair acc False -> acc + [new_header]
Request this.method this.uri new_headers this.body
## Set headers.
with_headers : [Header] -> Request
with_headers new_headers =
update_header req new_header = req.with_header new_header.name new_header.value
new_headers.fold this update_header
## Set body.
with_body : Request_Body -> Request
with_body new_body = Request this.method this.uri this.headers new_body
## Set body encoded as "application/json".
with_json : Text -> Request
with_json json_body =
new_body = Request_Body.Json json_body
Request this.method this.uri this.headers new_body . with_headers [Header.application_json]
## Set body as vector of parts encoded as
"application/x-www-form-urlencoded".
with_form : To_Form -> Request
with_form parts =
new_body = Request_Body.Form parts.to_form
Request this.method this.uri this.headers new_body . with_headers [Header.application_x_www_form_urlencoded]
## Create new HTTP request.
new : Method -> To_Uri -> Vector -> Request_Body -> Request
new method addr (headers = []) (body = Request_Body.Empty) =
Request method (Uri.panic_on_error (addr.to_uri)) headers body
## Create an Options request.
options : To_Uri -> Vector -> Request
options addr (headers = []) = here.new Method.Options addr headers
## Create a Get request.
get : To_Uri -> Vector -> Request
get addr (headers = []) = here.new Method.Get addr headers
## Create a Head request.
head : To_Uri -> Vector -> Request
head addr (headers = []) = here.new Method.Head addr headers
## Create a Post request.
post : To_Uri -> Request_Body -> Vector -> Request
post addr body (headers = []) = here.new Method.Post addr headers body
## Create a Put request.
put : To_Uri -> Request_Body -> Vector -> Request
put addr body (headers = []) = here.new Method.Put addr headers body
## Create a Delete request.
delete : To_Uri -> Vector -> Request
delete addr (headers = []) = here.new Method.Delete addr headers

View File

@ -0,0 +1,22 @@
from Base import all
## The HTTP request body.
type Body
## Empty request body.
type Empty
## Request body with text.
type Text text
## Request body with JSON.
type Json json
## Request body with form data.
type Form form
## Request body with file data.
type File file
## Request body with binary.
type Bytes bytes

View File

@ -0,0 +1,26 @@
from Base import all
import Base.Net.Http.Header
import Base.Net.Http.Response.Body as Response_Body
import Base.Net.Http.Status_Code
import Base.Vector
polyglot java import org.enso.base.Http_Utils
type Response
type Response internal_http_response
## Get the response headers.
headers : Vector
headers =
header_entries = Vector.vector (Http_Utils.get_headers [this.internal_http_response.headers []])
header_entries.map e-> Header.new (e.getKey []) (e.getValue [])
## Get the response body.
body : Response_Body
body = Response_Body.body (Vector.vector (this.internal_http_response.body []))
## Get the response status code.
code : Status_Code
code = Status_Code.status_code (this.internal_http_response.statusCode [])

View File

@ -0,0 +1,22 @@
from Base import all
import Base.Data.Json
import Base.System.File
type Body
## Response body
type Body bytes
## Convert response body to Text.
to_text : Text
to_text = Text.from_utf_8 this.bytes
## Convert response body to Json.
to_json : Json
to_json = Json.parse this.to_text
## Write response body to a File.
to_file : File -> File
to_file path =
path.write_bytes this.bytes
path

View File

@ -0,0 +1,166 @@
from Base import all
type Status_Code
## HTTP status code.
type Status_Code code
## 100 Continue.
continue : Status_Code
continue = Status_Code 100
## 101 Switching Protocols.
switching_protocols : Status_Code
switching_protocols = Status_Code 101
## 200 OK.
ok : Status_Code
ok = Status_Code 200
## 201 Created.
created : Status_Code
created = Status_Code 201
## 202 Accepted.
accepted : Status_Code
accepted = Status_Code 202
## 203 Non-Authoritative Information.
non_authoritative_information : Status_Code
non_authoritative_information = Status_Code 203
## 204 No Content.
no_content : Status_Code
no_content = Status_Code 204
## 205 Reset Content.
reset_content : Status_Code
reset_content = Status_Code 205
## 206 Partial Content.
partial_content : Status_Code
partial_content = Status_Code 206
## 300 Multiple Choices.
multiple_choices : Status_Code
multiple_choices = Status_Code 300
## 301 Moved Permanently.
moved_permanently : Status_Code
moved_permanently = Status_Code 301
## 302 Found.
found : Status_Code
found = Status_Code 302
## 303 See Other.
see_other : Status_Code
see_other = Status_Code 303
## 304 Not Modified.
not_modified : Status_Code
not_modified = Status_Code 304
## 305 Use Proxy.
use_proxy : Status_Code
use_proxy = Status_Code 305
## 307 Temporary Redirect.
temporary_redirect : Status_Code
temporary_redirect = Status_Code 307
## 400 Bad Request.
bad_request : Status_Code
bad_request = Status_Code 400
## 401 Unauthorized.
unauthorized : Status_Code
unauthorized = Status_Code 401
## 402 Payment Required.
payment_required : Status_Code
payment_required = Status_Code 402
## 403 Forbidden.
forbidden : Status_Code
forbidden = Status_Code 403
## 404 Not Found.
not_found : Status_Code
not_found = Status_Code 404
## 405 Method Not Allowed.
method_not_allowed : Status_Code
method_not_allowed = Status_Code 405
## 406 Not Acceptable.
not_acceptable : Status_Code
not_acceptable = Status_Code 406
## 407 Proxy Authentication Required.
proxy_authentication_required : Status_Code
proxy_authentication_required = Status_Code 407
## 408 Request Timeout.
request_timeout : Status_Code
request_timeout = Status_Code 408
## 409 Conflict.
conflict : Status_Code
conflict = Status_Code 409
## 410 Gone.
gone : Status_Code
gone = Status_Code 410
## 411 Length Required.
length_required : Status_Code
length_required = Status_Code 411
## 412 Precondition Failed.
precondition_failed : Status_Code
precondition_failed = Status_Code 412
## 413 Request Entity Too Large.
request_entity_too_large : Status_Code
request_entity_too_large = Status_Code 413
## 414 Request-URI Too Long.
request_uri_too_long : Status_Code
request_uri_too_long = Status_Code 414
## 415 Unsupported Media Type.
unsupported_media_type : Status_Code
unsupported_media_type = Status_Code 415
## 416 Requested Range Not Satisfiable.
requested_range_not_satisfiable : Status_Code
requested_range_not_satisfiable = Status_Code 416
## 417 Expectation Failed.
expectation_failed : Status_Code
expectation_failed = Status_Code 417
## 500 Internal Server Error.
internal_server_error : Status_Code
internal_server_error = Status_Code 500
## 501 Not Implemented.
not_implemented : Status_Code
not_implemented = Status_Code 501
## 502 Bad Gateway.
bad_gateway : Status_Code
bad_gateway = Status_Code 502
## 503 Service Unavailable.
service_unavailable : Status_Code
service_unavailable = Status_Code 503
## 504 Gateway Timeout
gateway_timeout : Status_Code
gateway_timeout = Status_Code 504
## 505 HTTP Version Not Supported.
http_version_not_supported : Status_Code
http_version_not_supported = Status_Code 505

View File

@ -0,0 +1,7 @@
type Version
## HTTP version 1.1.
type Http_1_1
## HTTP version 2.
type Http_2

View File

@ -0,0 +1,17 @@
from Base import all
## Proxy settings.
type Proxy
## Proxy is disabled.
type None
## Use a sysem proxy settings.
type System
## Use provided proxy.
type Proxy_Addr proxy_host proxy_port
## Create new proxy settins from host and port.
new : Text -> Integer -> Proxy
new host port=80 = Proxy_Addr host port

View File

@ -0,0 +1,148 @@
from Base import all
polyglot java import java.net.URI as Java_URI
polyglot java import java.util.Optional
## A type that can be converted to Uri.
type To_Uri
type To_Uri to_uri
## Implement To_Uri for Text
Text.to_uri = here.parse this
type Uri_Error
type Syntax_Error message
## PRIVATE
panic_on_error ~action =
action . catch <| case _ of
Syntax_Error msg -> Panic.throw (Syntax_Error msg)
type Uri
## Represents a Uniform Resource Identifier (URI) reference.
type Uri internal_uri
## Implement To_Uri
to_uri : Uri
to_uri = this
## Get scheme part of this Uri.
> Example
Return the "http" part of the HTTP address.
addr = "http://user:pass@example.com/foo/bar?key=val"
Uri.parse addr . scheme
scheme : Text
scheme = Optional.ofNullable [this.internal_uri.getScheme []] . orElse [""]
## Get user info part of this Uri.
> Example
Return the "user:pass" part of the HTTP address.
addr = "http://user:pass@example.com/foo/bar?key=val"
Uri.parse addr . user_info
user_info : Text
user_info = Optional.ofNullable [this.internal_uri.getUserInfo []] . orElse [""]
## Get host part of this Uri.
> Example
Return the "example.com" part of the HTTP address.
addr = "http://user:pass@example.com/foo/bar?key=val"
Uri.parse addr . host
host : Text
host = Optional.ofNullable [this.internal_uri.getHost []] . orElse [""]
## Get authority (user info and host) part of this Uri.
> Example
Return the "user:pass@example.com" part of the HTTP address.
addr = "http://user:pass@example.com/foo/bar?key=val"
Uri.parse addr . authority
authority : Text
authority = Optional.ofNullable [this.internal_uri.getAuthority []] . orElse [""]
## Get port part of this Uri.
> Example
Return the "80" part of the HTTP address.
addr = "http://user:pass@example.com:80/foo/bar?key=val"
Uri.parse addr . port
> Example
Return the empty string if the port is not specified.
addr = "http://user:pass@example.com:80/foo/bar?key=val"
Uri.parse addr . port
port : Text
port =
port_number = this.internal_uri.getPort []
if port_number == -1 then "" else port_number.to_text
## Get path part of this Uri.
> Example
Return the "/foo/bar" part of the HTTP address.
addr = "http://user:pass@example.com:80/foo/bar?key=val"
Uri.parse addr . path
path : Text
path = Optional.ofNullable [this.internal_uri.getPath []] . orElse [""]
## Get query part of this Uri.
> Example
Return the "key=val" part of the HTTP address.
addr = "http://user:pass@example.com:80/foo/bar?key=val"
Uri.parse addr . query
query : Text
query = Optional.ofNullable [this.internal_uri.getQuery []] . orElse [""]
## Get fragment part of this Uri.
> Example
Return the empty fragment of the HTTP address.
addr = "http://user:pass@example.com:80/foo/bar?key=val"
Uri.parse addr . fragment
fragment : Text
fragment = Optional.ofNullable [this.internal_uri.getFragment []] . orElse [""]
## Get unescaped user info part of this Uri.
raw_user_info : Text
raw_user_info = Optional.ofNullable [this.internal_uri.getRawUserInfo []] . orElse [""]
## Get unescaped authority part of this Uri.
raw_authority : Text
raw_authority = Optional.ofNullable [this.internal_uri.getRawAuthority []] . orElse [""]
## Get unescaped path part of this Uri.
raw_path : Text
raw_path = Optional.ofNullable [this.internal_uri.getRawPath []] . orElse [""]
## Get unescaped query part of this Uri.
raw_query : Text
raw_query = Optional.ofNullable [this.internal_uri.getRawQuery []] . orElse [""]
## Get unescaped fragment part of this Uri.
raw_fragment : Text
raw_fragment = Optional.ofNullable [this.internal_uri.getRawFragment []] . orElse [""]
## Convert this Uri to text.
to_text : Text
to_text = this.internal_uri.toString []
## Check Uri equality.
== : Uri -> Boolean
== that = this.internal_uri.equals [that.internal_uri]
## Parse Uri from text.
Return Syntax_Error when the text cannot be parsed as Uri.
> Example
Parse Uri text.
Uri.parse "http://example.com"
parse : Text -> Uri
parse text =
Panic.recover (Uri (Java_URI.create [text])) . catch <| case _ of
Polyglot_Error ex -> Error.throw (Syntax_Error (ex.getMessage []))
other -> Panic.throw other

View File

@ -69,6 +69,28 @@ Text.split_at separator =
Text.== : Text -> Boolean
Text.== that = Text_Utils.equals [this, that]
## Checks whether `this` is equal to `that`, ignoring case considerations.
Two texts are considered equal ignoring case if they are of the same length
and corresponding characters are equal ignoring case.
The definition of equality includes Unicode canonicalization. I.e. two texts
are equal if they are identical after canonical decomposition. This ensures
that different ways of expressing the same character in the underlying
binary representation are considered equal.
> Example
The string 'É' (i.e. the character U+00C9, LATIN CAPITAL LETTER E WITH
ACUTE) is equal ignore case to the string 'é' (i.e. the character U+00E9,
LATIN SMALL LETTER E WITH ACUTE), which is canonically the same as the
string 'e\u0301' (i.e. the letter `e` followed by U+0301, COMBINING ACUTE
ACCENT). Therefore:
(('É' . equals_ignore_case 'é') && ('é' == 'e\u0301')) == True
Text.equals_ignore_case : Text -> Boolean
Text.equals_ignore_case that = Text_Utils.equals_ignore_case [this, that]
## Checks if `this` is lexicographically before `that`.
Text.< : Text -> Boolean
Text.< that = Text_Utils.lt [this, that]

View File

@ -46,7 +46,7 @@ type Date
Add 6 months to a local date.
Date.new 2020 + 6.months
+ : Duration -> Date
+ amount = if amount.is_time then Error.throw (Time.Time_Error "Date does not support time intervals") else Date (this . internal_local_date . plus [amount.interval_period])
+ amount = if amount.is_time then Error.throw (Time.Time_Error "Date does not support time intervals") else Date (this . internal_local_date . plus [amount.internal_period])
## Subtract specified amount of time to this instant.
@ -54,7 +54,7 @@ type Date
Subtract 7 days from a local date.
Date.new 2020 - 7.days
- : Duration -> Date
- amount = if amount.is_time then Error.throw (Time.Time_Error "Date does not support time intervals") else Date (this . internal_local_date . minus [amount.interval_period])
- amount = if amount.is_time then Error.throw (Time.Time_Error "Date does not support time intervals") else Date (this . internal_local_date . minus [amount.internal_period])
## Format this date using the default formatter.
to_text : Text

View File

@ -8,7 +8,7 @@ type Duration
## An amount of time in terms of years, months, days, hours, minutes,
seconds and nanoseconds.
type Duration interval_period interval_duration
type Duration internal_period internal_duration
## Add specified amount of time to this duration.
@ -20,7 +20,7 @@ type Duration
Add 12 hours to a duration of a month.
1.month + 12.hours
+ : Duration -> Duration
+ other = Duration (this.interval_period . plus [other.interval_period]) (this.interval_duration . plus [other.interval_duration])
+ other = Duration (this.internal_period . plus [other.internal_period] . normalized []) (this.internal_duration . plus [other.internal_duration])
## Subtract specified amount of time from this duration.
> Example
@ -31,39 +31,39 @@ type Duration
Substract 30 minutes from a duration of 7 months.
7.months - 30.minutes
- : Duration -> Duration
- other = Duration (this.interval_period . minus [other.interval_period]) (this.interval_duration . minus [other.interval_duration])
- other = Duration (this.internal_period . minus [other.internal_period] . normalized []) (this.internal_duration . minus [other.internal_duration])
## Get the amount of nanoseconds of this duration.
nanoseconds : Integer
nanoseconds = this.interval_duration . toNanosPart []
nanoseconds = this.internal_duration . toNanosPart []
## Get the amount of milliseconds of this duration.
milliseconds : Integer
milliseconds = this.interval_duration . toMillisPart []
milliseconds = this.internal_duration . toMillisPart []
## Get the amount of minutes of this duration.
seconds : Integer
seconds = this.interval_duration . toSecondsPart []
seconds = this.internal_duration . toSecondsPart []
## Get the amount of minutes of this duration.
minutes : Integer
minutes = this.interval_duration . toMinutesPart []
minutes = this.internal_duration . toMinutesPart []
## Get the amount of hours of this duration.
hours : Integer
hours = this.interval_duration . toHours []
hours = this.internal_duration . toHours []
## Get the amount of days of this duration.
days : Integer
days = this.interval_period . getDays []
days = this.internal_period . getDays []
## Get the amount of months of this duration.
months : Integer
months = this.interval_period . getMonths []
months = this.internal_period . getMonths []
## Get the amount of days of this duration.
years : Integer
years = this.interval_period . getYears []
years = this.internal_period . getYears []
## Convert this duration to a Vector of years, months, days, hours, minutes,
seconds and nanosecnods.
@ -90,6 +90,10 @@ type Duration
is_empty : Boolean
is_empty = (not this.is_date) && (not this.is_time)
## Check the durations equality.
== : Duration -> Boolean
== that = this.to_vector == that.to_vector
## Duration in nanoseconds.
Integer.nanosecond : Duration
Integer.nanosecond = Duration (Java_Period.ofDays [0]) (Java_Duration.ofNanos [this])
@ -132,7 +136,7 @@ Integer.hours = this.hour
## Duration in days.
Integer.day : Duration
Integer.day = Duration (Java_Period.ofDays [this]) (Java_Duration.ofSeconds [0])
Integer.day = Duration (Java_Period.ofDays [this] . normalized []) (Java_Duration.ofSeconds [0])
## Duration in days.
Integer.days : Duration
@ -140,7 +144,7 @@ Integer.days = this.day
## Duration in months.
Integer.month : Duration
Integer.month = Duration (Java_Period.ofMonths [this]) (Java_Duration.ofSeconds [0])
Integer.month = Duration (Java_Period.ofMonths [this] . normalized []) (Java_Duration.ofSeconds [0])
## Duration in months.
Integer.months : Duration
@ -148,7 +152,7 @@ Integer.months = this.month
## Duration in years.
Integer.year : Duration
Integer.year = Duration (Java_Period.ofYears [this]) (Java_Duration.ofSeconds [0])
Integer.year = Duration (Java_Period.ofYears [this] . normalized []) (Java_Duration.ofSeconds [0])
## Duration in years.
Integer.years : Duration
@ -161,4 +165,4 @@ Integer.years = this.year
Duration.between Time.now (Time.new 2010 10 20)
between : Time -> Time -> Duration
between start_inclusive end_exclusive =
Duration (Java_Period.ofDays [0]) (Java_Duration.between [start_inclusive.internal_zoned_date_time, end_exclusive.internal_zoned_date_time])
Duration (Java_Period.ofDays [0] . normalized []) (Java_Duration.between [start_inclusive.internal_zoned_date_time, end_exclusive.internal_zoned_date_time])

View File

@ -93,7 +93,7 @@ type Time
Add 15 years and 3 hours to a zoned date time.
Time.new 2020 + 15.years + 3.hours
+ : Duration -> Time
+ amount = Time (this . internal_zoned_date_time . plus [amount.interval_period] . plus [amount.interval_duration])
+ amount = Time (this . internal_zoned_date_time . plus [amount.internal_period] . plus [amount.internal_duration])
## Subtract specified amount of time to this instant.
@ -105,7 +105,7 @@ type Time
Subtract 1 year and 9 months from a zoned date time.
Time.new 2020 - 1.year - 9.months
- : Duration -> Time
- amount = Time (this . internal_zoned_date_time . minus [amount.interval_period] . minus [amount.interval_duration])
- amount = Time (this . internal_zoned_date_time . minus [amount.internal_period] . minus [amount.internal_duration])
## Format this time using the default formatter.
to_text : Text

View File

@ -50,7 +50,7 @@ type Time_Of_Day
Add 3 seconds to a local time.
Time_Of_Day.new + 3.seconds
+ : Duration -> Time_Of_Day
+ amount = if amount.is_date then Error.throw (Time.Time_Error "Time_Of_Day does not support date intervals") else Time_Of_Day (this . internal_local_time . plus [amount.interval_duration])
+ amount = if amount.is_date then Error.throw (Time.Time_Error "Time_Of_Day does not support date intervals") else Time_Of_Day (this . internal_local_time . plus [amount.internal_duration])
## Subtract specified amount of time to this instant.
@ -58,7 +58,7 @@ type Time_Of_Day
Subtract 12 hours from a local time.
Time_Of_Day.new - 12.hours
- : Duration -> Time_Of_Day
- amount = if amount.is_date then Error.throw (Time.Time_Error "Time_Of_Day does not support date intervals") else Time_Of_Day (this . internal_local_time . minus [amount.interval_duration])
- amount = if amount.is_date then Error.throw (Time.Time_Error "Time_Of_Day does not support date intervals") else Time_Of_Day (this . internal_local_time . minus [amount.internal_duration])
## Format this time of day using the default formatter.
to_text : Text

View File

@ -77,6 +77,22 @@ type Vector
f = acc -> ix -> function acc (arr.at ix)
0.upto this.length . fold initial f
## Checks whether a predicate holds for at least one element of this vector.
exists : (Any -> Any) -> Boolean
exists p =
check found ix = if found then found else p ix
this.fold False check
## Checks whether this vector contains a given value as an element.
contains : Any -> Boolean
contains elem = this.exists ix-> ix == elem
## Selects all elements of this vector which satisfy a predicate.
filter : (Any -> Boolean) -> Vector
filter p =
check acc ix = if p ix then acc + [ix] else acc
this.fold [] check
## Creates a new vector of the given length, initializing elements using
the provided constructor function.

View File

@ -19,12 +19,14 @@ import org.enso.interpreter.runtime.type.TypesGen;
@NodeInfo(shortName = "<new>", description = "Instantiates a polyglot constructor.")
public class ConstructorDispatchNode extends BuiltinRootNode {
private ConstructorDispatchNode(Language language) {
super(language);
}
private @Child InteropLibrary library =
InteropLibrary.getFactory().createDispatched(Constants.CacheSizes.BUILTIN_INTEROP_DISPATCH);
private @Child HostValueToEnsoNode hostValueToEnsoNode = HostValueToEnsoNode.build();
private final BranchProfile err = BranchProfile.create();
/**
@ -53,7 +55,7 @@ public class ConstructorDispatchNode extends BuiltinRootNode {
Object state = Function.ArgumentsHelper.getState(frame.getArguments());
try {
Object[] arguments = TypesGen.expectArray(args[1]).getItems();
Object res = library.instantiate(cons, arguments);
Object res = hostValueToEnsoNode.execute(library.instantiate(cons, arguments));
return new Stateful(state, res);
} catch (UnsupportedMessageException
| ArityException

View File

@ -0,0 +1,55 @@
package org.enso.base;
import org.enso.base.net.http.BasicAuthorization;
import org.enso.base.net.http.MultipartBodyBuilder;
import org.enso.base.net.http.UrlencodedBodyBuilder;
import java.net.http.HttpHeaders;
import java.util.AbstractMap;
import java.util.List;
import java.util.Map;
/** Utils for standard HTTP library. */
public class Http_Utils {
/**
* Create the header for HTTP basic auth.
*
* @param user the user name.
* @param password the password.
* @return the new header.
*/
public static String header_basic_auth(String user, String password) {
return BasicAuthorization.header(user, password);
}
/**
* Create the builder for a multipart form data.
*
* @return the multipart form builder.
*/
public static MultipartBodyBuilder multipart_body_builder() {
return new MultipartBodyBuilder();
}
/**
* Create the builder for an url-encoded form data.
*
* @return the url-encoded form builder.
*/
public static UrlencodedBodyBuilder urlencoded_body_builder() {
return new UrlencodedBodyBuilder();
}
/**
* Get HTTP response headers as a list of map entries.
*
* @param headers HTTP response headers.
* @return the key-value list of headers.
*/
public static Object[] get_headers(HttpHeaders headers) {
Map<String, List<String>> map = headers.map();
return map.keySet().stream()
.flatMap(k -> map.get(k).stream().map(v -> new AbstractMap.SimpleImmutableEntry<>(k, v)))
.toArray();
}
}

View File

@ -65,6 +65,20 @@ public class Text_Utils {
.equals(Normalizer2.getNFDInstance().normalize(str2));
}
/**
* Checks whether two strings are equal up to Unicode canonicalization ignoring case
* considerations.
*
* @param str1 the first string
* @param str2 the second string
* @return the result of comparison
*/
public static boolean equals_ignore_case(String str1, String str2) {
return Normalizer2.getNFDInstance()
.normalize(str1)
.equalsIgnoreCase(Normalizer2.getNFDInstance().normalize(str2));
}
/**
* Converts an array of codepoints into a string.
*

View File

@ -0,0 +1,21 @@
package org.enso.base.net.http;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/** An authenticator for HTTP basic auth. */
public final class BasicAuthorization {
/**
* Build HTTP basic authorization header.
*
* @param user the user name.
* @param password the password
* @return return base64 encoded header for HTTP basic auth.
*/
public static String header(String user, String password) {
String auth = user + ":" + password;
String authEncoded = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
return "Basic " + authEncoded;
}
}

View File

@ -0,0 +1,218 @@
package org.enso.base.net.http;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.http.HttpRequest;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.function.Supplier;
/** A builder for a multipart form data. */
public class MultipartBodyBuilder {
private final List<PartsSpecification> partsSpecificationList = new ArrayList<>();
private final String boundary = UUID.randomUUID().toString();
/**
* Create HTTP body publisher for a multipart form data.
*
* @return the body publisher.
*/
public HttpRequest.BodyPublisher build() {
if (partsSpecificationList.size() == 0) {
throw new IllegalStateException("Must have at least one part to build multipart message.");
}
addFinalBoundaryPart();
return HttpRequest.BodyPublishers.ofByteArrays(PartsIterator::new);
}
/**
* Get the multipart boundary separator.
*
* @return the multipart boundary string.
*/
public String get_boundary() {
return boundary;
}
/**
* Add text field to the multipart form.
*
* @param name the field name.
* @param value the field value.
* @return this builder.
*/
public MultipartBodyBuilder add_part_text(String name, String value) {
PartsSpecification newPart = new PartsSpecification();
newPart.type = PartsSpecification.TYPE.STRING;
newPart.name = name;
newPart.value = value;
partsSpecificationList.add(newPart);
return this;
}
/**
* Add file field to the multipart form.
*
* @param name the field name.
* @param path the file path.
* @return this builder.
*/
public MultipartBodyBuilder add_part_file(String name, String path) {
PartsSpecification newPart = new PartsSpecification();
newPart.type = PartsSpecification.TYPE.FILE;
newPart.name = name;
newPart.path = Paths.get(path);
partsSpecificationList.add(newPart);
return this;
}
/**
* Add the data field to the multipart form.
*
* @param name the field name.
* @param value the field value.
* @param filename the file name.
* @param contentType the content type.
* @return this builder.
*/
public MultipartBodyBuilder add_part_bytes(
String name, byte[] value, String filename, String contentType) {
PartsSpecification newPart = new PartsSpecification();
newPart.type = PartsSpecification.TYPE.STREAM;
newPart.name = name;
newPart.stream = () -> new ByteArrayInputStream(value);
newPart.filename = filename;
newPart.contentType = contentType;
partsSpecificationList.add(newPart);
return this;
}
private void addFinalBoundaryPart() {
PartsSpecification newPart = new PartsSpecification();
newPart.type = PartsSpecification.TYPE.FINAL_BOUNDARY;
newPart.value = "--" + boundary + "--";
partsSpecificationList.add(newPart);
}
private static final class PartsSpecification {
public enum TYPE {
STRING,
FILE,
STREAM,
FINAL_BOUNDARY
}
TYPE type;
String name;
String value;
Path path;
Supplier<InputStream> stream;
String filename;
String contentType;
}
private final class PartsIterator implements Iterator<byte[]> {
private final Iterator<PartsSpecification> iter;
private InputStream currentFileInput;
private boolean done;
private byte[] next;
PartsIterator() {
iter = partsSpecificationList.iterator();
}
@Override
public boolean hasNext() {
if (done) return false;
if (next != null) return true;
try {
next = computeNext();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
if (next == null) {
done = true;
return false;
}
return true;
}
@Override
public byte[] next() {
if (!hasNext()) throw new NoSuchElementException();
byte[] res = next;
next = null;
return res;
}
private byte[] computeNext() throws IOException {
if (currentFileInput == null) {
if (!iter.hasNext()) return null;
PartsSpecification nextPart = iter.next();
if (PartsSpecification.TYPE.STRING.equals(nextPart.type)) {
String part =
"--"
+ boundary
+ "\r\n"
+ "Content-Disposition: form-data; name="
+ nextPart.name
+ "\r\n"
+ "Content-Type: text/plain; charset=UTF-8\r\n\r\n"
+ nextPart.value
+ "\r\n";
return part.getBytes(StandardCharsets.UTF_8);
}
if (PartsSpecification.TYPE.FINAL_BOUNDARY.equals(nextPart.type)) {
return nextPart.value.getBytes(StandardCharsets.UTF_8);
}
String filename;
String contentType;
if (PartsSpecification.TYPE.FILE.equals(nextPart.type)) {
Path path = nextPart.path;
filename = path.getFileName().toString();
contentType = Files.probeContentType(path);
if (contentType == null) contentType = "application/octet-stream";
currentFileInput = Files.newInputStream(path);
} else {
filename = nextPart.filename;
contentType = nextPart.contentType;
if (contentType == null) contentType = "application/octet-stream";
currentFileInput = nextPart.stream.get();
}
String partHeader =
"--"
+ boundary
+ "\r\n"
+ "Content-Disposition: form-data; name="
+ nextPart.name
+ "; filename="
+ filename
+ "\r\n"
+ "Content-Type: "
+ contentType
+ "\r\n\r\n";
return partHeader.getBytes(StandardCharsets.UTF_8);
} else {
byte[] buf = new byte[8192];
int length = currentFileInput.read(buf);
if (length > 0) {
byte[] actualBytes = new byte[length];
System.arraycopy(buf, 0, actualBytes, 0, length);
return actualBytes;
} else {
currentFileInput.close();
currentFileInput = null;
return "\r\n".getBytes(StandardCharsets.UTF_8);
}
}
}
}
}

View File

@ -0,0 +1,58 @@
package org.enso.base.net.http;
import java.io.IOException;
import java.net.URLEncoder;
import java.net.http.HttpRequest;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
/** A builder for an url-encoded form data. */
public final class UrlencodedBodyBuilder {
private final ArrayList<String> parts = new ArrayList<>();
/**
* Create HTTP body publisher for an url-encoded form data.
*
* @return the body publisher.
*/
public HttpRequest.BodyPublisher build() {
String contents = String.join("&", parts);
return HttpRequest.BodyPublishers.ofString(contents);
}
/**
* Add text field to the form.
*
* @param name the field name.
* @param value the field value.
* @return this builder.
*/
public UrlencodedBodyBuilder add_part_text(String name, String value) {
parts.add(encodePart(name, value));
return this;
}
/**
* Add file field to the form.
*
* @param name the field name.
* @param path the file path.
* @return this builder.
*/
public UrlencodedBodyBuilder add_part_file(String name, String path) throws IOException {
String contents = Files.readString(Paths.get(path), StandardCharsets.UTF_8);
parts.add(encodePart(name, contents));
return this;
}
private String encodePart(String name, String value) {
return
URLEncoder.encode(name, StandardCharsets.UTF_8)
+ "="
+ URLEncoder.encode(value, StandardCharsets.UTF_8);
}
}

View File

@ -13,6 +13,10 @@ import Test.Data.Json_Spec
import Test.Number_Spec
import Test.Process_Spec
import Test.Vector.Spec as Vector_Spec
import Test.Net.Http_Spec
import Test.Net.Uri_Spec
import Test.Net.Http.Header_Spec
import Test.Net.Http.Request_Spec
import Test.Numbers.Spec as Numbers_Spec
import Test.Text.Spec as Text_Spec
import Test.Time.Spec as Time_Spec
@ -35,3 +39,7 @@ main = Test.Suite.runMain <|
Meta_Spec.spec
Map_Spec.spec
Json_Spec.spec
Uri_Spec.spec
Header_Spec.spec
Request_Spec.spec
Http_Spec.spec

View File

@ -0,0 +1,12 @@
from Base import all
import Base.Test
import Base.Net.Http.Header
spec =
describe "Header" <|
it "should check equality" <|
Header.new "A" "B" . should_equal (Header.new "A" "B")
Header.new "A" "B" . should_equal (Header.new "a" "B")
(Header.new "A" "B" == Header.new "A" "b") . should_equal False
(Header.new "A" "B" == Header.new "a" "b") . should_equal False

View File

@ -0,0 +1,48 @@
from Base import all
import Base.Test
import Base.Net.Http.Form
import Base.Net.Http.Header
import Base.Net.Http.Method
import Base.Net.Http.Request
import Base.Net.Http.Request.Body as Request_Body
import Base.Net.Uri
spec =
test_uri = Uri.parse "https://httpbin.org/post"
test_headers = [Header.application_json, Header.new "X-Foo-Id" "0123456789"]
describe "Request" <|
it "should get method" <|
req = Request.new Method.Post test_uri
req.method.should_equal Method.Post
it "should get uri" <|
req = Request.get test_uri
req.uri.should_equal test_uri
it "should get headers" <|
req = Request.get test_uri test_headers
req.headers.should_equal test_headers
it "should add header" <|
new_header = Header.accept_all
req = Request.get test_uri test_headers . with_header new_header.name new_header.value
req.headers.should_equal (test_headers + [new_header])
it "should update header" <|
req = Request.get test_uri test_headers . with_header "X-Foo-Id" "42"
req.headers.should_equal [Header.application_json, Header.new "X-Foo-Id" "42"]
it "should add headers" <|
req = Request.get test_uri . with_headers test_headers
req.headers.should_equal test_headers
it "should update headers" <|
new_headers = [Header.multipart_form_data, Header.accept_all]
req = Request.get test_uri test_headers . with_headers new_headers
req.headers.should_equal [Header.multipart_form_data, test_headers.at 1, Header.accept_all]
it "should set json body" <|
json = '''
{"key":"val"}
req = Request.get test_uri . with_json json
req.body.should_equal (Request_Body.Json json)
req.headers.should_equal [Header.application_json]
it "should set form body" <|
body_form = [Form.text_field "key" "val"]
req = Request.get test_uri . with_form body_form
req.body.should_equal (Request_Body.Form body_form.to_form)
req.headers.should_equal [Header.application_x_www_form_urlencoded]

View File

@ -0,0 +1,225 @@
from Base import all
import Base.Data.Json
import Base.Test
import Base.Net.Http
import Base.Net.Http.Form
import Base.Net.Http.Header
import Base.Net.Http.Request
import Base.Net.Http.Request.Body as Request_Body
import Base.Net.Http.Status_Code
import Base.Net.Http.Version
import Base.Net.Proxy
import Base.Net.Uri
import Base.System.File
import Base.Time.Duration
polyglot java import java.lang.System
polyglot java import java.util.Objects
spec =
is_ci = Objects.equals ["true", System.getenv ["CI"]]
if is_ci then here.spec_impl else Unit
spec_impl =
describe "Http" <|
it "should create HTTP client with timeout setting" <|
http = Http.new (timeout = 30.seconds)
http.timeout.should_equal 30.seconds
it "should create HTTP client with follow_redirects setting" <|
http = Http.new (follow_redirects = False)
http.follow_redirects.should_equal False
it "should create HTTP client with proxy setting" <|
proxy_setting = Proxy.Proxy_Addr "example.com" 80
http = Http.new (proxy = proxy_setting)
http.proxy.should_equal proxy_setting
it "should create HTTP client with version setting" <|
version_setting = Version.Http_2
http = Http.new (version = version_setting)
http.version.should_equal version_setting
it "should throw error when requesting invalid Uri" <|
case Panic.recover (Http.new.get "not a uri") of
Uri.Syntax_Error _ -> Unit
other -> Test.fail ("Unexpected result: " + other)
it "should send Get request" <|
expected_response = Json.parse <| '''
{
"headers": {
"Content-Length": "0",
"User-Agent": "Java-http-client/11.0.8"
},
"origin": "127.0.0.1",
"url": "",
"args": {}
}
res = Http.new.get "http://localhost:8080/get"
res.code.should_equal Status_Code.ok
res.body.to_json.should_equal expected_response
it "should send Get request using module method" <|
expected_response = Json.parse <| '''
{
"headers": {
"Content-Length": "0",
"User-Agent": "Java-http-client/11.0.8"
},
"origin": "127.0.0.1",
"url": "",
"args": {}
}
res = Http.get "http://localhost:8080/get"
res.code.should_equal Status_Code.ok
res.body.to_json.should_equal expected_response
it "should Post empty body" <|
expected_response = Json.parse <| '''
{
"headers": {
"Content-Length": "0",
"User-Agent": "Java-http-client/11.0.8"
},
"origin": "127.0.0.1",
"url": "",
"args": {},
"data": "",
"files": null,
"form": null,
"json": null
}
body_empty = Request_Body.Empty
res = Http.new.post "http://localhost:8080/post" body_empty
res.code.should_equal Status_Code.ok
res.body.to_json.should_equal expected_response
it "should Post empty body using module method" <|
expected_response = Json.parse <| '''
{
"headers": {
"Content-Length": "0",
"User-Agent": "Java-http-client/11.0.8"
},
"origin": "127.0.0.1",
"url": "",
"args": {},
"data": "",
"files": null,
"form": null,
"json": null
}
body_empty = Request_Body.Empty
res = Http.post "http://localhost:8080/post" body_empty
res.code.should_equal Status_Code.ok
res.body.to_json.should_equal expected_response
it "should Post text body" <|
expected_response = Json.parse <| '''
{
"headers": {
"Content-Length": "12",
"Content-Type": "text/plain",
"User-Agent": "Java-http-client/11.0.8"
},
"origin": "127.0.0.1",
"url": "",
"args": {},
"data": "Hello World!",
"files": null,
"form": null,
"json": null
}
body_text = Request_Body.Text "Hello World!"
res = Http.new.post "http://localhost:8080/post" body_text
res.code.should_equal Status_Code.ok
res.body.to_json.should_equal expected_response
it "should Post form text" <|
expected_response = Json.parse <| '''
{
"headers": {
"Content-Length": "7",
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "Java-http-client/11.0.8"
},
"origin": "127.0.0.1",
"url": "",
"args": {},
"data": "key=val",
"files": null,
"form": null,
"json": null
}
form_parts = [Form.text_field "key" "val"]
res = Http.new.post_form "http://localhost:8080/post" form_parts
res.code.should_equal Status_Code.ok
res.body.to_json.should_equal expected_response
it "should Post form text using module method" <|
expected_response = Json.parse <| '''
{
"headers": {
"Content-Length": "7",
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "Java-http-client/11.0.8"
},
"origin": "127.0.0.1",
"url": "",
"args": {},
"data": "key=val",
"files": null,
"form": null,
"json": null
}
form_parts = [Form.text_field "key" "val"]
res = Http.post_form "http://localhost:8080/post" form_parts
res.code.should_equal Status_Code.ok
res.body.to_json.should_equal expected_response
it "should Post form file" <|
test_file = Enso_Project.data / "sample.txt"
form_parts = [Form.text_field "key" "val", Form.file_field "sample" test_file]
res = Http.new.post_form "http://localhost:8080/post" form_parts
res.code.should_equal Status_Code.ok
it "should Post form multipart" <|
test_file = Enso_Project.data / "sample.txt"
form_parts = [Form.text_field "key" "val", Form.file_field "sample" test_file]
res = Http.new.post_form "http://localhost:8080/post" form_parts [Header.multipart_form_data]
res.code.should_equal Status_Code.ok
it "should Post Json" <|
expected_response = Json.parse <| '''
{
"headers": {
"Content-Length": "13",
"Content-Type": "application/json",
"User-Agent": "Java-http-client/11.0.8"
},
"origin": "127.0.0.1",
"url": "",
"args": {},
"data": "{\\"key\\":\\"val\\"}",
"files": null,
"form": null,
"json": {
"key": "val"
}
}
json = Json.parse <| '''
{"key":"val"}
res = Http.new.post_json "http://localhost:8080/post" json
res.code.should_equal Status_Code.ok
res.body.to_json.should_equal expected_response
it "should Post Json using module method" <|
expected_response = Json.parse <| '''
{
"headers": {
"Content-Length": "13",
"Content-Type": "application/json",
"User-Agent": "Java-http-client/11.0.8"
},
"origin": "127.0.0.1",
"url": "",
"args": {},
"data": "{\\"key\\":\\"val\\"}",
"files": null,
"form": null,
"json": {
"key": "val"
}
}
json = Json.parse <| '''
{"key":"val"}
res = Http.post_json "http://localhost:8080/post" json
res.code.should_equal Status_Code.ok
res.body.to_json.should_equal expected_response

View File

@ -0,0 +1,35 @@
from Base import all
import Base.Test
import Base.Net.Uri
spec =
describe "Uri" <|
it "should parse Uri from string" <|
addr = Uri.parse "http://user:pass@example.com/foo/bar?key=val"
addr.scheme.should_equal "http"
addr.user_info.should_equal "user:pass"
addr.host.should_equal "example.com"
addr.authority.should_equal "user:pass@example.com"
addr.port.should_equal ""
addr.path.should_equal "/foo/bar"
addr.query.should_equal "key=val"
addr.fragment.should_equal ""
it "should escape Uri" <|
addr = Uri.parse "https://%D0%9B%D0%B8%D0%BD%D1%83%D1%81:pass@ru.wikipedia.org/wiki/%D0%AF%D0%B4%D1%80%D0%BE_Linux?%D0%9A%D0%BE%D0%B4"
addr.user_info.should_equal "Линус:pass"
addr.authority.should_equal "Линус:pass@ru.wikipedia.org"
addr.path.should_equal "/wiki/Ядро_Linux"
addr.query.should_equal "Код"
addr.fragment.should_equal ""
addr.raw_user_info.should_equal "%D0%9B%D0%B8%D0%BD%D1%83%D1%81:pass"
addr.raw_authority.should_equal "%D0%9B%D0%B8%D0%BD%D1%83%D1%81:pass@ru.wikipedia.org"
addr.raw_path.should_equal "/wiki/%D0%AF%D0%B4%D1%80%D0%BE_Linux"
addr.raw_query.should_equal "%D0%9A%D0%BE%D0%B4"
addr.raw_fragment.should_equal ""
it "should return Syntax_Error when parsing invalid Uri" <|
Uri.parse "a b c" . catch <| case _ of
Uri.Syntax_Error msg ->
msg.should_equal "Illegal character in path at index 1: a b c"
other ->
Test.fail ("Unexpected result: " + other.to_text)

View File

@ -47,3 +47,23 @@ spec =
it "should check if empty" <|
interval = 0.seconds
interval.is_empty . should_be_true
it "should normalize periods" <|
interval = 12.months
interval.to_vector . should_equal [1, 0, 0, 0, 0, 0, 0]
it "should normalize addition" <|
interval = 11.months + 1.month
interval.to_vector . should_equal [1, 0, 0, 0, 0, 0, 0]
it "should normalize subtraction" <|
interval = 13.months - 1.month
interval.to_vector . should_equal [1, 0, 0, 0, 0, 0, 0]
it "should check equality" <|
3.seconds.should_equal 3.seconds
60.seconds.should_equal 1.minute
61.seconds.should_equal (1.minute + 1.second)
60.minutes.should_equal 1.hour
(24.hours == 1.day) . should_be_false
(30.days == 1.month) . should_be_false
12.months.should_equal 1.year
18.months.should_equal (1.year + 6.months)
1.year.should_equal (11.months + 1.month)
10.years.should_equal 10.years

View File

@ -34,4 +34,16 @@ spec = describe "Vectors" <|
vec.drop_right 2 . should_equal first_four
vec.take 4 . should_equal first_four
vec.take_right 4 . should_equal last_four
it "should check exists" <|
vec = [1, 2, 3, 4, 5]
vec.exists (ix -> ix > 3) . should_be_true
vec.exists (ix -> ix < 0) . should_be_false
it "should check contains" <|
vec = [1, 2, 3, 4, 5]
vec.contains 1 . should_be_true
vec.contains 0 . should_be_false
it "should filter elements" <|
vec = [1, 2, 3, 4, 5]
vec.filter (ix -> ix > 3) . should_equal [4, 5]
vec.filter (ix -> ix == 1) . should_equal [1]
vec.filter (ix -> ix < 0) . should_equal []