diff --git a/.gitignore b/.gitignore index c7c30d3..936332c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ *.so _docs/ -vls.log diff --git a/.woodpecker/.lint.yml b/.woodpecker/.lint.yml new file mode 100644 index 0000000..31df08c --- /dev/null +++ b/.woodpecker/.lint.yml @@ -0,0 +1,13 @@ +branches: + exclude: [ main ] + +platform: 'linux/amd64' + +pipeline: + lint: + image: 'chewingbever/vlang:latest' + pull: true + commands: + - make lint + when: + event: [ push ] diff --git a/.woodpecker/lint.yml b/.woodpecker/lint.yml deleted file mode 100644 index e502737..0000000 --- a/.woodpecker/lint.yml +++ /dev/null @@ -1,19 +0,0 @@ -branches: - exclude: [ main ] - -platform: 'linux/amd64' - -pipeline: - build: - image: 'git.rustybever.be/chewing_bever/vlang:0.3.2' - commands: - - make - when: - event: [ push ] - - lint: - image: 'git.rustybever.be/chewing_bever/vlang:0.3.2' - commands: - - make lint - when: - event: [ push ] diff --git a/containers.v b/containers.v index 809ef38..2d6d1a8 100644 --- a/containers.v +++ b/containers.v @@ -3,22 +3,79 @@ module docker import json import time import net.http { Method } -import types { ContainerListItem } -[params] -pub struct ContainerListConfig { - all bool - limit int - size bool - filters map[string][]string +pub struct Port { + ip string [json: IP] + private_port u16 [json: PrivatePort] + public_port u16 [json: PublicPort] + type_ string [json: Type] } -pub fn (mut d DockerConn) container_list(c ContainerListConfig) ![]ContainerListItem { - d.get('/containers/json') - d.params(c) - d.send()! +pub struct HostConfig { + network_mode string [json: NetworkMode] +} - return d.read_json_response<[]ContainerListItem>() +pub struct EndpointIpamConfig { + ipv4_address string [json: IPv4Address] + ipv6_address string [json: IPv6Address] + link_local_ips []string [json: LinkLocalIPs] +} + +pub struct EndpointSettings { + ipam_config EndpointIpamConfig [json: IPAMConfig] + links []string [json: Links] + aliases []string [json: Aliases] + network_id string [json: NetworkID] + endpoint_id string [json: EndpointID] + gateway string [json: Gateway] + ip_address string [json: IPAddress] + ip_prefix_len int [json: IPPrefixLen] + ipv6_gateway string [json: IPv6Gateway] + global_ipv6_address string [json: GlobalIPv6Address] + global_ipv6_prefix_len i64 [json: GlobalIPv6PrefixLen] + mac_address string [json: MacAddress] + driver_opts map[string]string [json: DriverOpts] +} + +pub struct NetworkSettings { + networks map[string]EndpointSettings [json: Networks] +} + +pub struct MountPoint { + type_ string [json: Type] + name string [json: Name] + source string [json: Source] + destination string [json: Destination] + driver string [json: Driver] + mode string [json: Mode] + rw bool [json: RW] + propagation string [json: Propagation] +} + +pub struct ContainerListItem { + id string [json: Id] + names []string [json: Names] + image string [json: Image] + image_id string [json: ImageID] + command string [json: Command] + created i64 [json: Created] + ports []Port [json: Ports] + size_rw i64 [json: SizeRw] + size_root_fs i64 [json: SizeRootFs] + labels map[string]string [json: Labels] + state string [json: State] + status string [json: Status] + host_config HostConfig [json: HostConfig] + network_settings NetworkSettings [json: NetworkSettings] + mounts []MountPoint [json: Mounts] +} + +pub fn (mut d DockerConn) container_list() ?[]ContainerListItem { + d.send_request(Method.get, '/containers/json')? + + data := d.read_json_response<[]ContainerListItem>()? + + return data } pub struct NewContainer { @@ -36,28 +93,28 @@ pub: warnings []string [json: Warnings] } -pub fn (mut d DockerConn) container_create(c NewContainer) !CreatedContainer { - d.send_request_with_json(Method.post, '/containers/create', c)! - head, res := d.read_response()! +pub fn (mut d DockerConn) container_create(c NewContainer) ?CreatedContainer { + d.send_request_with_json(Method.post, '/containers/create', c)? + head, res := d.read_response()? if head.status_code != 201 { - data := json.decode(DockerError, res)! + data := json.decode(DockerError, res)? return error(data.message) } - data := json.decode(CreatedContainer, res)! + data := json.decode(CreatedContainer, res)? return data } // start_container starts the container with the given id. -pub fn (mut d DockerConn) container_start(id string) ! { - d.send_request(Method.post, '/containers/$id/start')! - head, body := d.read_response()! +pub fn (mut d DockerConn) container_start(id string) ? { + d.send_request(Method.post, '/containers/$id/start')? + head, body := d.read_response()? if head.status_code != 204 { - data := json.decode(DockerError, body)! + data := json.decode(DockerError, body)? return error(data.message) } @@ -81,47 +138,47 @@ pub mut: end_time time.Time [skip] } -pub fn (mut d DockerConn) container_inspect(id string) !ContainerInspect { - d.send_request(Method.get, '/containers/$id/json')! - head, body := d.read_response()! +pub fn (mut d DockerConn) container_inspect(id string) ?ContainerInspect { + d.send_request(Method.get, '/containers/$id/json')? + head, body := d.read_response()? if head.status_code != 200 { - data := json.decode(DockerError, body)! + data := json.decode(DockerError, body)? return error(data.message) } - mut data := json.decode(ContainerInspect, body)! + mut data := json.decode(ContainerInspect, body)? // The Docker engine API *should* always return UTC time. - data.state.start_time = time.parse_rfc3339(data.state.start_time_str)! + data.state.start_time = time.parse_rfc3339(data.state.start_time_str)? if data.state.status == 'exited' { - data.state.end_time = time.parse_rfc3339(data.state.end_time_str)! + data.state.end_time = time.parse_rfc3339(data.state.end_time_str)? } return data } -pub fn (mut d DockerConn) container_remove(id string) ! { - d.send_request(Method.delete, '/containers/$id')! - head, body := d.read_response()! +pub fn (mut d DockerConn) container_remove(id string) ? { + d.send_request(Method.delete, '/containers/$id')? + head, body := d.read_response()? if head.status_code != 204 { - data := json.decode(DockerError, body)! + data := json.decode(DockerError, body)? return error(data.message) } } -pub fn (mut d DockerConn) container_get_logs(id string) !&StreamFormatReader { - d.send_request(Method.get, '/containers/$id/logs?stdout=true&stderr=true')! - head := d.read_response_head()! +pub fn (mut d DockerConn) container_get_logs(id string) ?&StreamFormatReader { + d.send_request(Method.get, '/containers/$id/logs?stdout=true&stderr=true')? + head := d.read_response_head()? if head.status_code != 200 { - content_length := head.header.get(http.CommonHeader.content_length)!.int() - body := d.read_response_body(content_length)! - data := json.decode(DockerError, body)! + content_length := head.header.get(http.CommonHeader.content_length)?.int() + body := d.read_response_body(content_length)? + data := json.decode(DockerError, body)? return error(data.message) } diff --git a/docker.v b/docker.v index 5479b36..2963f0d 100644 --- a/docker.v +++ b/docker.v @@ -17,22 +17,15 @@ const ( api_version = 'v1.41' ) -[heap] pub struct DockerConn { mut: socket &unix.StreamConn reader &io.BufferedReader - // Data for the request that's currently being constructed. - method http.Method - url string - params map[string]string - content_type string - body string } // new_conn creates a new connection to the Docker daemon. -pub fn new_conn() !&DockerConn { - s := unix.connect_stream(docker.socket)! +pub fn new_conn() ?&DockerConn { + s := unix.connect_stream(docker.socket)? d := &DockerConn{ socket: s @@ -43,27 +36,27 @@ pub fn new_conn() !&DockerConn { } // close closes the underlying socket connection. -pub fn (mut d DockerConn) close() ! { - d.socket.close()! +pub fn (mut d DockerConn) close() ? { + d.socket.close()? } // send_request sends an HTTP request without body. -fn (mut d DockerConn) send_request(method http.Method, url_str string) ! { - url := urllib.parse('/$docker.api_version$url_str')! +fn (mut d DockerConn) send_request(method http.Method, url_str string) ? { + url := urllib.parse('/$docker.api_version$url_str')? req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\n\n' - d.socket.write_string(req)! + d.socket.write_string(req)? // When starting a new request, the reader needs to be reset. d.reader = io.new_buffered_reader(reader: d.socket) } // send_request_with_body sends an HTTP request with the given body. -fn (mut d DockerConn) send_request_with_body(method http.Method, url_str string, content_type string, body string) ! { - url := urllib.parse('/$docker.api_version$url_str')! +fn (mut d DockerConn) send_request_with_body(method http.Method, url_str string, content_type string, body string) ? { + url := urllib.parse('/$docker.api_version$url_str')? req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\nContent-Type: $content_type\nContent-Length: $body.len\n\n$body\n\n' - d.socket.write_string(req)! + d.socket.write_string(req)? // When starting a new request, the reader needs to be reset. d.reader = io.new_buffered_reader(reader: d.socket) @@ -71,7 +64,7 @@ fn (mut d DockerConn) send_request_with_body(method http.Method, url_str string, // send_request_with_json is a convenience wrapper around // send_request_with_body that encodes the input as JSON. -fn (mut d DockerConn) send_request_with_json(method http.Method, url_str string, data &T) ! { +fn (mut d DockerConn) send_request_with_json(method http.Method, url_str string, data &T) ? { body := json.encode(data) return d.send_request_with_body(method, url_str, 'application/json', body) @@ -81,17 +74,17 @@ fn (mut d DockerConn) send_request_with_json(method http.Method, url_str stri // '\r\n\r\n', after which it parses the response as an HTTP response. // Importantly, this function never consumes the reader past the HTTP // separator, so the body can be read fully later on. -fn (mut d DockerConn) read_response_head() !http.Response { +fn (mut d DockerConn) read_response_head() ?http.Response { mut res := []u8{} - util.read_until_separator(mut d.reader, mut res, docker.http_separator)! + util.read_until_separator(mut d.reader, mut res, docker.http_separator)? return http.parse_response(res.bytestr()) } // read_response_body reads `length` bytes from the stream. It can be used when // the response encoding isn't chunked to fully read it. -fn (mut d DockerConn) read_response_body(length int) !string { +fn (mut d DockerConn) read_response_body(length int) ?string { if length == 0 { return '' } @@ -103,7 +96,7 @@ fn (mut d DockerConn) read_response_body(length int) !string { for builder.len < length { c = d.reader.read(mut buf) or { break } - builder.write(buf[..c])! + builder.write(buf[..c])? } return builder.str() @@ -112,34 +105,34 @@ fn (mut d DockerConn) read_response_body(length int) !string { // read_response is a convenience function which always consumes the entire // response & returns it. It should only be used when we're certain that the // result isn't too large. -fn (mut d DockerConn) read_response() !(http.Response, string) { - head := d.read_response_head()! +fn (mut d DockerConn) read_response() ?(http.Response, string) { + head := d.read_response_head()? if head.header.get(http.CommonHeader.transfer_encoding) or { '' } == 'chunked' { mut builder := strings.new_builder(1024) mut body := d.get_chunked_response_reader() - util.reader_to_writer(mut body, mut builder)! + util.reader_to_writer(mut body, mut builder)? return head, builder.str() } - content_length := head.header.get(http.CommonHeader.content_length)!.int() - res := d.read_response_body(content_length)! + content_length := head.header.get(http.CommonHeader.content_length)?.int() + res := d.read_response_body(content_length)? return head, res } -fn (mut d DockerConn) read_json_response() !T { - head, body := d.read_response()! +fn (mut d DockerConn) read_json_response() ?T { + head, body := d.read_response()? if head.status_code < 200 || head.status_code > 300 { - data := json.decode(DockerError, body)! + data := json.decode(DockerError, body)? return docker_error(head.status_code, data.message) } - mut data := json.decode(T, body)! + mut data := json.decode(T, body)? //$for field in T.fields { //$if field.typ is time.Time { diff --git a/images.v b/images.v index a409875..a7659dd 100644 --- a/images.v +++ b/images.v @@ -9,14 +9,14 @@ pub: } // pull_image pulls the given image:tag. -pub fn (mut d DockerConn) pull_image(image string, tag string) ! { - d.send_request(Method.post, '/images/create?fromImage=$image&tag=$tag')! - head := d.read_response_head()! +pub fn (mut d DockerConn) pull_image(image string, tag string) ? { + d.send_request(Method.post, '/images/create?fromImage=$image&tag=$tag')? + head := d.read_response_head()? if head.status_code != 200 { - content_length := head.header.get(http.CommonHeader.content_length)!.int() - body := d.read_response_body(content_length)! - data := json.decode(DockerError, body)! + content_length := head.header.get(http.CommonHeader.content_length)?.int() + body := d.read_response_body(content_length)? + data := json.decode(DockerError, body)? return error(data.message) } @@ -32,28 +32,28 @@ pub fn (mut d DockerConn) pull_image(image string, tag string) ! { } // create_image_from_container creates a new image from a container. -pub fn (mut d DockerConn) create_image_from_container(id string, repo string, tag string) !Image { - d.send_request(Method.post, '/commit?container=$id&repo=$repo&tag=$tag')! - head, body := d.read_response()! +pub fn (mut d DockerConn) create_image_from_container(id string, repo string, tag string) ?Image { + d.send_request(Method.post, '/commit?container=$id&repo=$repo&tag=$tag')? + head, body := d.read_response()? if head.status_code != 201 { - data := json.decode(DockerError, body)! + data := json.decode(DockerError, body)? return error(data.message) } - data := json.decode(Image, body)! + data := json.decode(Image, body)? return data } // remove_image removes the image with the given id. -pub fn (mut d DockerConn) remove_image(id string) ! { - d.send_request(Method.delete, '/images/$id')! - head, body := d.read_response()! +pub fn (mut d DockerConn) remove_image(id string) ? { + d.send_request(Method.delete, '/images/$id')? + head, body := d.read_response()? if head.status_code != 200 { - data := json.decode(DockerError, body)! + data := json.decode(DockerError, body)? return error(data.message) } diff --git a/request.v b/request.v deleted file mode 100644 index fdee0d4..0000000 --- a/request.v +++ /dev/null @@ -1,51 +0,0 @@ -module docker - -import net.http -import net.urllib -import io - -fn (mut d DockerConn) request(method http.Method, url_str string) { - d.method = method - d.url = url_str - d.params.clear() - d.content_type = '' - d.body = '' -} - -fn (mut d DockerConn) get(url_str string) { - d.request(http.Method.get, url_str) -} - -fn (mut d DockerConn) params(o T) { - $for field in T.fields { - v := o.$(field.name) - - if !isnil(v) { - d.params[field.name] = urllib.query_escape(v.str().replace("'", '"')) - } - } -} - -fn (mut d DockerConn) send() ! { - mut full_url := d.url - - if d.params.len > 0 { - params_str := d.params.keys().map('$it=${d.params[it]}').join('&') - full_url += '?$params_str' - } - - // This is to make sure we actually created a valid URL - parsed_url := urllib.parse(full_url)! - final_url := parsed_url.request_uri() - - req := if d.body == '' { - '$d.method $final_url HTTP/1.1\nHost: localhost\n\n' - } else { - '$d.method $final_url HTTP/1.1\nHost: localhost\nContent-Type: $d.content_type\nContent-Length: $d.body.len\n\n$d.body\n\n' - } - - d.socket.write_string(req)! - - // When starting a new request, the reader needs to be reset. - d.reader = io.new_buffered_reader(reader: d.socket) -} diff --git a/stream.v b/stream.v index 39ab42a..001f4b3 100644 --- a/stream.v +++ b/stream.v @@ -25,11 +25,11 @@ pub fn new_chunked_response_reader(reader io.BufferedReader) &ChunkedResponseRea } // read satisfies the io.Reader interface. -pub fn (mut r ChunkedResponseReader) read(mut buf []u8) !int { +pub fn (mut r ChunkedResponseReader) read(mut buf []u8) ?int { if r.bytes_left_in_chunk == 0 { // An io.BufferedReader always returns none if its stream has // ended. - r.bytes_left_in_chunk = r.read_chunk_size()! + r.bytes_left_in_chunk = r.read_chunk_size()? } mut c := 0 @@ -37,9 +37,9 @@ pub fn (mut r ChunkedResponseReader) read(mut buf []u8) !int { // Make sure we don't read more than we can safely read. This is to avoid // the underlying reader from becoming out of sync with our parsing: if buf.len > r.bytes_left_in_chunk { - c = r.reader.read(mut buf[..r.bytes_left_in_chunk])! + c = r.reader.read(mut buf[..r.bytes_left_in_chunk])? } else { - c = r.reader.read(mut buf)! + c = r.reader.read(mut buf)? } r.bytes_left_in_chunk -= u64(c) @@ -50,21 +50,21 @@ pub fn (mut r ChunkedResponseReader) read(mut buf []u8) !int { // read_chunk_size advances the reader & reads the size of the next HTTP chunk. // This function should only be called if the previous chunk has been // completely consumed. -fn (mut r ChunkedResponseReader) read_chunk_size() !u64 { +fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { if r.started { mut buf := []u8{len: 2} // Each chunk ends with a `\r\n` which we want to skip first - r.reader.read(mut buf)! + r.reader.read(mut buf)? } r.started = true mut res := []u8{} - util.read_until_separator(mut r.reader, mut res, http_chunk_separator)! + util.read_until_separator(mut r.reader, mut res, http_chunk_separator)? // The length of the next chunk is provided as a hexadecimal - mut num_data := hex.decode(res#[..-2].bytestr())! + mut num_data := hex.decode(res#[..-2].bytestr())? for num_data.len < 8 { num_data.insert(0, 0) @@ -75,7 +75,7 @@ fn (mut r ChunkedResponseReader) read_chunk_size() !u64 { // This only occurs for the very last chunk, which always reports a size of // 0. if num == 0 { - return error('end of stream') + return none } return num @@ -100,17 +100,17 @@ pub fn new_stream_format_reader(reader ChunkedResponseReader) &StreamFormatReade } // read satisfies the io.Reader interface. -pub fn (mut r StreamFormatReader) read(mut buf []u8) !int { +pub fn (mut r StreamFormatReader) read(mut buf []u8) ?int { if r.bytes_left_in_chunk == 0 { - r.bytes_left_in_chunk = r.read_chunk_size()! + r.bytes_left_in_chunk = r.read_chunk_size()? } mut c := 0 if buf.len > r.bytes_left_in_chunk { - c = r.reader.read(mut buf[..r.bytes_left_in_chunk])! + c = r.reader.read(mut buf[..r.bytes_left_in_chunk])? } else { - c = r.reader.read(mut buf)! + c = r.reader.read(mut buf)? } r.bytes_left_in_chunk -= u32(c) @@ -120,15 +120,15 @@ pub fn (mut r StreamFormatReader) read(mut buf []u8) !int { // read_chunk_size advances the reader & reads the header bytes for the length // of the next chunk. -fn (mut r StreamFormatReader) read_chunk_size() !u32 { +fn (mut r StreamFormatReader) read_chunk_size() ?u32 { mut buf := []u8{len: 8} - r.reader.read(mut buf)! + r.reader.read(mut buf)? num := binary.big_endian_u32(buf[4..]) if num == 0 { - return error('end of stream') + return none } return num diff --git a/types/container.v b/types/container.v deleted file mode 100644 index ec26940..0000000 --- a/types/container.v +++ /dev/null @@ -1,111 +0,0 @@ -module types - -pub struct Port { -pub: - ip string [json: IP] - private_port u16 [json: PrivatePort] - public_port u16 [json: PublicPort] - type_ string [json: Type] -} - -pub struct HostConfig { -pub: - network_mode string [json: NetworkMode] -} - -pub struct EndpointIpamConfig { -pub: - ipv4_address string [json: IPv4Address] - ipv6_address string [json: IPv6Address] - link_local_ips []string [json: LinkLocalIPs] -} - -pub struct EndpointSettings { -pub: - ipam_config EndpointIpamConfig [json: IPAMConfig] - links []string [json: Links] - aliases []string [json: Aliases] - network_id string [json: NetworkID] - endpoint_id string [json: EndpointID] - gateway string [json: Gateway] - ip_address string [json: IPAddress] - ip_prefix_len int [json: IPPrefixLen] - ipv6_gateway string [json: IPv6Gateway] - global_ipv6_address string [json: GlobalIPv6Address] - global_ipv6_prefix_len i64 [json: GlobalIPv6PrefixLen] - mac_address string [json: MacAddress] - driver_opts map[string]string [json: DriverOpts] -} - -pub struct NetworkSettings { -pub: - networks map[string]EndpointSettings [json: Networks] -} - -pub struct MountPoint { -pub: - type_ string [json: Type] - name string [json: Name] - source string [json: Source] - destination string [json: Destination] - driver string [json: Driver] - mode string [json: Mode] - rw bool [json: RW] - propagation string [json: Propagation] -} - -pub struct ContainerListItem { -pub: - id string [json: Id] - names []string [json: Names] - image string [json: Image] - image_id string [json: ImageID] - command string [json: Command] - created i64 [json: Created] - ports []Port [json: Ports] - size_rw i64 [json: SizeRw] - size_root_fs i64 [json: SizeRootFs] - labels map[string]string [json: Labels] - state string [json: State] - status string [json: Status] - host_config HostConfig [json: HostConfig] - network_settings NetworkSettings [json: NetworkSettings] - mounts []MountPoint [json: Mounts] -} - -pub struct HealthConfig { -pub: - test []string [json: Test] - interval int [json: Interval] - timeout int [json: Timeout] - retries int [json: Retries] - start_period int [json: StartPeriod] -} - -pub struct ContainerCreate { -pub: - hostname string [json: Hostname] - domain_name string [json: Domainname] - user string [json: User] - attach_stdin bool [json: AttachStdin] - attach_stdout bool [json: AttachStderr] = true - // ExposedPorts - tty bool [json: Tty] - open_stdin bool [json: OpenStdin] - stdin_once bool [json: StdinOnce] - env []string [json: Env] - cmd []string [json: Cmd] - healthcheck HealthConfig [json: Healthcheck] - args_escaped bool [json: ArgsEscaped] - image string [json: Image] - // Volumes - working_dir string [json: WorkingDir] - entrypoint []string [json: Entrypoint] - network_disabled bool [json: NetworkDisabled] - mac_address string [json: MacAddress] - on_build []string [json: OnBuild] - labels map[string]string [json: Labels] - stop_signal string [json: StopSignal] - stop_timeout int [json: StopTimeout] - shell []string [json: Shell] -} diff --git a/util/util.v b/util/util.v index 1181480..412e39c 100644 --- a/util/util.v +++ b/util/util.v @@ -3,7 +3,7 @@ module util import io // reader_to_writer tries to consume the entire reader & write it to the writer. -pub fn reader_to_writer(mut reader io.Reader, mut writer io.Writer) ! { +pub fn reader_to_writer(mut reader io.Reader, mut writer io.Writer) ? { mut buf := []u8{len: 10 * 1024} for { @@ -39,11 +39,11 @@ pub fn match_array_in_array(a1 []T, a2 []T) int { // read_until_separator consumes an io.Reader until it encounters some // separator array. The data read is stored inside the provided res array. -pub fn read_until_separator(mut reader io.Reader, mut res []u8, sep []u8) ! { +pub fn read_until_separator(mut reader io.Reader, mut res []u8, sep []u8) ? { mut buf := []u8{len: sep.len} for { - c := reader.read(mut buf)! + c := reader.read(mut buf)? res << buf[..c] match_len := match_array_in_array(buf[..c], sep) @@ -54,7 +54,7 @@ pub fn read_until_separator(mut reader io.Reader, mut res []u8, sep []u8) ! { if match_len > 0 { match_left := sep.len - match_len - c2 := reader.read(mut buf[..match_left])! + c2 := reader.read(mut buf[..match_left])? res << buf[..c2] if buf[..c2] == sep[match_len..] { diff --git a/volumes.v b/volumes.v index 5bd9936..00db10a 100644 --- a/volumes.v +++ b/volumes.v @@ -35,13 +35,13 @@ struct VolumeListResponse { warnings []string [json: Warnings] } -pub fn (mut d DockerConn) volume_list() !VolumeListResponse { - d.send_request(Method.get, '/volumes')! +pub fn (mut d DockerConn) volume_list() ?VolumeListResponse { + d.send_request(Method.get, '/volumes')? - mut data := d.read_json_response()! + mut data := d.read_json_response()? for mut vol in data.volumes { - vol.created_at = time.parse_rfc3339(vol.created_at_str)! + vol.created_at = time.parse_rfc3339(vol.created_at_str)? } return data