Skip to content

Commit

Permalink
example refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
TanklesXL committed Jul 28, 2023
1 parent 0e08bed commit bdd4098
Show file tree
Hide file tree
Showing 11 changed files with 241 additions and 81 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
gleam: ["0.30.0-rc3"]
erlang: ["25.3.2.3", "26.0.2"]
gleam: ["0.30.3"]
steps:
- uses: actions/checkout@v2
- uses: ./.github/actions/test
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- uses: actions/checkout@v2
- uses: ./.github/actions/test
with:
gleam-version: "0.30.0-rc3"
gleam-version: "0.30.3"
- name: publish to hex
env:
HEXPM_USER: ${{ secrets.HEXPM_USER }}
Expand Down
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1 +1 @@
gleam 0.30.0-rc4
gleam 0.30.3
88 changes: 86 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,90 @@ gleam add glint

## Usage

You can import `glint` as a dependency and use it as follows:
### Glint's Core

...
`glint` is conceptually quite small, your general flow will be:

1. create a new glint instance with `glint.new`
1. configure it with `glint.with_pretty_help` and other configuration functions
1. add commands with `glint.add`
1. create a new command with `glint.cmd`
1. assign that command any flags required
1. assign the command a custom description
1. run your cli with `glnt.run`, run with a function to handle command output with `glint.run_and_handle`

### Mini Example

You can import `glint` as a dependency and use it to build simple command-line applications like the following simplified version of the [the hello world example](./examples/hello/README.md)

```gleam
// stdlib imports
import gleam/io
import gleam/list
import gleam/result
import gleam/string.{uppercase}
// external dep imports
import snag
// glint imports
import glint
import glint/flag
// erlang-specific imports
@target(erlang)
import gleam/erlang.{start_arguments}
/// the key for the caps flag
const caps = "caps"
/// a boolean flag with default False to control message capitalization.
///
fn caps_flag() -> flag.FlagBuilder(Bool) {
flag.new(flag.B)
|> flag.default(False)
|> flag.description("Capitalize the provided name")
}
/// the command function that will be executed
///
fn hello(input: glint.CommandInput) -> Nil {
let assert Ok(caps) = flag.get_bool(from: input.flags, for: caps)
let name =
case input.args {
[] -> "Joe"
[name,..] -> name
}
let msg = "Hello, " <> name <> "!"
case caps {
True -> uppercase(msg)
False -> msg
}
|> io.println
}
pub fn main() {
// create a new glint instance
glint.new()
// with an app name of "hello", this is used when printing help text
|> glint.with_name("hello")
// with pretty help enabled, using the built-in colours
|> glint.with_pretty_help(glint.default_pretty_help())
// with a root command that executes the `hello` function
|> glint.add(
// add the command to the root
at: [],
// create the command, add any flags
do: glint.command(hello)
// with flag `caps`
|> glint.flag(caps, caps_flag())
// with flag `repeat`
|> glint.flag(repeat, repeat_flag())
// with a short description
|> glint.description("Prints Hello, <NAME>!"),
)
|> glint.run(start_arguments())
}
```
2 changes: 1 addition & 1 deletion examples/hello/.github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- uses: erlef/[email protected]
with:
otp-version: "25.2"
gleam-version: "0.30.0-rc3"
gleam-version: "0.30.3"
rebar3-version: "3"
# elixir-version: "1.14.2"
- run: gleam format --check src test
Expand Down
1 change: 0 additions & 1 deletion examples/hello/.tool-versions

This file was deleted.

47 changes: 32 additions & 15 deletions examples/hello/README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,41 @@
# hello

[![Package Version](https://img.shields.io/hexpm/v/hello)](https://hex.pm/packages/hello)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/hello/)
This is an example project that demonstrates a simple workflow using `glint`.
It contains only one command, the root command.
Feel free to browse `src/hello.gleam` to get a sense of how a small cli application is written.

A Gleam project
## Usage

## Quick start
### Running the application

```sh
gleam run # Run the project
gleam test # Run the tests
gleam shell # Run an Erlang shell
```
You can run this example from the `examples/hello` directory by calling `gleam run` which prints `Hello, <NAMES>!`

## Installation
The `hello` application accepts any number of arguments, being the names of people to say hello to.

If available on Hex this package can be added to your Gleam project:
- No input: `gleam run` -> prints "Hello, Joe!"
- One input: `gleam run Rob` -> prints "Hello, Rob!"
- Two inputs: `gleam run Rob Louis` -> prints "Hello, Rob and Louis!"
- \>2 inputs: `gleam run Rob Louis Hayleigh` -> prints "Hello, Rob, Louis and Hayleigh!"

```sh
gleam add hello
```
### Flags

The root command accepts two flags:

- `--caps`: capitalizes the output, so if output would be "Hello, Joe!" it prints "HELLO, JOE!"
- `--repeat=N`: repeats the output N times separated , so with N=2 if output would be "Hello, Joe!" it prints "Hello, Joe!\nHello, Joe!"

## Help Output

and its documentation can be found at <https://hexdocs.pm/hello>.
Generated help output for the root command is as follows

```txt
Prints Hello, <NAME>!
USAGE:
hello [ ARGS ] [ --caps=<BOOL> --repeat=<INT> ]
FLAGS:
--caps=<BOOL> Capitalize the provided name
--help Print help information
--repeat=<INT> Repeat the message n-times
```
136 changes: 81 additions & 55 deletions examples/hello/src/hello.gleam
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
// stdlib imports
import gleam/io
import gleam/list
import gleam/result
import gleam/string.{join, uppercase}
import gleam/function.{compose}
import gleam/string.{uppercase}
// external dep imports
import snag
// glint imports
Expand All @@ -13,24 +11,66 @@ import glint/flag
@target(erlang)
import gleam/erlang.{start_arguments}

// ----- APPLICATION LOGIC -----

/// a helper function to join a list of names
fn join_names(names: List(String)) -> String {
case names {
[] -> "Joe"
[name] -> name
[name, ..rest] -> do_join_names(rest, name)
}
}

// tail-recursive implementation of join_naemes
fn do_join_names(names: List(String), acc: String) {
case names {
[] -> acc
[a] -> acc <> " and " <> a
[a, ..b] -> do_join_names(b, acc <> ", " <> a)
}
}

pub fn message(names: List(String)) {
"Hello, " <> join_names(names) <> "!"
}

pub fn capitalize(msg, caps) -> String {
case caps {
True -> uppercase(msg)
False -> msg
}
}

/// hello is a function that
pub fn hello(names: List(String), caps: Bool, repeat: Int) -> String {
names
|> message
|> capitalize(caps)
|> list.repeat(repeat)
|> string.join("\n")
}

// ----- CLI SETUP -----

/// the key for the caps flag
const caps = "caps"
pub const caps = "caps"

/// a boolean flag with default False to control message capitalization.
///
fn caps_flag() -> flag.FlagBuilder(Bool) {
pub fn caps_flag() -> flag.FlagBuilder(Bool) {
flag.new(flag.B)
|> flag.default(False)
|> flag.description("Capitalize the provided name")
}

/// the key for the repeat flag
const repeat = "repeat"
pub const repeat = "repeat"

/// an int flag with default 1 to control how many times to repeat the message.
/// this flag has the `gtz` constraint applied to it.
///
fn repeat_flag() -> flag.FlagBuilder(Int) {
pub fn repeat_flag() -> flag.FlagBuilder(Int) {
flag.new(flag.I)
|> flag.default(1)
|> flag.constraint(gtz)
Expand All @@ -46,58 +86,44 @@ fn gtz(n: Int) -> snag.Result(Nil) {
}
}

/// the command function that will be executed
/// the command function that will be executed as the root command
///
fn hello(input: glint.CommandInput) -> snag.Result(String) {
let assert Ok(caps) = flag.get_bool(from: input.flags, for: caps)
let assert Ok(repeat) = flag.get_int(from: input.flags, for: repeat)

use name <- result.map(case input.args {
[] -> snag.error("no arguments provided")
_ -> Ok(input.args)
})

["Hello,", ..name]
|> case caps {
True -> compose(join(_, " "), uppercase)
False -> join(_, " ")
pub fn hello_cmd() -> glint.Command(String) {
{
use input <- glint.command()
// the caps flag has a default value, so we can be sure it will always be present
let assert Ok(caps) = flag.get_bool(from: input.flags, for: caps)
// the repeat flag has a default value, so we can be sure it will always be present
let assert Ok(repeat) = flag.get_int(from: input.flags, for: repeat)
// call the hello function with all necessary inputs
hello(input.args, caps, repeat)
}
|> string.append("!")
|> list.repeat(repeat)
|> string.join("\n")
// with flag `caps`
|> glint.flag(caps, caps_flag())
// with flag `repeat`
|> glint.flag(repeat, repeat_flag())
// with flag `repeat`
|> glint.description("Prints Hello, <NAMES>!")
}

@target(erlang)
pub fn main() {
use res <- glint.run_and_handle(
// create a new glint instance
glint.new()
// with an app name of "hello", this is used when printing help text
|> glint.with_name("hello")
// with pretty help enabled, using the built-in colours
|> glint.with_pretty_help(glint.default_pretty_help())
// with a root command that executes the `hello` function
|> glint.add(
// add the command to the root
at: [],
// create the command, add any flags
do: glint.command(hello)
// with flag `caps`
|> glint.flag(caps, caps_flag())
// with flag `repeat`
|> glint.flag(repeat, repeat_flag())
|> glint.description("Prints Hello, <NAME>!"),
),
start_arguments(),
// the function that describes our cli structure
pub fn app() {
// create a new glint instance
glint.new()
// with an app name of "hello", this is used when printing help text
|> glint.with_name("hello")
// with pretty help enabled, using the built-in colours
|> glint.with_pretty_help(glint.default_pretty_help())
// with a root command that executes the `hello` function
|> glint.add(
// add the hello command to the root
at: [],
do: hello_cmd(),
)
}

// run with a handler that converts the command output to a string and prints it
case res {
Ok(out) -> out
Error(err) ->
err
|> snag.layer("failed to execute command")
|> snag.pretty_print()
}
|> io.println
@target(erlang)
pub fn main() {
// run with a handler that prints the command output
glint.run_and_handle(app(), start_arguments(), io.println)
}
35 changes: 33 additions & 2 deletions examples/hello/test/hello_test.gleam
Original file line number Diff line number Diff line change
@@ -1,7 +1,38 @@
import gleeunit

// import gleeunit/should
import gleeunit/should
import hello
import gleam/list

pub fn main() {
gleeunit.main()
}

type TestCase {
TestCase(input: List(String), caps: Bool, repeat: Int, expected: String)
}

pub fn hello_test() {
use tc <- list.each([
TestCase([], False, 1, "Hello, Joe!"),
TestCase(["Rob"], False, 1, "Hello, Rob!"),
TestCase([], True, 1, "HELLO, JOE!"),
TestCase([], True, 2, "HELLO, JOE!\nHELLO, JOE!"),
TestCase(["Rob"], True, 1, "HELLO, ROB!"),
TestCase(["Tony", "Maria"], True, 1, "HELLO, TONY AND MARIA!"),
TestCase(
["Tony", "Maria", "Nadia"],
True,
1,
"HELLO, TONY, MARIA AND NADIA!",
),
TestCase(["Tony", "Maria"], False, 1, "Hello, Tony and Maria!"),
TestCase(
["Tony", "Maria", "Nadia"],
False,
1,
"Hello, Tony, Maria and Nadia!",
),
])
hello.hello(tc.input, tc.caps, tc.repeat)
|> should.equal(tc.expected)
}
Loading

0 comments on commit bdd4098

Please sign in to comment.