@ -1,4 +1,4 @@
|
||||
# Changelog
|
||||
|
||||
## v0.0.1 - 2022-07-04
|
||||
## v0.0.1 - 2022-09-05
|
||||
* First public release
|
||||
|
@ -0,0 +1,269 @@
|
||||
# Boto Example
|
||||
|
||||
```elixir
|
||||
Mix.install([:boto, :req])
|
||||
```
|
||||
|
||||
## Exemple Implementation
|
||||
|
||||
This example include two resolvers.
|
||||
The first one, `Weather`, aims to resolve the current temperature for a given ip.
|
||||
The second one is a `HackerNews` API implementation.
|
||||
|
||||
## Weather
|
||||
|
||||
```elixir
|
||||
defmodule Weather do
|
||||
@behaviour :boto_resolver
|
||||
|
||||
@impl :boto_resolver
|
||||
def resolver_init do
|
||||
[
|
||||
local_ip: [input: [], output: [:net@ip]],
|
||||
ip_location: [input: [:net@ip], output: [:location@lat, :location@long]],
|
||||
location_temp: [input: [:location@lat, :location@long], output: [:location@temp]]
|
||||
]
|
||||
end
|
||||
|
||||
@impl :boto_resolver
|
||||
def resolve(:local_ip, _not_required) do
|
||||
resp = Req.get!("https://api.myip.com").body |> Jason.decode!()
|
||||
|
||||
%{net@ip: resp["ip"]}
|
||||
end
|
||||
|
||||
def resolve(:ip_location, %{net@ip: ip}) do
|
||||
resp = Req.get!("https://get.geojs.io/v1/ip/geo/#{ip}.json").body
|
||||
|
||||
%{location@lat: resp["latitude"], location@long: resp["longitude"]}
|
||||
end
|
||||
|
||||
def resolve(:location_temp, %{location@lat: lat, location@long: long}) do
|
||||
resp =
|
||||
Req.get!(
|
||||
"https://api.open-meteo.com/v1/forecast?latitude=#{lat}&longitude=#{long}¤t_weather=true",
|
||||
compressed: false
|
||||
).body
|
||||
|
||||
temp = resp["current_weather"]["temperature"]
|
||||
%{location@temp: "#{temp}°C"}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
So understanding this `Weather` example.
|
||||
|
||||
We're using the `open.meteo.com` api to get the current forecast for a given `latitude` and `longitude`. This is done with the `:location_temp` resolver.
|
||||
|
||||
Since the idea is to get the forecast for an ip, we're using `geojs.io` to lookup for the location data for a given IP. This is done with the `:ip_location` resolver.
|
||||
|
||||
And in case no IP is provided, we use `myip.com` to get the current public ip for the machine running the code. This is done with `:local_ip` resolver.
|
||||
|
||||
So `:local_ip` doesn't require any attribute, and outputs a `:net@ip` attribute.
|
||||
`:ip_location` requires a `:net@ip` to output `:location@lat` and `:location@long` attributes.
|
||||
At last, `:location_temp`, requires `:location@lat` and `:location@long` to provide a `:location@temp`.
|
||||
|
||||
```mermaid
|
||||
graph TD;
|
||||
net+ip-->location+lat;
|
||||
net+ip-->location+long;
|
||||
location+lat-->location+temp;
|
||||
location+long-->location+temp;
|
||||
```
|
||||
|
||||
## HackerNews
|
||||
|
||||
```elixir
|
||||
defmodule HackerNews do
|
||||
@behaviour :boto_resolver
|
||||
|
||||
@impl :boto_resolver
|
||||
def resolver_init do
|
||||
[
|
||||
item: [
|
||||
input: [:hnitem@id],
|
||||
output: [
|
||||
:hnitem@deleted,
|
||||
:hnitem@type,
|
||||
:hnuser@id,
|
||||
:hnitem@time,
|
||||
:hnitem@text,
|
||||
:hnitem@dead,
|
||||
{:hnitem@parent, [:hnitem@id]},
|
||||
{:hnitem@poll, [:hnitem@id]},
|
||||
{:hnitem@kids, [:hnitem@id]},
|
||||
:hnitem@url,
|
||||
:hnitem@score,
|
||||
:hnitem@title,
|
||||
{:hnitem@parts, [:hnitem@id]},
|
||||
:hnitem@descendants
|
||||
]
|
||||
],
|
||||
user: [
|
||||
input: [:hnuser@id],
|
||||
output: [
|
||||
:hnuser@created,
|
||||
:hnuser@karma,
|
||||
:hnuser@about,
|
||||
{:hnuser@submitted, [:hnitem@id]}
|
||||
]
|
||||
],
|
||||
top: [input: [:hntop@size], output: [{:hntop@items, [:hnitem@id]}]]
|
||||
]
|
||||
end
|
||||
|
||||
@impl :boto_resolver
|
||||
def resolve(:item, %{hnitem@id: id}) do
|
||||
"https://hacker-news.firebaseio.com/v0/item/#{id}.json"
|
||||
|> Req.get!()
|
||||
|> Map.get(:body)
|
||||
|> Enum.map(&map_item/1)
|
||||
|> Enum.into(%{})
|
||||
end
|
||||
|
||||
def resolve(:user, %{hnuser@id: id}) do
|
||||
user =
|
||||
"https://hacker-news.firebaseio.com/v0/user/#{id}.json"
|
||||
|> Req.get!()
|
||||
|> Map.get(:body)
|
||||
|
||||
submitted = Enum.map(user["submitted"], &wrap_item_id/1)
|
||||
|
||||
%{}
|
||||
|> Map.put(:hnuser@created, DateTime.from_unix!(user["created"]))
|
||||
|> Map.put(:hnuser@karma, user["karma"])
|
||||
|> Map.put(:hnuser@about, user["about"])
|
||||
|> Map.put(:hnuser@submitted, submitted)
|
||||
end
|
||||
|
||||
def resolve(:top, %{hntop@size: size}) do
|
||||
top =
|
||||
"https://hacker-news.firebaseio.com/v0/topstories.json"
|
||||
|> Req.get!()
|
||||
|> Map.get(:body)
|
||||
|> Enum.take(size)
|
||||
|> Enum.map(&wrap_item_id/1)
|
||||
|
||||
%{hntop@items: top}
|
||||
end
|
||||
|
||||
defp wrap_item_id(id), do: %{hnitem@id: id}
|
||||
|
||||
@mapping %{
|
||||
"id" => :hnitem@id,
|
||||
"deleted" => :hnitem@deleted,
|
||||
"type" => :hnitem@type,
|
||||
"by" => :hnuser@id,
|
||||
"time" => :hnitem@time,
|
||||
"text" => :hnitem@text,
|
||||
"dead" => :hnitem@dead,
|
||||
"parent" => :hnitem@parent,
|
||||
"poll" => :hnitem@poll,
|
||||
"kids" => :hnitem@kids,
|
||||
"url" => :hnitem@url,
|
||||
"score" => :hnitem@score,
|
||||
"title" => :hnitem@title,
|
||||
"parts" => :hnitem@parts,
|
||||
"descendants" => :hnitem@descendants
|
||||
}
|
||||
defp map_item({"kids", ids}), do: {@mapping["kids"], Enum.map(ids, &wrap_item_id/1)}
|
||||
defp map_item({"parent", id}), do: {@mapping["kids"], wrap_item_id(id)}
|
||||
defp map_item({"poll", id}), do: {@mapping["kids"], wrap_item_id(id)}
|
||||
defp map_item({"parts", ids}), do: {@mapping["parts"], Enum.map(ids, &wrap_item_id/1)}
|
||||
defp map_item({"time", t}), do: {@mapping["time"], DateTime.from_unix!(t)}
|
||||
defp map_item({k, v}), do: {@mapping[k], v}
|
||||
end
|
||||
```
|
||||
|
||||
So `HackerNews` is a implementation of the [hackernews api](https://github.com/HackerNews/API).
|
||||
This example is a little bit more complex because it provides nested attributes, so I'll only explain the available resolvers and their required attributes. For more details on the meaing of the data you can check the documentation of the api.
|
||||
|
||||
So we start with the `:top` resolver, that requires only the `:hntop@size` attribute, it gonna give back a list of `:hnitem@id`.
|
||||
We also have `:item` that requires a `:hnitem@id`.
|
||||
And at last there is `:user` that requires a `:hnuser@id`.
|
||||
|
||||
Just to highlight some relations:
|
||||
|
||||
```mermaid
|
||||
graph TD;
|
||||
hntop+size-->hntop+items;
|
||||
hntop+items-->hnitem+id;
|
||||
hnitem+id-->hnuser+id;
|
||||
hnitem+id-->hnitem+kids;
|
||||
hnitem+kids-->hnitem+id;
|
||||
```
|
||||
|
||||
## Using Boto
|
||||
|
||||
```elixir
|
||||
:boto_server.start_link(resolvers: [Weather, HackerNews])
|
||||
```
|
||||
|
||||
Here we're starting the server with the resolvers `Weather` and `HackerNews`. This enables `:boto` to query both resolvers to gather data.
|
||||
|
||||
Just to be explicit, although those resolvers doesn't relate to each other, they could include data that depends on each other to be queried with no issues.
|
||||
|
||||
```elixir
|
||||
:boto.query(%{}, [:location@temp, :location@lat, :location@long])
|
||||
```
|
||||
|
||||
So to resolve this we resolved the following resolvers:
|
||||
|
||||
```mermaid
|
||||
graph TD;
|
||||
local_ip-->ip_location
|
||||
ip_location-->location_temp
|
||||
```
|
||||
|
||||
```elixir
|
||||
:boto.query(%{net@ip: "8.8.8.8"}, [:location@temp, :location@lat, :location@long])
|
||||
```
|
||||
|
||||
And to resolve this one, we passed through the resolvers:
|
||||
|
||||
```mermaid
|
||||
graph TD;
|
||||
ip_location-->location_temp
|
||||
```
|
||||
|
||||
```elixir
|
||||
:boto.query(%{hntop@size: 3},
|
||||
hntop@items: [
|
||||
:hnitem@title,
|
||||
:hnitem@score,
|
||||
:hnuser@id,
|
||||
:hnuser@karma
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
So with this we actually passed through:
|
||||
|
||||
```mermaid
|
||||
graph TD;
|
||||
top-->item;
|
||||
item-->user;
|
||||
```
|
||||
|
||||
But it's interesting to note that for all `hntop@items`(in this case 3), it called `item` resolver and `user` resolver to provide all the requested data.
|
||||
|
||||
<!-- livebook:{"break_markdown":true} -->
|
||||
|
||||
Just a last example to show that the in a single query you can ask for data for both modules implementing the `:boto_resolver` behaviour.
|
||||
|
||||
```elixir
|
||||
:boto.query(
|
||||
%{hntop@size: 1, net@ip: "209.216.230.240"},
|
||||
[
|
||||
:location@temp,
|
||||
hntop@items: [
|
||||
:hnitem@title,
|
||||
:hnitem@score,
|
||||
:hnuser@id,
|
||||
:hnuser@karma
|
||||
]
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
This last query got just the top item for hacker news and the current temperature for the ip `209.216.230.240` that is the ip for `news.ycombinator.com` per [who.is data](https://who.is/dns/news.ycombinator.com).
|
Loading…
Reference in New Issue