Skip to content

Commit

Permalink
✨ Config
Browse files Browse the repository at this point in the history
  • Loading branch information
MystPi committed Mar 5, 2024
1 parent 79a74e9 commit 86861e7
Show file tree
Hide file tree
Showing 10 changed files with 198 additions and 99 deletions.
5 changes: 5 additions & 0 deletions birdie_snapshots/(erlang)_labels_are_not_shown.accepted
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
version: 1.0.4
title: (erlang) labels are not shown
---
Foo(42, "bar", "baz")
5 changes: 5 additions & 0 deletions birdie_snapshots/(javascript)_labels_are_shown.accepted
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
version: 1.0.4
title: (javascript) labels are shown
---
Foo(42, bar: "bar", baz: "baz")

This file was deleted.

13 changes: 0 additions & 13 deletions birdie_snapshots/data_is_colored_depending_on_its_type.accepted

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
version: 1.0.4
title: (erlang) data is colored depending on its type
title: data is styled depending on its type
---
#(
Ok(1234),
Expand All @@ -10,4 +10,5 @@ title: (erlang) data is colored depending on its type
//fn(a) { ... },
3.14,
"A",
Foo(1, "2", "3"),
)
2 changes: 1 addition & 1 deletion src/ffi.erl
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ decode_custom_type(X) ->
case tuple_to_list(Tuple) of
[Atom | Elements] when is_atom(Atom) ->
case inspect_maybe_gleam_atom(erlang:atom_to_binary(Atom), none, <<>>) of
{ok, AtomName} -> {ok, {t_custom, AtomName, Elements}};
{ok, AtomName} -> {ok, {t_custom, AtomName, lists:map(fun(E) -> {positional, E} end, Elements)}};
{error, nil} -> decode_error("CustomType", X)
end;
_ -> decode_error("CustomType", X)
Expand Down
10 changes: 6 additions & 4 deletions src/ffi.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ function decoder_error_no_classify(expected, got) {
export function decode_custom_type(value) {
if (value instanceof $gleam.CustomType) {
const name = value.constructor.name;
const fields = Object.values(value);
const fields = Object.keys(value).map((label) => {
return isNaN(parseInt(label))
? new $decoder.Labelled(label, value[label])
: new $decoder.Positional(value[label]);
});

return new $gleam.Ok(
new $decoder.TCustom(name, $gleam.toList(fields))
);
return new $gleam.Ok(new $decoder.TCustom(name, $gleam.toList(fields)));
}

return decoder_error('CustomType', value);
Expand Down
190 changes: 145 additions & 45 deletions src/pprint.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,68 @@ import gleam/float
import gleam/dict.{type Dict}
import gleam/string
import gleam/dynamic.{type Dynamic}
import gleam/bit_array
import glam/doc.{type Document}
import pprint/decoder

// --- PUBLIC API --------------------------------------------------------------

/// Configuration for the pretty printer.
///
pub type Config {
Config(
style_mode: StyleMode,
bit_array_mode: BitArrayMode,
labels_mode: LabelsMode,
)
}

/// Styling can be configured with `StyleMode`.
///
pub type StyleMode {
/// Data structures are styled with ANSI style codes.
Styled
/// Everything remains unstyled.
Unstyled
}

/// Since Erlang handles BitArrays differently than JavaScript does, the
/// `BitArraysAsString` config option enables compatibility between the two targets.
///
/// These options only affect the JS target, which does not convert bit arrays to
/// strings by default like Erlang does.
///
pub type BitArrayMode {
/// Bit arrays will be converted to strings when pretty printed.
BitArraysAsString
/// Bit arrays will be kept the same.
KeepBitArrays
}

/// This option only affects the JavaScript target since Erlang has a different
/// runtime representation of custom types that omits labels.
///
pub type LabelsMode {
/// Show field labels in custom types.
/// ```
/// Foo(42, bar: "bar", baz: "baz")
/// ```
Labels
/// Leave out field labels.
/// ```
/// Foo(42, "bar", "baz")
/// ```
NoLabels
}

const max_width = 40

/// Pretty print a value with coloring to stderr for debugging purposes. The value
/// is returned back from the function so it can be used in pipelines.
/// Pretty print a value with the config below to stderr for debugging purposes.
/// The value is returned back from the function so it can be used in pipelines.
///
/// ```
/// Config(Styled, KeepBitArrays, Labels)
/// ```
///
/// # Examples
///
Expand All @@ -31,71 +84,104 @@ const max_width = 40
///
pub fn debug(value: a) -> a {
value
|> format_colored
|> with_config(Config(Styled, KeepBitArrays, Labels))
|> io.println_error

value
}

/// Prettify a value into a string, without coloring. This is useful for snapshot
/// testing with packages such as `birdie`.
/// Pretty print a value as a string with the following config:
/// ```
/// Config(Unstyled, BitArraysAsString, NoLabels)
/// ```
/// This function behaves identically on both targets so it can be relied upon
/// for snapshot testing.
///
pub fn format(value: a) -> String {
value
|> dynamic.from
|> pretty_dynamic(False)
|> doc.to_string(max_width)
with_config(value, Config(Unstyled, BitArraysAsString, NoLabels))
}

/// Pretty print a value as a string with the following config:
/// ```
/// Config(Styled, BitArraysAsString, NoLabels)
/// ```
/// This function behaves identically on both targets so it can be relied upon
/// for snapshot testing.
///
pub fn styled(value: a) -> String {
with_config(value, Config(Styled, BitArraysAsString, NoLabels))
}

/// Prettify a value into a string with ANSI coloring.
/// Pretty print a value as a string with a custom config.
///
pub fn format_colored(value: a) -> String {
/// # Examples
///
/// ```
/// [1, 2, 3, 4]
/// |> pprint.with_config(Config(Color, KeepBitArrays, Labels))
/// ```
///
pub fn with_config(value: a, config: Config) -> String {
value
|> dynamic.from
|> pretty_dynamic(True)
|> pretty_dynamic(config)
|> doc.to_string(max_width)
}

// ---- PRETTY PRINTING --------------------------------------------------------

fn pretty_type(value: decoder.Type, color: Bool) -> Document {
fn pretty_type(value: decoder.Type, config: Config) -> Document {
case value {
decoder.TString(s) ->
{ "\"" <> s <> "\"" }
|> ansi(green, color)
decoder.TString(s) -> pretty_string(s, config)

decoder.TInt(i) ->
int.to_string(i)
|> ansi(yellow, color)
|> ansi(yellow, config)

decoder.TFloat(f) ->
float.to_string(f)
|> ansi(yellow, color)
|> ansi(yellow, config)

decoder.TBool(b) ->
bool.to_string(b)
|> ansi(blue, color)
|> ansi(blue, config)

decoder.TBitArray(b) ->
string.inspect(b)
|> ansi(magenta, color)

decoder.TNil -> ansi("Nil", blue, color)
decoder.TList(items) -> pretty_list(items, color)
decoder.TDict(d) -> pretty_dict(d, color)
decoder.TTuple(items) -> pretty_tuple(items, color)
decoder.TCustom(name, fields) -> pretty_custom_type(name, fields, color)
decoder.TForeign(f) -> ansi(f, dim, color)
case config.bit_array_mode {
KeepBitArrays -> pretty_bit_array(b, config)
BitArraysAsString ->
case bit_array.to_string(b) {
Ok(s) -> pretty_string(s, config)
Error(Nil) -> pretty_bit_array(b, config)
}
}

decoder.TNil -> ansi("Nil", blue, config)
decoder.TList(items) -> pretty_list(items, config)
decoder.TDict(d) -> pretty_dict(d, config)
decoder.TTuple(items) -> pretty_tuple(items, config)
decoder.TCustom(name, fields) -> pretty_custom_type(name, fields, config)
decoder.TForeign(f) -> ansi(f, dim, config)
}
}

fn pretty_dynamic(value: Dynamic, color: Bool) -> Document {
fn pretty_dynamic(value: Dynamic, config: Config) -> Document {
value
|> decoder.classify
|> pretty_type(color)
|> pretty_type(config)
}

fn pretty_string(string: String, config: Config) -> Document {
{ "\"" <> string <> "\"" }
|> ansi(green, config)
}

fn pretty_bit_array(bits: BitArray, config: Config) -> Document {
string.inspect(bits)
|> ansi(magenta, config)
}

fn pretty_list(items: List(Dynamic), color: Bool) -> Document {
fn pretty_list(items: List(Dynamic), config: Config) -> Document {
let items = list.map(items, decoder.classify)

// When the list consists only of numbers, the values are joined with flex spaces
Expand All @@ -105,12 +191,12 @@ fn pretty_list(items: List(Dynamic), color: Bool) -> Document {
_ -> doc.space
}

list.map(items, pretty_type(_, color))
list.map(items, pretty_type(_, config))
|> doc.concat_join([doc.from_string(","), space])
|> wrap(doc.from_string("["), doc.from_string("]"), trailing: ",")
}

fn pretty_dict(d: Dict(decoder.Type, decoder.Type), color: Bool) -> Document {
fn pretty_dict(d: Dict(decoder.Type, decoder.Type), config: Config) -> Document {
dict.to_list(d)
|> list.sort(fn(one_field, other_field) {
// We need to sort dicts so that those always have a consistent order.
Expand All @@ -121,9 +207,9 @@ fn pretty_dict(d: Dict(decoder.Type, decoder.Type), color: Bool) -> Document {
|> list.map(fn(field) {
// Format the dict's items into tuple literals
[
pretty_type(field.0, color),
pretty_type(field.0, config),
doc.from_string(", "),
pretty_type(field.1, color),
pretty_type(field.1, config),
]
|> doc.concat
|> doc.prepend(doc.from_string("#("))
Expand All @@ -137,25 +223,39 @@ fn pretty_dict(d: Dict(decoder.Type, decoder.Type), color: Bool) -> Document {
)
}

fn pretty_tuple(items: List(Dynamic), color: Bool) -> Document {
list.map(items, pretty_dynamic(_, color))
fn pretty_tuple(items: List(Dynamic), config: Config) -> Document {
list.map(items, pretty_dynamic(_, config))
|> doc.concat_join([doc.from_string(","), doc.space])
|> wrap(doc.from_string("#("), doc.from_string(")"), trailing: ",")
}

fn pretty_custom_type(
name: String,
fields: List(Dynamic),
color: Bool,
fields: List(decoder.Field),
config: Config,
) -> Document {
// Common built-in constructor names are styled
let style = case name {
"Ok" | "Error" | "Some" | "None" -> bold
_ -> ""
}

let fields = list.map(fields, pretty_dynamic(_, color))
let open = doc.concat([ansi(name, style, color), doc.from_string("(")])
let fields =
list.map(fields, fn(field) {
case field, config.labels_mode {
decoder.Positional(value), Labels
| decoder.Positional(value), NoLabels
| decoder.Labelled(_, value), NoLabels -> pretty_dynamic(value, config)

decoder.Labelled(label, value), Labels ->
doc.concat([
ansi(label <> ": ", dim, config),
pretty_dynamic(value, config),
])
}
})

let open = doc.concat([ansi(name, style, config), doc.from_string("(")])
let close = doc.from_string(")")

case fields {
Expand Down Expand Up @@ -206,12 +306,12 @@ const bold = "\u{001b}[1m"

const dim = "\u{001b}[2m"

fn ansi(text: String, code: String, enabled: Bool) -> Document {
fn ansi(text: String, code: String, config: Config) -> Document {
let text_doc = doc.from_string(text)

case enabled {
False -> text_doc
True ->
case config.style_mode {
Unstyled -> text_doc
Styled ->
doc.concat([
doc.zero_width_string(code),
text_doc,
Expand Down
7 changes: 6 additions & 1 deletion src/pprint/decoder.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@ pub type Type {
TList(List(Dynamic))
TDict(Dict(Type, Type))
TTuple(List(Dynamic))
TCustom(name: String, fields: List(Dynamic))
TCustom(name: String, fields: List(Field))
TForeign(String)
}

pub type Field {
Labelled(label: String, value: Dynamic)
Positional(value: Dynamic)
}

// ---- DECODERS ---------------------------------------------------------------

pub fn classify(value: Dynamic) -> Type {
Expand Down
Loading

0 comments on commit 86861e7

Please sign in to comment.