From cb909693a3cf1bcc56af195eb1860d10da754954 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Mon, 26 Feb 2024 15:14:48 +0100 Subject: [PATCH 1/3] add decoders --- CHANGELOG | 1 + .../decoding_a_module_interface.accepted | 164 ++++++ gleam.toml | 4 + manifest.toml | 17 + priv/interface.json | 168 ++++++ priv/interface_module.gleam | 29 ++ src/gleam/package_interface.gleam | 493 +++++++++++++++++- test/gleam_package_interface_test.gleam | 276 +++++++++- 8 files changed, 1144 insertions(+), 8 deletions(-) create mode 100644 CHANGELOG create mode 100644 birdie_snapshots/decoding_a_module_interface.accepted create mode 100644 priv/interface.json create mode 100644 priv/interface_module.gleam diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..825c32f --- /dev/null +++ b/CHANGELOG @@ -0,0 +1 @@ +# Changelog diff --git a/birdie_snapshots/decoding_a_module_interface.accepted b/birdie_snapshots/decoding_a_module_interface.accepted new file mode 100644 index 0000000..9a93507 --- /dev/null +++ b/birdie_snapshots/decoding_a_module_interface.accepted @@ -0,0 +1,164 @@ +--- +version: 1.0.4 +title: Decoding a module interface +--- +Package( + name: "gleam_package_interface", + version: "1.0.0", + gleam_version_contraint: None(), + modules: { + my_module: Module( + documentation: [], + type_aliases: { + Wobble: TypeAlias( + alias: Named( + package: "gleam_package_interface", + module: "my_module", + name: "Wibble", + parameters: [ + Named( + package: "", + module: "gleam", + name: "Int", + parameters: [], + ), + ], + ), + deprecation: Some( + item: Deprecation(message: "this is deprecated!"), + ), + documentation: Some(item: " Documentation!"), + parameters: 0, + ), + }, + constants: { + wabble: Constant( + deprecation: None(), + documentation: None(), + implementations: Implementations( + gleam: True, + uses_erlang_externals: False, + uses_javascript_externals: False, + ), + type_: Fn( + parameters: [ + Named( + package: "", + module: "gleam", + name: "String", + parameters: [], + ), + ], + return: Named( + package: "gleam_package_interface", + module: "my_module", + name: "Wibble", + parameters: [Variable(id: 0)], + ), + ), + ), + wibble: Constant( + deprecation: None(), + documentation: Some(item: " Documentation!"), + implementations: Implementations( + gleam: True, + uses_erlang_externals: False, + uses_javascript_externals: False, + ), + type_: Named( + package: "gleam_package_interface", + module: "my_module", + name: "Wibble", + parameters: [ + Named( + package: "", + module: "gleam", + name: "Int", + parameters: [], + ), + ], + ), + ), + wobble: Constant( + deprecation: None(), + documentation: None(), + implementations: Implementations( + gleam: True, + uses_erlang_externals: False, + uses_javascript_externals: False, + ), + type_: Tuple( + elements: [ + Named( + package: "", + module: "gleam", + name: "Int", + parameters: [], + ), + Named( + package: "", + module: "gleam", + name: "Int", + parameters: [], + ), + ], + ), + ), + }, + functions: { + main: Function( + deprecation: None(), + documentation: Some(item: " Documentation!"), + implementations: Implementations( + gleam: True, + uses_erlang_externals: False, + uses_javascript_externals: False, + ), + parameters: [ + Parameter( + label: Some(item: "wibble"), + type: Named( + package: "", + module: "gleam", + name: "String", + parameters: [], + ), + ), + ], + return: Named( + package: "gleam_package_interface", + module: "my_module", + name: "Wibble", + parameters: [Variable(id: 0)], + ), + ), + }, + types: { + Wibble: TypeDefinition( + constructors: [ + TypeConstructor( + documentation: Some(item: " Documentation!"), + name: "Wibble", + parameters: [ + Parameter(label: Some(item: "label"), type: Variable(id: 0)), + ], + ), + TypeConstructor( + documentation: Some(item: " Documentation!"), + name: "Wobble", + parameters: [Parameter(label: None(), type: Variable(id: 0))], + ), + TypeConstructor( + documentation: None(), + name: "Wabble", + parameters: [], + ), + ], + deprecation: None(), + documentation: Some(item: " Documentation!"), + parameters: 1, + ), + }, + ), + }, +) \ No newline at end of file diff --git a/gleam.toml b/gleam.toml index bf6aed1..8a65096 100644 --- a/gleam.toml +++ b/gleam.toml @@ -8,6 +8,10 @@ links = [{ title = "Website", href = "https://gleam.run" }] [dependencies] gleam_stdlib = "~> 0.34 or ~> 1.0" +gleam_json = "~> 1.0" [dev-dependencies] gleeunit = "~> 1.0" +simplifile = "~> 1.4" +birdie = "~> 1.0" +glam = "~> 2.0" diff --git a/manifest.toml b/manifest.toml index 7762492..3fdde71 100644 --- a/manifest.toml +++ b/manifest.toml @@ -2,10 +2,27 @@ # You typically do not need to edit this file packages = [ + { name = "argv", version = "1.0.1", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "A6E9009E50BBE863EB37D963E4315398D41A3D87D0075480FC244125808F964A" }, + { name = "birdie", version = "1.0.4", build_tools = ["gleam"], requirements = ["argv", "filepath", "gap", "gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "gleeunit", "justin", "rank", "simplifile"], otp_app = "birdie", source = "hex", outer_checksum = "0F7E16A3B12957B5B4A3B39152BD6D6175941AF40C1838F86C5A909DCFF7CF04" }, + { name = "filepath", version = "0.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "FC1B1B29438A5BA6C990F8047A011430BEC0C5BA638BFAA62718C4EAEFE00435" }, + { name = "gap", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_stdlib"], otp_app = "gap", source = "hex", outer_checksum = "2EE1B0A17E85CF73A0C1D29DA315A2699117A8F549C8E8D89FA8261BE41EDEB1" }, + { name = "glam", version = "2.0.0", build_tools = ["gleam"], requirements = ["birdie", "gleam_stdlib"], otp_app = "glam", source = "hex", outer_checksum = "1C10BE5EA72659E409DC2325BA5E94E0CC92C6C50B2A1DBADE6D07E8C9484D51" }, + { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" }, + { name = "gleam_community_colour", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "A49A5E3AE8B637A5ACBA80ECB9B1AFE89FD3D5351FF6410A42B84F666D40D7D5" }, + { name = "gleam_erlang", version = "0.24.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "26BDB52E61889F56A291CB34167315780EE4AA20961917314446542C90D1C1A0" }, + { name = "gleam_json", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "8B197DD5D578EA6AC2C0D4BDC634C71A5BCA8E7DB5F47091C263ECB411A60DF3" }, { name = "gleam_stdlib", version = "0.34.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "1FB8454D2991E9B4C0C804544D8A9AD0F6184725E20D63C3155F0AEB4230B016" }, { name = "gleeunit", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D364C87AFEB26BDB4FB8A5ABDE67D635DC9FA52D6AB68416044C35B096C6882D" }, + { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, + { name = "rank", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "rank", source = "hex", outer_checksum = "5660E361F0E49CBB714CC57CC4C89C63415D8986F05B2DA0C719D5642FAD91C9" }, + { name = "simplifile", version = "1.4.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "C93A62CE23B2E44F969DC3134F9EE899C362B0A2F4181233CDD5D759F82EE486" }, + { name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" }, ] [requirements] +birdie = { version = "~> 1.0" } +glam = { version = "~> 2.0" } +gleam_json = { version = "~> 1.0" } gleam_stdlib = { version = "~> 0.34 or ~> 1.0" } gleeunit = { version = "~> 1.0" } +simplifile = { version = "~> 1.4" } diff --git a/priv/interface.json b/priv/interface.json new file mode 100644 index 0000000..8ec4770 --- /dev/null +++ b/priv/interface.json @@ -0,0 +1,168 @@ +{ + "name": "gleam_package_interface", + "version": "1.0.0", + "gleam-version-constraint": null, + "modules": { + "my_module": { + "documentation": [], + "type-aliases": { + "Wobble": { + "documentation": " Documentation!", + "deprecation": { "message": "this is deprecated!" }, + "parameters": 0, + "alias": { + "kind": "named", + "name": "Wibble", + "package": "gleam_package_interface", + "module": "my_module", + "parameters": [ + { + "kind": "named", + "name": "Int", + "package": "", + "module": "gleam", + "parameters": [] + } + ] + } + } + }, + "types": { + "Wibble": { + "documentation": " Documentation!", + "deprecation": null, + "parameters": 1, + "constructors": [ + { + "documentation": " Documentation!", + "name": "Wibble", + "parameters": [ + { "label": "label", "type": { "kind": "variable", "id": 0 } } + ] + }, + { + "documentation": " Documentation!", + "name": "Wobble", + "parameters": [ + { "label": null, "type": { "kind": "variable", "id": 0 } } + ] + }, + { "documentation": null, "name": "Wabble", "parameters": [] } + ] + } + }, + "constants": { + "wabble": { + "documentation": null, + "deprecation": null, + "implementations": { + "gleam": true, + "uses-erlang-externals": false, + "uses-javascript-externals": false + }, + "type": { + "kind": "fn", + "parameters": [ + { + "kind": "named", + "name": "String", + "package": "", + "module": "gleam", + "parameters": [] + } + ], + "return": { + "kind": "named", + "name": "Wibble", + "package": "gleam_package_interface", + "module": "my_module", + "parameters": [{ "kind": "variable", "id": 0 }] + } + } + }, + "wibble": { + "documentation": " Documentation!", + "deprecation": null, + "implementations": { + "gleam": true, + "uses-erlang-externals": false, + "uses-javascript-externals": false + }, + "type": { + "kind": "named", + "name": "Wibble", + "package": "gleam_package_interface", + "module": "my_module", + "parameters": [ + { + "kind": "named", + "name": "Int", + "package": "", + "module": "gleam", + "parameters": [] + } + ] + } + }, + "wobble": { + "documentation": null, + "deprecation": null, + "implementations": { + "gleam": true, + "uses-erlang-externals": false, + "uses-javascript-externals": false + }, + "type": { + "kind": "tuple", + "elements": [ + { + "kind": "named", + "name": "Int", + "package": "", + "module": "gleam", + "parameters": [] + }, + { + "kind": "named", + "name": "Int", + "package": "", + "module": "gleam", + "parameters": [] + } + ] + } + } + }, + "functions": { + "main": { + "documentation": " Documentation!", + "deprecation": null, + "implementations": { + "gleam": true, + "uses-erlang-externals": false, + "uses-javascript-externals": false + }, + "parameters": [ + { + "label": "wibble", + "type": { + "kind": "named", + "name": "String", + "package": "", + "module": "gleam", + "parameters": [] + } + } + ], + "return": { + "kind": "named", + "name": "Wibble", + "package": "gleam_package_interface", + "module": "my_module", + "parameters": [{ "kind": "variable", "id": 0 }] + } + } + } + } + } +} diff --git a/priv/interface_module.gleam b/priv/interface_module.gleam new file mode 100644 index 0000000..4da5ba9 --- /dev/null +++ b/priv/interface_module.gleam @@ -0,0 +1,29 @@ +//// This is the module used to generate the json interface +//// used by the tests! +//// + +/// Documentation! +pub type Wibble(a) { + /// Documentation! + Wibble(label: a) + /// Documentation! + Wobble(a) + Wabble +} + +/// Documentation! +@deprecated("this is deprecated!") +pub type Wobble = + Wibble(Int) + +/// Documentation! +pub const wibble = Wibble(1) + +pub const wobble = #(1, 1) + +pub const wabble = main + +/// Documentation! +pub fn main(wibble _wobble: String) -> Wibble(a) { + Wabble +} diff --git a/src/gleam/package_interface.gleam b/src/gleam/package_interface.gleam index f3c8ff3..eb0dbec 100644 --- a/src/gleam/package_interface.gleam +++ b/src/gleam/package_interface.gleam @@ -1,5 +1,492 @@ -import gleam/io +import gleam/dict.{type Dict} +import gleam/dynamic.{type DecodeErrors, type Decoder, type Dynamic} +import gleam/option.{type Option} +import gleam/result -pub fn main() { - io.println("Hello from package_interface_decoder!") +// --- TYPES ------------------------------------------------------------------- + +/// A Gleam package. +/// +pub type Package { + Package( + name: String, + version: String, + /// The Gleam version constraint that the package specifies in its + /// `gleam.toml`. + gleam_version_constraint: Option(String), + modules: Dict(String, Module), + ) +} + +/// A Gleam module. +/// +pub type Module { + Module( + /// All the lines composing the module's documentation (that is every line + /// preceded by a `////`). + documentation: List(String), + /// The public type aliases defined in the module. + type_aliases: Dict(String, TypeAlias), + /// The public custom types defined in the module. + types: Dict(String, TypeDefinition), + /// The public constants defined in the module. + constants: Dict(String, Constant), + /// The public functions defined in the module. + functions: Dict(String, Function), + ) +} + +/// A Gleam type alias. +/// +/// ```gleam +/// // This is a type alias. +/// type Ints = List(Int) +/// ``` +/// +pub type TypeAlias { + TypeAlias( + /// The type alias' documentation comment (that is every line preceded by + /// `///`). + /// + documentation: Option(String), + /// If the type alias is deprecated this will hold the reason of the + /// deprecation. + /// + deprecation: Option(Deprecation), + /// The number of type variables of the type alias. + /// + /// ```gleam + /// type Results(a, b) = List(Result(a, b)) + /// // ^^^^^^^^^^^^^ This type alias has 2 type variables. + /// + /// type Ints = List(Int) + /// // ^^^^ This type alias has 0 type variables. + /// ``` + /// + parameters: Int, + /// The aliased type. + /// + /// ```gleam + /// type Ints = List(Int) + /// // ^^^^^^^^^ This is the aliased type. + /// ``` + /// + alias: Type, + ) +} + +/// A Gleam custom type definition. +/// +/// ```gleam +/// // This is a custom type definition. +/// pub type Result(a, b) { +/// Ok(a) +/// Error(b) +/// } +/// ``` +/// +pub type TypeDefinition { + TypeDefinition( + /// The type definition's documentation comment (that is every line preceded + /// by `///`). + /// + documentation: Option(String), + /// If the type definition is deprecated this will hold the reason of the + /// deprecation. + /// + deprecation: Option(Deprecation), + /// The number of type variables of the type definition. + /// + /// ```gleam + /// type Result(a, b) { ... } + /// // ^^^^^^^^^^^^ This type definition has 2 type variables. + /// + /// type Person { ... } + /// // ^^^^^^ This type alias has 0 type variables. + /// ``` + /// + parameters: Int, + /// The type constructors. If the type is opaque this list will be empty as + /// the type doesn't have any public constructor. + /// + /// ```gleam + /// type Result(a, b) { + /// Ok(a) + /// Error(b) + /// } + /// // `Ok` and `Error` are the type constructors + /// // of the `Error` type. + /// ``` + /// + constructors: List(TypeConstructor), + ) +} + +/// A Gleam type constructor. +/// +/// ```gleam +/// type Result(a, b) { +/// Ok(a) +/// Error(b) +/// } +/// // `Ok` and `Error` are the type constructors +/// // of the `Error` type. +/// ``` +/// +pub type TypeConstructor { + TypeConstructor( + /// The type constructor's documentation comment (that is every line + /// preceded by `///`). + /// + documentation: Option(String), + name: String, + /// The parameters required by the constructor. + /// + /// ```gleam + /// type Box(a) { + /// Box(content: a) + /// // ^^^^^^^^^^ The `Box` constructor has a single + /// // labelled argument. + /// } + /// ``` + /// + parameters: List(Parameter), + ) +} + +/// A parameter (that might be labelled) of a module function or type +/// constructor. +/// +/// ```gleam +/// pub fn map(over list: List(a), with fun: fn(a) -> b) -> b { todo } +/// // ^^^^^^^^^^^^^^^^^^ A labelled parameter. +/// ``` +/// +pub type Parameter { + Parameter(label: Option(String), type_: Type) +} + +/// A Gleam constant. +/// +/// ```gleam +/// pub const my_favourite_number = 11 +/// ``` +/// +pub type Constant { + Constant( + /// The constant's documentation comment (that is every line preceded by + /// `///`). + /// + documentation: Option(String), + /// If the constant is deprecated this will hold the reason of the + /// deprecation. + /// + deprecation: Option(Deprecation), + implementations: Implementations, + type_: Type, + ) +} + +/// A Gleam function definition. +/// +/// ```gleam +/// pub fn reverse(list: List(a)) -> List(a) { todo } +/// ``` +pub type Function { + Function( + /// The function's documentation comment (that is every line preceded by + /// `///`). + /// + documentation: Option(String), + /// If the function is deprecated this will hold the reason of the + /// deprecation. + /// + deprecation: Option(Deprecation), + implementations: Implementations, + parameters: List(Parameter), + return: Type, + ) +} + +/// A deprecation notice that can be added to definition using the +/// `@deprecated` annotation. +/// +pub type Deprecation { + Deprecation(message: String) +} + +/// Metadata about how a value is implemented and the targets it supports. +/// +pub type Implementations { + Implementations( + /// Set to `True` if the const/function has a pure Gleam implementation + /// (that is, it never uses external code). + /// Being pure Gleam means that the function will support all Gleam + /// targets, even future ones that are not present to this day. + /// + /// Consider the following function: + /// + /// ```gleam + /// @external(erlang, "foo", "bar") + /// pub fn a_random_number() -> Int { + /// 4 + /// // This is a default implementation. + /// } + /// ``` + /// + /// The implementations for this function will look like this: + /// + /// ```gleam + /// Implementations( + /// gleam: True, + /// uses_erlang_externals: True, + /// uses_javascript_externals: False, + /// ) + /// ``` + /// + /// - `gleam: True` means that the function has a pure Gleam implementation + /// and thus it can be used on all Gleam targets with no problems. + /// - `uses_erlang_externals: True` means that the function will use Erlang + /// external code when compiled to the Erlang target. + /// - `uses_javascript_externals: False` means that the function won't use + /// JavaScript external code when compiled to JavaScript. The function can + /// still be used on the JavaScript target since it has a pure Gleam + /// implementation. + /// + gleam: Bool, + /// Set to `True` if the const/function is defined using Erlang external + /// code. That means that the function will use Erlang code through FFI when + /// compiled for the Erlang target. + /// + uses_erlang_externals: Bool, + /// Set to `True` if the const/function is defined using JavaScript external + /// code. That means that the function will use JavaScript code through FFI + /// when compiled for the JavaScript target. + /// + /// Let's have a look at an example: + /// + /// ```gleam + /// @external(javascript, "foo", "bar") + /// pub fn javascript_only() -> Int + /// ``` + /// + /// It's implementations field will look like this: + /// + /// ```gleam + /// Implementations( + /// gleam: False, + /// uses_erlang_externals: False, + /// uses_javascript_externals: True, + /// ) + /// ``` + /// + /// - `gleam: False` means that the function doesn't have a pure Gleam + /// implementations. This means that the function is only defined using + /// externals and can only be used on some targets. + /// - `uses_erlang_externals: False` the function is not using external + /// Erlang code. So, since the function doesn't have a fallback pure Gleam + /// implementation, you won't be able to compile it on this target. + /// - `uses_javascript_externals: True` the function is using JavaScript + /// external code. This means that you will be able to use it on the + /// JavaScript target with no problems. + /// + uses_javascript_externals: Bool, + ) +} + +/// A Gleam type. +/// +pub type Type { + /// A tuple type like `#(Int, Float)`. + /// + Tuple(elements: List(Type)) + /// A function type like `fn(Int, a) -> List(a)`. + /// + Fn(parameters: List(Type), return: Type) + /// A type variable. + /// + /// ```gleam + /// pub fn foo(value: a) -> a { todo } + /// // ^ This is a type variable. + /// ``` + /// + Variable(id: Int) + /// A custom named type. + /// ```gleam + /// let value: Bool = True + /// // ^^^^ Bool is a named type coming from Gleam's prelude + /// ``` + /// + /// All prelude types - like Bool, String, etc. - are named types as well. + /// In that case, their package is an empty string `""` and their module + /// name is the string `"gleam"`. + /// + Named( + name: String, + /// The package the type comes from. + /// + package: String, + /// The module the type is defined in. + /// + module: String, + /// The concrete type's type parameters + /// . + /// ```gleam + /// let result: Result(Int, e) = Ok(1) + /// // ^^^^^^^^ The `Result` named type has 2 parameters. + /// // In this case it's the `Int` type and a + /// // type variable. + /// ``` + /// + parameters: List(Type), + ) +} + +// --- DECODERS ---------------------------------------------------------------- + +pub fn decoder(dynamic: Dynamic) -> Result(Package, DecodeErrors) { + dynamic.decode4( + Package, + dynamic.field("name", dynamic.string), + dynamic.field("version", dynamic.string), + dynamic.field("gleam-version-constraint", dynamic.optional(dynamic.string)), + dynamic.field("modules", string_dict(module_decoder)), + )(dynamic) +} + +pub fn module_decoder(dynamic: Dynamic) -> Result(Module, DecodeErrors) { + dynamic.decode5( + Module, + dynamic.field("documentation", dynamic.list(dynamic.string)), + dynamic.field("type-aliases", string_dict(type_alias_decoder)), + dynamic.field("types", string_dict(type_definition_decoder)), + dynamic.field("constants", string_dict(constant_decoder)), + dynamic.field("functions", string_dict(function_decoder)), + )(dynamic) +} + +pub fn type_alias_decoder(dynamic: Dynamic) -> Result(TypeAlias, DecodeErrors) { + dynamic.decode4( + TypeAlias, + dynamic.field("documentation", dynamic.optional(dynamic.string)), + dynamic.field("deprecation", dynamic.optional(deprecation_decoder)), + dynamic.field("parameters", dynamic.int), + dynamic.field("alias", type_decoder), + )(dynamic) +} + +pub fn type_definition_decoder( + dynamic: Dynamic, +) -> Result(TypeDefinition, DecodeErrors) { + dynamic.decode4( + TypeDefinition, + dynamic.field("documentation", dynamic.optional(dynamic.string)), + dynamic.field("deprecation", dynamic.optional(deprecation_decoder)), + dynamic.field("parameters", dynamic.int), + dynamic.field("constructors", dynamic.list(constructor_decoder)), + )(dynamic) +} + +pub fn constant_decoder(dynamic: Dynamic) -> Result(Constant, DecodeErrors) { + dynamic.decode4( + Constant, + dynamic.field("documentation", dynamic.optional(dynamic.string)), + dynamic.field("deprecation", dynamic.optional(deprecation_decoder)), + dynamic.field("implementations", implementations_decoder), + dynamic.field("type", type_decoder), + )(dynamic) +} + +pub fn function_decoder(dynamic: Dynamic) -> Result(Function, DecodeErrors) { + dynamic.decode5( + Function, + dynamic.field("documentation", dynamic.optional(dynamic.string)), + dynamic.field("deprecation", dynamic.optional(deprecation_decoder)), + dynamic.field("implementations", implementations_decoder), + dynamic.field("parameters", dynamic.list(parameter_decoder)), + dynamic.field("return", type_decoder), + )(dynamic) +} + +pub fn deprecation_decoder( + dynamic: Dynamic, +) -> Result(Deprecation, DecodeErrors) { + dynamic.decode1(Deprecation, dynamic.field("message", dynamic.string))( + dynamic, + ) +} + +pub fn constructor_decoder( + dynamic: Dynamic, +) -> Result(TypeConstructor, DecodeErrors) { + dynamic.decode3( + TypeConstructor, + dynamic.field("documentation", dynamic.optional(dynamic.string)), + dynamic.field("name", dynamic.string), + dynamic.field("parameters", dynamic.list(parameter_decoder)), + )(dynamic) +} + +pub fn implementations_decoder( + dynamic: Dynamic, +) -> Result(Implementations, DecodeErrors) { + dynamic.decode3( + Implementations, + dynamic.field("gleam", dynamic.bool), + dynamic.field("uses-erlang-externals", dynamic.bool), + dynamic.field("uses-javascript-externals", dynamic.bool), + )(dynamic) +} + +pub fn parameter_decoder(dynamic: Dynamic) -> Result(Parameter, DecodeErrors) { + dynamic.decode2( + Parameter, + dynamic.field("label", dynamic.optional(dynamic.string)), + dynamic.field("type", type_decoder), + )(dynamic) +} + +pub fn type_decoder(dynamic: Dynamic) -> Result(Type, DecodeErrors) { + use kind <- result.try(dynamic.field("kind", dynamic.string)(dynamic)) + case kind { + "variable" -> + dynamic.decode1(Variable, dynamic.field("id", dynamic.int))(dynamic) + + "tuple" -> + dynamic.decode1( + Tuple, + dynamic.field("elements", dynamic.list(type_decoder)), + )(dynamic) + + "named" -> + dynamic.decode4( + Named, + dynamic.field("name", dynamic.string), + dynamic.field("package", dynamic.string), + dynamic.field("module", dynamic.string), + dynamic.field("parameters", dynamic.list(type_decoder)), + )(dynamic) + + "fn" -> + dynamic.decode2( + Fn, + dynamic.field("parameters", dynamic.list(type_decoder)), + dynamic.field("return", type_decoder), + )(dynamic) + + unknown_tag -> + Error([ + dynamic.DecodeError( + expected: "one of variable, tuple, named, fn", + found: unknown_tag, + path: ["kind"], + ), + ]) + } +} + +// --- UTILITY FUNCTIONS ------------------------------------------------------- + +fn string_dict(values: Decoder(a)) -> Decoder(Dict(String, a)) { + dynamic.dict(dynamic.string, values) } diff --git a/test/gleam_package_interface_test.gleam b/test/gleam_package_interface_test.gleam index 3831e7a..13e412c 100644 --- a/test/gleam_package_interface_test.gleam +++ b/test/gleam_package_interface_test.gleam @@ -1,12 +1,278 @@ +import birdie +import glam/doc.{type Document} +import gleam/dict.{type Dict} +import gleam/int +import gleam/json +import gleam/list +import gleam/option.{type Option, None, Some} +import gleam/package_interface.{ + type Constant, type Deprecation, type Function, type Implementations, + type Module, type Package, type Parameter, type Type, type TypeAlias, + type TypeConstructor, type TypeDefinition, Constant, Deprecation, Fn, Function, + Implementations, Module, Named, Package, Parameter, Tuple, TypeAlias, + TypeConstructor, TypeDefinition, Variable, +} +import gleam/string import gleeunit -import gleeunit/should +import simplifile pub fn main() { gleeunit.main() } -// gleeunit test functions end in `_test` -pub fn hello_world_test() { - 1 - |> should.equal(1) +pub fn decoding_a_module_interface_test() { + let assert Ok(raw_package) = simplifile.read("./priv/interface.json") + let assert Ok(package) = json.decode(raw_package, package_interface.decoder) + + pretty_package(package) + |> birdie.snap(title: "Decoding a module interface") +} + +// --- PRETTY PRINTING THE PACKAGE --------------------------------------------- +// We use a custom pretty printer to make reviewing the snapshots easier. + +fn pretty_package(package: Package) -> String { + package_to_doc(package) + |> doc.to_string(80) +} + +fn package_to_doc(package: Package) -> Document { + let Package( + name: name, + version: version, + gleam_version_constraint: gleam_version_constraint, + modules: modules, + ) = package + constructor("Package", [ + #("name", string(name)), + #("version", string(version)), + #("gleam_version_contraint", optional(gleam_version_constraint, string)), + #("modules", sorted_dict(modules, module_to_doc)), + ]) +} + +fn module_to_doc(module: Module) -> Document { + let Module( + documentation: documentation, + type_aliases: type_aliases, + constants: constants, + functions: functions, + types: types, + ) = module + + constructor("Module", [ + #("documentation", list(list.map(documentation, string))), + #("type_aliases", sorted_dict(type_aliases, type_alias_to_doc)), + #("constants", sorted_dict(constants, constant_to_doc)), + #("functions", sorted_dict(functions, function_to_doc)), + #("types", sorted_dict(types, type_definition_to_doc)), + ]) +} + +fn type_alias_to_doc(type_alias: TypeAlias) -> Document { + let TypeAlias( + alias: alias, + deprecation: deprecation, + documentation: documentation, + parameters: parameters, + ) = type_alias + + constructor("TypeAlias", [ + #("alias", type_to_doc(alias)), + #("deprecation", optional(deprecation, deprecation_to_doc)), + #("documentation", optional(documentation, string)), + #("parameters", int(parameters)), + ]) +} + +fn deprecation_to_doc(deprecation: Deprecation) -> Document { + let Deprecation(message: message) = deprecation + constructor("Deprecation", [#("message", string(message))]) +} + +fn constant_to_doc(constant: Constant) -> Document { + let Constant( + deprecation: deprecation, + documentation: documentation, + implementations: implementations, + type_: type_, + ) = constant + + constructor("Constant", [ + #("deprecation", optional(deprecation, deprecation_to_doc)), + #("documentation", optional(documentation, string)), + #("implementations", implementations_to_doc(implementations)), + #("type_", type_to_doc(type_)), + ]) +} + +fn function_to_doc(function: Function) -> Document { + let Function( + deprecation: deprecation, + documentation: documentation, + implementations: implementations, + parameters: parameters, + return: return, + ) = function + + constructor("Function", [ + #("deprecation", optional(deprecation, deprecation_to_doc)), + #("documentation", optional(documentation, string)), + #("implementations", implementations_to_doc(implementations)), + #("parameters", list(list.map(parameters, parameter_to_doc))), + #("return", type_to_doc(return)), + ]) +} + +fn type_definition_to_doc(type_definition: TypeDefinition) -> Document { + let TypeDefinition( + constructors: constructors, + deprecation: deprecation, + documentation: documentation, + parameters: parameters, + ) = type_definition + + constructor("TypeDefinition", [ + #("constructors", list(list.map(constructors, constructor_to_doc))), + #("deprecation", optional(deprecation, deprecation_to_doc)), + #("documentation", optional(documentation, string)), + #("parameters", int(parameters)), + ]) +} + +fn constructor_to_doc(constructor_: TypeConstructor) -> Document { + let TypeConstructor( + documentation: documentation, + name: name, + parameters: parameters, + ) = constructor_ + + constructor("TypeConstructor", [ + #("documentation", optional(documentation, string)), + #("name", string(name)), + #("parameters", list(list.map(parameters, parameter_to_doc))), + ]) +} + +fn implementations_to_doc(implementations: Implementations) -> Document { + let Implementations( + gleam: gleam, + uses_erlang_externals: uses_erlang_externals, + uses_javascript_externals: uses_javascript_externals, + ) = implementations + + constructor("Implementations", [ + #("gleam", bool(gleam)), + #("uses_erlang_externals", bool(uses_erlang_externals)), + #("uses_javascript_externals", bool(uses_javascript_externals)), + ]) +} + +fn parameter_to_doc(parameter: Parameter) -> Document { + let Parameter(label: label, type_: type_) = parameter + constructor("Parameter", [ + #("label", optional(label, string)), + #("type", type_to_doc(type_)), + ]) +} + +fn type_to_doc(type_: Type) -> Document { + case type_ { + Fn(parameters: parameters, return: return) -> + constructor("Fn", [ + #("parameters", list(list.map(parameters, type_to_doc))), + #("return", type_to_doc(return)), + ]) + + Tuple(elements: elements) -> + constructor("Tuple", [ + #("elements", list(list.map(elements, type_to_doc))), + ]) + + Variable(id: id) -> constructor("Variable", [#("id", int(id))]) + + Named(package: package, module: module, name: name, parameters: parameters) -> + constructor("Named", [ + #("package", string(package)), + #("module", string(module)), + #("name", string(name)), + #("parameters", list(list.map(parameters, type_to_doc))), + ]) + } +} + +fn sorted_dict( + dict: Dict(String, a), + with to_doc: fn(a) -> Document, +) -> Document { + dict.to_list(dict) + |> list.sort(fn(one, other) { string.compare(one.0, other.0) }) + |> list.map(fn(entry) { + let #(name, value) = entry + doc.concat([ + doc.from_string(name <> ": "), + to_doc(value) + |> doc.nest(by: 2), + ]) + }) + |> parenthesise("{", "}", _) +} + +fn constructor( + named name: String, + args args: List(#(String, Document)), +) -> Document { + let args = + list.map(args, fn(pair) { + let #(name, arg) = pair + [doc.from_string(name <> ": "), arg] + |> doc.concat + }) + + [doc.from_string(name), parenthesise("(", ")", args)] + |> doc.concat +} + +fn list(of docs: List(Document)) -> Document { + parenthesise("[", "]", docs) +} + +fn optional(value: Option(a), fun: fn(a) -> Document) -> Document { + case value { + Some(a) -> constructor("Some", [#("item", fun(a))]) + None -> constructor("None", []) + } +} + +fn bool(value: Bool) -> Document { + case value { + True -> doc.from_string("True") + False -> doc.from_string("False") + } +} + +fn int(value: Int) -> Document { + doc.from_string(int.to_string(value)) +} + +fn string(value: String) -> Document { + doc.from_string("\"" <> value <> "\"") +} + +fn parenthesise(open: String, close: String, docs: List(Document)) -> Document { + case docs { + [] -> doc.from_string(open <> close) + [_, ..] -> + [ + doc.from_string(open), + doc.nest(doc.break("", ""), by: 2), + docs + |> doc.join(with: doc.break(", ", ",")) + |> doc.nest(by: 2), + doc.break("", ","), + doc.from_string(close), + ] + |> doc.concat + |> doc.group + } } From f65d5e52a77d190f436063f2720f6a3b99b85bd6 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 29 Feb 2024 11:55:37 +0100 Subject: [PATCH 2/3] run js tests in CI --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 951b2fe..fbf4563 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: otp-version: "26.0.2" gleam-version: "0.34.1" rebar3-version: "3" - # elixir-version: "1.15.4" - run: gleam deps download - - run: gleam test + - run: gleam test --target=erlang + - run: gleam test --target=javascript - run: gleam format --check src test From 51d9f00d88356328f9d0277727f833407614ff28 Mon Sep 17 00:00:00 2001 From: Giacomo Cavalieri Date: Thu, 29 Feb 2024 11:55:49 +0100 Subject: [PATCH 3/3] README! --- README.md | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6bb808b..9207a22 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,94 @@ [![Package Version](https://img.shields.io/hexpm/v/gleam_package_interface)](https://hex.pm/packages/gleam_package_interface) [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/gleam_package_interface/) +![Supported targets](https://img.shields.io/badge/supports-all_targets-ffaff3) + +## Installation + +Add `gleam_package_interface` to your Gleam project: ```sh gleam add gleam_package_interface ``` +## What's a package interface? + +Whenever you build your project's documentation with `gleam docs build`, the +Gleam compiler will also produce a handy json file +`./build/dev/docs//package-interface.json` +containing data about your package: that's the package interface. + +It has all public information your package exposes to the outside world: type +definitions, type aliases, public functions and constants — each annotated with +its own type and with its documentation. + +> You can also have the compiler build the package interface using the +> `gleam export package-interface` command. + +Let's have a look at a small example. Imagine you have a module called `wibble` +with the following definition: + ```gleam -import gleam_package_interface +/// Documentation! +pub fn wibbler(label n: Int) -> Int { + todo +} +``` -pub fn main() { - // TODO: An example of the project in use +The resulting package interface will look something like this (some keys where +omitted to keep the example short): + +```json +{ + "package": "your package", + "modules": { + "wibble": { + "functions": { + "wibbler": { + "documentation": "Documentation!\n", + "parameters": [ + { + "label": "label", + "type": { + "kind": "named", + "package": "", + "module": "gleam", + "name": "Int" + } + } + ], + "return": { + "kind": "named", + "package": "", + "module": "gleam", + "name": "Int" + } + } + } + } + } } ``` -Further documentation can be found at . +To get a proper feel of the structure of the generated package interface you can +have a look at this +[package's types](https://hexdocs.pm/gleam_package_interface/). + +## Usage + +This package provides Gleam types to describe a package interface and a +`decoder` to decode the json package interface into a Gleam value. + +```gleam +// gleam add gleam_json +// gleam add simplifile +import gleam/json +import gleam/package_interface +import simplifile + +pub fn main() { + let assert Ok(json) = simplifile.read("path to the package interface") + let assert Ok(interface) = json.decode(json, using: package_interface.decoder) + todo as "now you can use the package interface however you want!" +} +```