Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ redraw = ">= 19.2.2 and < 20.0.0"
redraw_dom = ">= 19.2.2 and < 20.0.0"
lustre = ">= 5.7.1 and < 6.0.0"
mendraw = ">= 2.0.0 and < 3.0.0"
simplifile = ">= 2.6.0 and < 3.0.0"

[dev_dependencies]
gleeunit = ">= 1.11.0 and < 2.0.0"
3 changes: 3 additions & 0 deletions manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
packages = [
{ name = "etch", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "etch", source = "hex", outer_checksum = "A4B4458EE1E7D22753538C03F0D64C930309863365D2D26570E2D2C6B7CFC5EA" },
{ name = "exception", version = "2.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "6BDEA95248093599391C3B5DF1835C5C6A86C353C2F99CE539B450E3432FE117" },
{ name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" },
{ name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" },
{ name = "gleam_fetch", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "284CE88E37436699545F9F65D413E1DFB6C1EA3FE3824B6EA2018D0ECF088FFC" },
{ name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" },
Expand All @@ -22,6 +23,7 @@ packages = [
{ name = "mendraw", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "redraw", "redraw_dom"], otp_app = "mendraw", source = "hex", outer_checksum = "BB913054A5FEDC872AAB114FFADDFB791DACFE605B3DCEEAD3620F11BB59D090" },
{ name = "redraw", version = "19.2.2", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_stdlib"], otp_app = "redraw", source = "hex", outer_checksum = "B8CEEB74E8846CE10B8360B924DAD22441B61D947F9449854164F0686C4B8661" },
{ name = "redraw_dom", version = "19.2.2", build_tools = ["gleam"], requirements = ["gleam_fetch", "gleam_stdlib", "redraw"], otp_app = "redraw_dom", source = "hex", outer_checksum = "80278296AD6E3D4457D6FF6A14FEA3E90696284BEA297BDE10E430FC4CE726B8" },
{ name = "simplifile", version = "2.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "A2727627B063E87351934C7F7F008F2D1FDB16F6DE0B8C79F9E46459CFC9C164" },
]

[requirements]
Expand All @@ -34,3 +36,4 @@ lustre = { version = ">= 5.7.1 and < 6.0.0" }
mendraw = { version = ">= 2.0.0 and < 3.0.0" }
redraw = { version = ">= 19.2.2 and < 20.0.0" }
redraw_dom = { version = ">= 19.2.2 and < 20.0.0" }
simplifile = { version = ">= 2.6.0 and < 3.0.0" }
138 changes: 91 additions & 47 deletions src/glendix/define/file_boundary.gleam
Original file line number Diff line number Diff line change
@@ -1,83 +1,127 @@
//// Provides typed filesystem operations for the widget definition editor.
////

import gleam/dynamic
import gleam/dynamic/decode
import gleam/json
import gleam/list
import gleam/option
import gleam/result
import gleam/string
import simplifile

/// Describes a widget definition filesystem failure.
pub type FileError {
/// The current package does not declare a widget name.
WidgetNameWasNotDeclared(path: String)
/// The expected widget XML file does not exist.
WidgetXmlWasNotFound(path: String)
/// A file could not be read.
/// A file could not be read or decoded.
///
/// Filesystem reasons come from `simplifile.describe_error`. JSON reasons
/// use Glendix's stable description of `gleam_json.DecodeError`.
FileCouldNotBeRead(path: String, reason: String)
/// A file could not be written.
///
/// The reason comes from `simplifile.describe_error`.
FileCouldNotBeWritten(path: String, reason: String)
}

/// Finds the widget XML path declared by the current package.
pub fn find_widget_xml() -> Result(String, FileError) {
find_widget_xml_raw()
|> result.map_error(map_raw_error)
let package_path = "package.json"
use package_contents <- result.try(read(package_path))
use widget_name <- result.try(widget_name(package_contents, package_path))
let widget_path = "src/" <> widget_name <> ".xml"

case simplifile.is_file(widget_path) {
Ok(True) -> Ok(widget_path)
Ok(False) -> Error(WidgetXmlWasNotFound(path: widget_path))
Error(error) ->
Error(FileCouldNotBeRead(
path: widget_path,
reason: simplifile.describe_error(error),
))
}
}

/// Reads a UTF-8 text file.
pub fn read(path path: String) -> Result(String, FileError) {
read_raw(path)
|> result.map_error(map_raw_error)
simplifile.read(from: path)
|> result.map_error(fn(error) {
FileCouldNotBeRead(path: path, reason: simplifile.describe_error(error))
})
}

/// Writes a UTF-8 text file.
pub fn write(
path path: String,
content content: String,
) -> Result(Nil, FileError) {
write_raw(path, content)
|> result.map_error(map_raw_error)
simplifile.write(to: path, contents: content)
|> result.map_error(fn(error) {
FileCouldNotBeWritten(path: path, reason: simplifile.describe_error(error))
})
}

type RawFileError

fn map_raw_error(error: RawFileError) -> FileError {
case raw_file_error_kind(error) {
1 -> WidgetNameWasNotDeclared(path: raw_file_error_path(error))
2 -> WidgetXmlWasNotFound(path: raw_file_error_path(error))
3 ->
FileCouldNotBeRead(
path: raw_file_error_path(error),
reason: raw_file_error_reason(error),
)
4 ->
FileCouldNotBeWritten(
path: raw_file_error_path(error),
reason: raw_file_error_reason(error),
)
_ ->
FileCouldNotBeRead(
path: raw_file_error_path(error),
reason: raw_file_error_reason(error),
)
fn widget_name(
package_contents: String,
package_path: String,
) -> Result(String, FileError) {
case json.parse(package_contents, package_widget_name_decoder()) {
Error(error) ->
Error(FileCouldNotBeRead(
path: package_path,
reason: json_decode_error_reason(error),
))
Ok(option.None) -> Error(WidgetNameWasNotDeclared(path: package_path))
Ok(option.Some(value)) ->
case decode.run(value, decode.string) {
Ok(name) ->
case string.trim(name) {
"" -> Error(WidgetNameWasNotDeclared(path: package_path))
_ -> Ok(name)
}
Error(_) -> Error(WidgetNameWasNotDeclared(path: package_path))
}
}
}

// -- FFI --
@external(javascript, "./file_boundary_ffi.mjs", "find_widget_xml")
fn find_widget_xml_raw() -> Result(String, RawFileError)

@external(javascript, "./file_boundary_ffi.mjs", "read_file")
fn read_raw(path path: String) -> Result(String, RawFileError)

@external(javascript, "./file_boundary_ffi.mjs", "write_file")
fn write_raw(
path path: String,
content content: String,
) -> Result(Nil, RawFileError)

@external(javascript, "./file_boundary_ffi.mjs", "file_error_kind")
fn raw_file_error_kind(error: RawFileError) -> Int
fn package_widget_name_decoder() -> decode.Decoder(
option.Option(dynamic.Dynamic),
) {
let object_decoder = {
use name <- decode.optional_field(
"widgetName",
option.None,
decode.optional(decode.dynamic),
)
decode.success(name)
}
decode.one_of(object_decoder, or: [decode.success(option.None)])
}

@external(javascript, "./file_boundary_ffi.mjs", "file_error_path")
fn raw_file_error_path(error: RawFileError) -> String
fn json_decode_error_reason(error: json.DecodeError) -> String {
case error {
json.UnexpectedEndOfInput -> "JSON ended unexpectedly"
json.UnexpectedByte(byte) -> "JSON contained unexpected byte: " <> byte
json.UnexpectedSequence(sequence) ->
"JSON contained unexpected sequence: " <> sequence
json.UnableToDecode(errors) -> {
let reasons =
errors
|> list.map(dynamic_decode_error_reason)
|> string.join("; ")
"JSON value could not be decoded: " <> reasons
}
}
}

@external(javascript, "./file_boundary_ffi.mjs", "file_error_reason")
fn raw_file_error_reason(error: RawFileError) -> String
fn dynamic_decode_error_reason(error: decode.DecodeError) -> String {
let decode.DecodeError(expected, found, path) = error
let location = case path {
[] -> "root"
[_, ..] -> string.join(path, ".")
}
location <> " expected " <> expected <> " but found " <> found
}
84 changes: 0 additions & 84 deletions src/glendix/define/file_boundary_ffi.mjs

This file was deleted.

Loading
Loading