Skip to content

Commit f08ef0e

Browse files
authored
Merge pull request #1 from ralaruri/gleam-cli/rewriting-into-rs-lookup
ADHOC: Updating Gleam CLI to use Runescape API
2 parents 209b5b4 + 0458260 commit f08ef0e

File tree

10 files changed

+379
-62
lines changed

10 files changed

+379
-62
lines changed

README.md

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,48 @@
1-
# vars
1+
# ge
22

3-
[![Package Version](https://img.shields.io/hexpm/v/vars)](https://hex.pm/packages/vars)
4-
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/vars/)
3+
A command-line tool for checking Old School RuneScape Grand Exchange prices in real time. It pulls live buy/sell prices from the OSRS Wiki API so you can look up any item by name or search for items matching a keyword. Built with Gleam and runs on the Erlang VM.
4+
5+
## Installation
6+
7+
Make sure you have [Gleam](https://gleam.run) (v1.0+) and Erlang/OTP installed, then clone this repo and build:
58

69
```sh
7-
gleam add vars
10+
git clone https://github.com/ralaruri/gleam-cli.git
11+
cd gleam-cli
12+
gleam build
813
```
9-
```gleam
10-
import vars
1114

12-
pub fn main() {
13-
// TODO: An example of the project in use
14-
}
15+
## Usage
16+
17+
```sh
18+
# Look up an item's latest GE price
19+
gleam run -- price "abyssal whip"
20+
21+
# Search for items matching a query
22+
gleam run -- search dragon
1523
```
1624

17-
Further documentation can be found at <https://hexdocs.pm/vars>.
25+
## Example Output
26+
27+
```
28+
$ ge price "abyssal whip"
29+
Abyssal whip (ID: 4151)
30+
Buy price: 1,350,000 gp
31+
Sell price: 1,340,000 gp
32+
GE limit: 70
33+
Members: Yes
34+
35+
$ ge search dragon
36+
Found 191 items matching "dragon":
37+
Dragon scimitar (ID: 4587)
38+
Dragon longsword (ID: 1305)
39+
...
40+
```
1841

1942
## Development
2043

2144
```sh
22-
gleam run # Run the project
23-
gleam test # Run the tests
24-
gleam shell # Run an Erlang shell
45+
gleam build # Build the project
46+
gleam run # Run the project
47+
gleam test # Run the tests
2548
```

gleam.toml

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,16 @@
1-
name = "vars"
1+
name = "ge"
22
version = "1.0.0"
3-
4-
# Fill out these fields if you intend to generate HTML documentation or publish
5-
# your project to the Hex package manager.
6-
#
7-
# description = ""
8-
# licences = ["Apache-2.0"]
9-
# repository = { type = "github", user = "username", repo = "project" }
10-
# links = [{ title = "Website", href = "https://gleam.run" }]
11-
#
12-
# For a full reference of all the available options, you can have a look at
13-
# https://gleam.run/writing-gleam/gleam-toml/.
3+
description = "OSRS Grand Exchange Price Checker CLI"
144

155
[dependencies]
166
gleam_stdlib = "~> 0.34 or ~> 1.0"
17-
envoy = "~> 1.0"
7+
gleam_httpc = "~> 3.0"
8+
gleam_json = "~> 2.0"
189
argv = "~> 1.0"
10+
gleam_http = "~> 3.7"
1911

2012
[dev-dependencies]
2113
gleeunit = "~> 1.0"
14+
15+
[erlang]
16+
extra_applications = ["inets", "ssl"]

manifest.toml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,18 @@
33

44
packages = [
55
{ name = "argv", version = "1.0.1", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "A6E9009E50BBE863EB37D963E4315398D41A3D87D0075480FC244125808F964A" },
6-
{ name = "envoy", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "CFAACCCFC47654F7E8B75E614746ED924C65BD08B1DE21101548AC314A8B6A41" },
6+
{ name = "gleam_erlang", version = "0.33.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "A1D26B80F01901B59AABEE3475DD4C18D27D58FA5C897D922FCB9B099749C064" },
7+
{ name = "gleam_http", version = "3.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "A9EE0722106FCCAB8AD3BF9D0A3EFF92BFE8561D59B83BAE96EB0BE1938D4E0F" },
8+
{ name = "gleam_httpc", version = "3.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "091CDD2BEC8092E82707BEA03FB5205A2BBBDE4A2F551E3C069E13B8BC0C428E" },
9+
{ name = "gleam_json", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "CB10B0E7BF44282FB25162F1A24C1A025F6B93E777CCF238C4017E4EEF2CDE97" },
710
{ name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" },
811
{ name = "gleeunit", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D364C87AFEB26BDB4FB8A5ABDE67D635DC9FA52D6AB68416044C35B096C6882D" },
912
]
1013

1114
[requirements]
12-
argv = { version = "~> 1.0"}
13-
envoy = { version = "~> 1.0"}
15+
argv = { version = "~> 1.0" }
16+
gleam_http = { version = "~> 3.7"}
17+
gleam_httpc = { version = "~> 3.0" }
18+
gleam_json = { version = "~> 2.0" }
1419
gleam_stdlib = { version = "~> 0.34 or ~> 1.0" }
1520
gleeunit = { version = "~> 1.0" }

src/ge.gleam

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import gleam/io
2+
import gleam/string
3+
import gleam/list
4+
import argv
5+
import ge/api
6+
import ge/display
7+
import ge/types
8+
9+
pub fn main() {
10+
case argv.load().arguments {
11+
["price", ..name_parts] -> {
12+
let name = string.join(name_parts, " ")
13+
price_command(name)
14+
}
15+
["search", ..query_parts] -> {
16+
let query = string.join(query_parts, " ")
17+
search_command(query)
18+
}
19+
_ -> io.println("Usage:\n ge price <item_name>\n ge search <query>")
20+
}
21+
}
22+
23+
fn price_command(name: String) -> Nil {
24+
case api.fetch_mapping() {
25+
Ok(items) -> {
26+
let lower_name = string.lowercase(name)
27+
let matches =
28+
list.filter(items, fn(item: types.Item) {
29+
string.lowercase(item.name) == lower_name
30+
})
31+
case matches {
32+
[item, ..] -> {
33+
case api.fetch_latest(item.id) {
34+
Ok(price) -> io.println(display.format_item_price(item, price))
35+
Error(err) -> io.println("Error: " <> err)
36+
}
37+
}
38+
[] -> io.println("No item found with name \"" <> name <> "\"")
39+
}
40+
}
41+
Error(err) -> io.println("Error: " <> err)
42+
}
43+
}
44+
45+
fn search_command(query: String) -> Nil {
46+
case api.fetch_mapping() {
47+
Ok(items) -> {
48+
let lower_query = string.lowercase(query)
49+
let matches =
50+
list.filter(items, fn(item: types.Item) {
51+
string.contains(string.lowercase(item.name), lower_query)
52+
})
53+
case matches {
54+
[] -> io.println("No items found matching \"" <> query <> "\"")
55+
_ -> io.println(display.format_search_results(matches, query))
56+
}
57+
}
58+
Error(err) -> io.println("Error: " <> err)
59+
}
60+
}

src/ge/api.gleam

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import gleam/httpc
2+
import gleam/http/request
3+
import gleam/json
4+
import gleam/dynamic.{type Dynamic}
5+
import gleam/dict
6+
import gleam/int
7+
import ge/types.{type Item, type Price, Item, Price}
8+
9+
const base_url = "https://prices.runescape.wiki/api/v1/osrs"
10+
11+
const user_agent = "ge-gleam-cli"
12+
13+
pub fn fetch_mapping() -> Result(List(Item), String) {
14+
let assert Ok(req) = request.to(base_url <> "/mapping")
15+
let req = request.set_header(req, "user-agent", user_agent)
16+
17+
case httpc.send(req) {
18+
Ok(resp) -> {
19+
case json.decode(from: resp.body, using: dynamic.list(item_decoder())) {
20+
Ok(items) -> Ok(items)
21+
Error(_) -> Error("Failed to decode mapping response")
22+
}
23+
}
24+
Error(_) -> Error("Failed to fetch mapping")
25+
}
26+
}
27+
28+
pub fn fetch_latest(item_id: Int) -> Result(Price, String) {
29+
let url = base_url <> "/latest?id=" <> int.to_string(item_id)
30+
let assert Ok(req) = request.to(url)
31+
let req = request.set_header(req, "user-agent", user_agent)
32+
33+
case httpc.send(req) {
34+
Ok(resp) -> {
35+
let decoder =
36+
dynamic.field("data", dynamic.dict(dynamic.string, price_decoder()))
37+
case json.decode(from: resp.body, using: decoder) {
38+
Ok(data) -> {
39+
case dict.get(data, int.to_string(item_id)) {
40+
Ok(price) -> Ok(price)
41+
Error(_) -> Error("Item not found in response")
42+
}
43+
}
44+
Error(_) -> Error("Failed to decode price response")
45+
}
46+
}
47+
Error(_) -> Error("Failed to fetch latest prices")
48+
}
49+
}
50+
51+
fn item_decoder() -> fn(Dynamic) -> Result(Item, List(dynamic.DecodeError)) {
52+
dynamic.decode5(
53+
Item,
54+
dynamic.field("id", dynamic.int),
55+
dynamic.field("name", dynamic.string),
56+
dynamic.field("members", dynamic.bool),
57+
dynamic.optional_field("highalch", dynamic.int),
58+
dynamic.optional_field("limit", dynamic.int),
59+
)
60+
}
61+
62+
fn price_decoder() -> fn(Dynamic) -> Result(Price, List(dynamic.DecodeError)) {
63+
dynamic.decode4(
64+
Price,
65+
dynamic.optional_field("high", dynamic.int),
66+
dynamic.optional_field("highTime", dynamic.int),
67+
dynamic.optional_field("low", dynamic.int),
68+
dynamic.optional_field("lowTime", dynamic.int),
69+
)
70+
}

src/ge/display.gleam

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import gleam/int
2+
import gleam/string
3+
import gleam/list
4+
import gleam/option.{None, Some}
5+
import ge/types.{type Item, type Price}
6+
7+
pub fn format_item_price(item: Item, price: Price) -> String {
8+
let members_str = case item.members {
9+
True -> "Yes"
10+
False -> "No"
11+
}
12+
let limit_str = case item.limit {
13+
Some(l) -> format_number(l)
14+
None -> "N/A"
15+
}
16+
let buy_str = case price.high {
17+
Some(h) -> format_number(h) <> " gp"
18+
None -> "N/A"
19+
}
20+
let sell_str = case price.low {
21+
Some(l) -> format_number(l) <> " gp"
22+
None -> "N/A"
23+
}
24+
25+
item.name
26+
<> " (ID: "
27+
<> int.to_string(item.id)
28+
<> ")\n"
29+
<> " Buy price: "
30+
<> buy_str
31+
<> "\n"
32+
<> " Sell price: "
33+
<> sell_str
34+
<> "\n"
35+
<> " GE limit: "
36+
<> limit_str
37+
<> "\n"
38+
<> " Members: "
39+
<> members_str
40+
}
41+
42+
pub fn format_search_results(items: List(Item), query: String) -> String {
43+
let count = list.length(items)
44+
let header =
45+
"Found " <> int.to_string(count) <> " items matching \"" <> query <> "\":\n"
46+
let entries =
47+
list.map(items, fn(item) {
48+
" " <> item.name <> " (ID: " <> int.to_string(item.id) <> ")"
49+
})
50+
header <> string.join(entries, "\n")
51+
}
52+
53+
pub fn format_number(n: Int) -> String {
54+
case n < 0 {
55+
True -> "-" <> format_number(-n)
56+
False -> {
57+
let digits = int.to_string(n)
58+
let len = string.length(digits)
59+
case len <= 3 {
60+
True -> digits
61+
False -> insert_commas(digits, len)
62+
}
63+
}
64+
}
65+
}
66+
67+
fn insert_commas(digits: String, len: Int) -> String {
68+
let first_group = len % 3
69+
let chars = string.to_graphemes(digits)
70+
case first_group {
71+
0 -> group_digits(chars, [])
72+
_ -> {
73+
let #(first, rest) = list.split(chars, first_group)
74+
let first_str = string.concat(first)
75+
case rest {
76+
[] -> first_str
77+
_ -> first_str <> "," <> group_digits(rest, [])
78+
}
79+
}
80+
}
81+
}
82+
83+
fn group_digits(chars: List(String), acc: List(String)) -> String {
84+
case chars {
85+
[] -> string.join(list.reverse(acc), ",")
86+
_ -> {
87+
let #(group, rest) = list.split(chars, 3)
88+
let group_str = string.concat(group)
89+
group_digits(rest, [group_str, ..acc])
90+
}
91+
}
92+
}

src/ge/types.gleam

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import gleam/option.{type Option}
2+
3+
pub type Item {
4+
Item(
5+
id: Int,
6+
name: String,
7+
members: Bool,
8+
highalch: Option(Int),
9+
limit: Option(Int),
10+
)
11+
}
12+
13+
pub type Price {
14+
Price(
15+
high: Option(Int),
16+
high_time: Option(Int),
17+
low: Option(Int),
18+
low_time: Option(Int),
19+
)
20+
}

src/vars.gleam

Lines changed: 0 additions & 21 deletions
This file was deleted.

0 commit comments

Comments
 (0)