livebook_example #11

Merged
cevado merged 2 commits from livebook_example into main 9 months ago

@ -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}&current_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…
Cancel
Save