defmodule OrangeSite do @moduledoc """ An Elixir client library for the Hacker News API. This module provides functions to interact with the Hacker News API, including fetching items (stories, comments, jobs, polls), user data, and various story feeds. ## Examples # Fetch a story {:ok, item} = OrangeSite.get_item(8863) # Fetch a user {:ok, user} = OrangeSite.get_user("pg") # Get top stories {:ok, story_ids} = OrangeSite.get_top_stories() # Get the maximum item id {:ok, max_id} = OrangeSite.get_max_item() """ alias OrangeSite.{Client, Item, User} @doc """ Fetches an item by its ID. Returns `{:ok, %Item{}}` on success, or `{:error, reason}` on failure. ## Examples OrangeSite.get_item(8863) # {:ok, %OrangeSite.Item{id: 8863, type: "story", ...}} """ @spec get_item(integer()) :: {:ok, Item.t()} | {:error, term()} def get_item(id) when is_integer(id) do case Client.get("/item/#{id}.json") do {:ok, nil} -> {:error, :not_found} {:ok, data} -> {:ok, Item.new(data)} error -> error end end @doc """ Fetches an item by its ID, raising on error. ## Examples OrangeSite.get_item!(8863) # %OrangeSite.Item{id: 8863, type: "story", ...} """ @spec get_item!(integer()) :: Item.t() def get_item!(id) when is_integer(id) do case get_item(id) do {:ok, item} -> item {:error, reason} -> raise "Failed to fetch item #{id}: #{inspect(reason)}" end end @doc """ Fetches a user by their username. Returns `{:ok, %User{}}` on success, or `{:error, reason}` on failure. ## Examples OrangeSite.get_user("pg") # {:ok, %OrangeSite.User{id: "pg", ...}} """ @spec get_user(String.t()) :: {:ok, User.t()} | {:error, term()} def get_user(username) when is_binary(username) do case Client.get("/user/#{username}.json") do {:ok, nil} -> {:error, :not_found} {:ok, data} -> {:ok, User.new(data)} error -> error end end @doc """ Fetches a user by their username, raising on error. ## Examples OrangeSite.get_user!("pg") # %OrangeSite.User{id: "pg", ...} """ @spec get_user!(String.t()) :: User.t() def get_user!(username) when is_binary(username) do case get_user(username) do {:ok, user} -> user {:error, reason} -> raise "Failed to fetch user #{username}: #{inspect(reason)}" end end @doc """ Gets the current largest item ID. Returns `{:ok, integer()}` on success, or `{:error, reason}` on failure. ## Examples OrangeSite.get_max_item() # {:ok, 37839134} """ @spec get_max_item() :: {:ok, integer()} | {:error, term()} def get_max_item do Client.get("/maxitem.json") end @doc """ Gets the current largest item ID, raising on error. """ @spec get_max_item!() :: integer() def get_max_item! do Client.get!("/maxitem.json") end @doc """ Gets up to 500 top story IDs. Returns `{:ok, [integer()]}` on success, or `{:error, reason}` on failure. ## Examples OrangeSite.get_top_stories() # {:ok, [37839134, 37838989, ...]} """ @spec get_top_stories() :: {:ok, list(integer())} | {:error, term()} def get_top_stories do Client.get("/topstories.json") end @doc """ Gets up to 500 top story IDs, raising on error. """ @spec get_top_stories!() :: list(integer()) def get_top_stories! do Client.get!("/topstories.json") end @doc """ Gets up to 500 new story IDs. Returns `{:ok, [integer()]}` on success, or `{:error, reason}` on failure. ## Examples OrangeSite.get_new_stories() # {:ok, [37839200, 37839150, ...]} """ @spec get_new_stories() :: {:ok, list(integer())} | {:error, term()} def get_new_stories do Client.get("/newstories.json") end @doc """ Gets up to 500 new story IDs, raising on error. """ @spec get_new_stories!() :: list(integer()) def get_new_stories! do Client.get!("/newstories.json") end @doc """ Gets up to 500 best story IDs. Returns `{:ok, [integer()]}` on success, or `{:error, reason}` on failure. ## Examples OrangeSite.get_best_stories() # {:ok, [37835820, 37834999, ...]} """ @spec get_best_stories() :: {:ok, list(integer())} | {:error, term()} def get_best_stories do Client.get("/beststories.json") end @doc """ Gets up to 500 best story IDs, raising on error. """ @spec get_best_stories!() :: list(integer()) def get_best_stories! do Client.get!("/beststories.json") end @doc """ Gets up to 200 Ask HN story IDs. Returns `{:ok, [integer()]}` on success, or `{:error, reason}` on failure. ## Examples OrangeSite.get_ask_stories() # {:ok, [37832000, 37831500, ...]} """ @spec get_ask_stories() :: {:ok, list(integer())} | {:error, term()} def get_ask_stories do Client.get("/askstories.json") end @doc """ Gets up to 200 Ask HN story IDs, raising on error. """ @spec get_ask_stories!() :: list(integer()) def get_ask_stories! do Client.get!("/askstories.json") end @doc """ Gets up to 200 Show HN story IDs. Returns `{:ok, [integer()]}` on success, or `{:error, reason}` on failure. ## Examples OrangeSite.get_show_stories() # {:ok, [37830000, 37829500, ...]} """ @spec get_show_stories() :: {:ok, list(integer())} | {:error, term()} def get_show_stories do Client.get("/showstories.json") end @doc """ Gets up to 200 Show HN story IDs, raising on error. """ @spec get_show_stories!() :: list(integer()) def get_show_stories! do Client.get!("/showstories.json") end @doc """ Gets up to 200 job story IDs. Returns `{:ok, [integer()]}` on success, or `{:error, reason}` on failure. ## Examples OrangeSite.get_job_stories() # {:ok, [37825000, 37824500, ...]} """ @spec get_job_stories() :: {:ok, list(integer())} | {:error, term()} def get_job_stories do Client.get("/jobstories.json") end @doc """ Gets up to 200 job story IDs, raising on error. """ @spec get_job_stories!() :: list(integer()) def get_job_stories! do Client.get!("/jobstories.json") end @doc """ Fetches multiple items in parallel. Returns a list of `{:ok, item}` or `{:error, reason}` tuples. ## Examples OrangeSite.get_items([8863, 8864]) # [{:ok, %OrangeSite.Item{...}}, {:ok, %OrangeSite.Item{...}}] """ @spec get_items(list(integer())) :: list({:ok, Item.t()} | {:error, term()}) def get_items(ids) when is_list(ids) do ids |> Task.async_stream(&get_item/1, ordered: true) |> Enum.map(fn {:ok, result} -> result end) end @doc """ Fetches the top N stories with their full data. ## Examples OrangeSite.fetch_top_stories(10) # {:ok, [%OrangeSite.Item{...}, ...]} """ @spec fetch_top_stories(integer()) :: {:ok, list(Item.t())} | {:error, term()} def fetch_top_stories(limit \\ 30) do with {:ok, ids} <- get_top_stories() do stories = ids |> Enum.take(limit) |> get_items() |> Enum.filter(fn {:ok, _} -> true _ -> false end) |> Enum.map(fn {:ok, item} -> item end) {:ok, stories} end end @doc """ Fetches the top N stories with their full data, raising on error. """ @spec fetch_top_stories!(integer()) :: list(Item.t()) def fetch_top_stories!(limit \\ 30) do case fetch_top_stories(limit) do {:ok, stories} -> stories {:error, reason} -> raise "Failed to fetch top stories: #{inspect(reason)}" end end end