This document describes the public API of BlogEngine modules.
The core module providing business logic for post management.
@type post_id :: non_neg_integer()
@type posts :: list(Post.t())
@type state :: %{posts: posts(), next_id: post_id()}@spec init() :: state()Initializes the blog engine by loading existing posts from storage.
Returns: Initial state with posts and next available ID
Example:
state = BlogEngine.init()
# => %{posts: [...], next_id: 5}@spec create_post(state(), String.t(), String.t(), list(String.t()) | nil) :: {state(), Post.t()}Creates a new post with the given title, body, and optional tags.
Parameters:
state- Current application statetitle- Post titlebody- Post body contenttags- Optional list of tags (default: nil)
Returns: Tuple of {updated_state, created_post}
Example:
{new_state, post} = BlogEngine.create_post(
state,
"My Title",
"Post content here",
["elixir", "tutorial"]
)@spec list_posts(state(), String.t() | nil) :: posts()Lists all posts, optionally filtered by tag. Posts are sorted by creation date (newest first).
Parameters:
state- Current application statetag- Optional tag to filter by (default: nil)
Returns: List of posts
Example:
all_posts = BlogEngine.list_posts(state)
elixir_posts = BlogEngine.list_posts(state, "elixir")@spec find_post(state(), post_id()) :: {:ok, Post.t()} | {:error, :not_found}Finds a post by its ID.
Parameters:
state- Current application stateid- Post ID to find
Returns:
{:ok, post}if found{:error, :not_found}if not found
Example:
case BlogEngine.find_post(state, 1) do
{:ok, post} -> IO.puts(post.title)
{:error, :not_found} -> IO.puts("Not found")
end@spec update_post(state(), post_id(), keyword()) :: {:ok, state(), Post.t()} | {:error, :not_found}Updates a post with new attributes.
Parameters:
state- Current application stateid- Post ID to updateattrs- Keyword list of attributes to update (:title,:body,:tags)
Returns:
{:ok, updated_state, updated_post}if successful{:error, :not_found}if post doesn't exist
Example:
{:ok, new_state, updated_post} = BlogEngine.update_post(
state,
1,
title: "New Title",
tags: ["updated"]
)@spec delete_post(state(), post_id()) :: {:ok, state()} | {:error, :not_found}Deletes a post by its ID.
Parameters:
state- Current application stateid- Post ID to delete
Returns:
{:ok, updated_state}if successful{:error, :not_found}if post doesn't exist
Example:
{:ok, new_state} = BlogEngine.delete_post(state, 1)@spec search_posts(state(), String.t()) :: posts()Searches posts by query string. Searches in title, body, and tags (case-insensitive).
Parameters:
state- Current application statequery- Search query string
Returns: List of matching posts (sorted by date, newest first)
Example:
results = BlogEngine.search_posts(state, "elixir")@spec get_all_tags(state()) :: list({String.t(), non_neg_integer()})Gets all unique tags from all posts with their post counts.
Parameters:
state- Current application state
Returns: List of {tag, count} tuples, sorted by count (descending)
Example:
tags = BlogEngine.get_all_tags(state)
# => [{"elixir", 5}, {"tutorial", 3}, ...]@spec export_posts(state(), String.t()) :: :ok | {:error, term()}Exports all posts to a JSON file.
Parameters:
state- Current application statepath- File path to export to
Returns: :ok or {:error, reason}
Example:
:ok = BlogEngine.export_posts(state, "/tmp/backup.json")@spec import_posts(state(), String.t()) :: {:ok, state()} | {:error, term()}Imports posts from a JSON file and merges with existing posts. Assigns new IDs to avoid conflicts.
Parameters:
state- Current application statepath- File path to import from
Returns:
{:ok, updated_state}if successful{:error, reason}if file can't be read or parsed
Example:
{:ok, new_state} = BlogEngine.import_posts(state, "/tmp/backup.json")Represents a blog post with all its metadata.
@type t :: %__MODULE__{
id: non_neg_integer(),
title: String.t(),
body: String.t(),
tags: list(String.t()) | nil,
created_at: DateTime.t(),
updated_at: DateTime.t() | nil
}@spec new(non_neg_integer(), String.t(), String.t(), list(String.t()) | nil) :: t()Creates a new post with the given attributes.
Parameters:
id- Post IDtitle- Post titlebody- Post body contenttags- Optional list of tags (default: nil)
Returns: New Post struct
Example:
post = Post.new(1, "Title", "Body", ["tag1"])@spec update(t(), keyword()) :: t()Updates a post with new attributes. Sets the updated_at timestamp.
Parameters:
post- Post to updateattrs- Keyword list of attributes to update
Returns: Updated Post struct
Example:
updated = Post.update(post, title: "New Title", tags: ["new"])@spec format_date(DateTime.t()) :: String.t()Formats a DateTime for display.
Parameters:
datetime- DateTime to format
Returns: Formatted string (YYYY-MM-DD HH:MM:SS)
Example:
Post.format_date(post.created_at)
# => "2025-11-13 17:32:15"@spec preview(t()) :: String.t()Returns a short preview of the post (title and first 100 chars of body).
Parameters:
post- Post to preview
Returns: Preview string
Example:
preview = Post.preview(post)@spec matches_query?(t(), String.t()) :: boolean()Checks if a post matches a search query (searches title, body, and tags, case-insensitive).
Parameters:
post- Post to checkquery- Search query
Returns: true if matches, false otherwise
Example:
if Post.matches_query?(post, "elixir") do
IO.puts("Match found!")
end@spec has_tag?(t(), String.t()) :: boolean()Checks if a post has a specific tag (case-insensitive).
Parameters:
post- Post to checktag- Tag to search for
Returns: true if post has the tag, false otherwise
Example:
if Post.has_tag?(post, "tutorial") do
IO.puts("This is a tutorial!")
endHandles persistent storage of blog posts to JSON files.
@spec load_posts() :: list(Post.t())Loads all posts from the storage file.
Returns: List of posts (empty list if file doesn't exist)
Example:
posts = Storage.load_posts()@spec save_posts(list(Post.t())) :: :ok | {:error, term()}Saves all posts to the storage file. Creates the directory if needed.
Parameters:
posts- List of posts to save
Returns: :ok or {:error, reason}
Example:
:ok = Storage.save_posts(posts)@spec export_posts(list(Post.t()), String.t()) :: :ok | {:error, term()}Exports posts to a specified file path.
Parameters:
posts- List of posts to exportpath- File path to export to
Returns: :ok or {:error, reason}
Example:
:ok = Storage.export_posts(posts, "/tmp/export.json")@spec import_posts(String.t()) :: {:ok, list(Post.t())} | {:error, term()}Imports posts from a specified file path.
Parameters:
path- File path to import from
Returns:
{:ok, posts}if successful{:error, reason}if file can't be read or parsed
Example:
{:ok, imported_posts} = Storage.import_posts("/tmp/import.json")Command-line interface for the BlogEngine application.
def main(_args)Main entry point for the escript.
Parameters:
_args- Command-line arguments (currently unused)
def start()Starts the interactive blog engine CLI.
Example:
BlogEngine.CLI.start()# Initialize
state = BlogEngine.init()
# Create
{state, post} = BlogEngine.create_post(state, "Title", "Body", ["tag"])
# Read
{:ok, post} = BlogEngine.find_post(state, post.id)
# Update
{:ok, state, post} = BlogEngine.update_post(state, post.id, title: "New Title")
# Delete
{:ok, state} = BlogEngine.delete_post(state, post.id)# Search
results = BlogEngine.search_posts(state, "elixir")
# Filter by tag
elixir_posts = BlogEngine.list_posts(state, "elixir")
# Get all tags
tags = BlogEngine.get_all_tags(state)# Export
:ok = BlogEngine.export_posts(state, "backup.json")
# Import
{:ok, state} = BlogEngine.import_posts(state, "backup.json")# Create and modify
post = Post.new(1, "Title", "Body", ["tag"])
updated = Post.update(post, title: "New Title")
# Check properties
if Post.has_tag?(post, "tutorial") do
IO.puts("This is a tutorial")
end
if Post.matches_query?(post, "elixir") do
IO.puts("Found elixir content")
end
# Format for display
formatted_date = Post.format_date(post.created_at)
preview = Post.preview(post)All functions that can fail return tagged tuples:
# Success
{:ok, result}
{:ok, state, result}
:ok
# Failure
{:error, reason}
{:error, :not_found}Always pattern match on results:
case BlogEngine.find_post(state, id) do
{:ok, post} ->
# Handle success
{:error, :not_found} ->
# Handle not found
end
BlogEngine is designed for single-user CLI usage and is not thread-safe. The state is managed through functional updates rather than shared mutable state.
If you need concurrent access, wrap operations in a GenServer or similar process.
For more information, see: