fix: AI tools better described now
This commit is contained in:
@@ -515,11 +515,30 @@ defmodule BDS.AI.Chat do
|
||||
base = get_setting("ai.system_prompt") || @default_system_prompt
|
||||
|
||||
case project_stats_summary(project_id) do
|
||||
nil -> base
|
||||
summary -> base <> "\n\nCurrent blog statistics:\n" <> summary
|
||||
nil ->
|
||||
base
|
||||
|
||||
summary ->
|
||||
base <> "\n\nCurrent blog statistics:\n" <> summary <> "\n\n" <> blog_tool_guidance()
|
||||
end
|
||||
end
|
||||
|
||||
defp blog_tool_guidance do
|
||||
Enum.join(
|
||||
[
|
||||
"Available blog data tools:",
|
||||
"- Use blog_stats for aggregate counts of posts, media, tags, and categories.",
|
||||
"- Use search_posts for full-text blog search and filtered post lookup by category, tag, language, year, month, or status.",
|
||||
"- Use read_post_by_slug to read full post content and metadata when a slug is known.",
|
||||
"- Use list_posts when asked for post titles, slugs, URLs, statuses, backlinks, or recent/top/latest post lists. This is allowed project data access.",
|
||||
"- Use list_media when asked for media titles, filenames, MIME types, or recent media lists. This is allowed project data access.",
|
||||
"- Use list_tags, list_categories, and count_posts for taxonomy and grouped analytics questions.",
|
||||
"If a requested blog fact is available through these tools, call the tool instead of saying you cannot access the data."
|
||||
],
|
||||
"\n"
|
||||
)
|
||||
end
|
||||
|
||||
defp project_stats_summary(nil), do: nil
|
||||
|
||||
defp project_stats_summary(project_id) do
|
||||
|
||||
@@ -5,9 +5,11 @@ defmodule BDS.AI.ChatTools do
|
||||
|
||||
alias BDS.AI.Chat
|
||||
alias BDS.Media.Media
|
||||
alias BDS.MCP.Queries
|
||||
alias BDS.Posts.Post
|
||||
alias BDS.Projects.Project
|
||||
alias BDS.Repo
|
||||
alias BDS.Search
|
||||
|
||||
@spec execute(String.t(), map(), String.t() | nil) :: map()
|
||||
def execute("blog_stats", _arguments, project_id) do
|
||||
@@ -23,20 +25,79 @@ defmodule BDS.AI.ChatTools do
|
||||
}
|
||||
end
|
||||
|
||||
def execute("list_posts", arguments, project_id) do
|
||||
limit = normalize_limit(arguments["limit"])
|
||||
def execute("check_term", arguments, project_id) do
|
||||
project_id = project_id || active_project_id()
|
||||
term = normalize_term(arguments["term"])
|
||||
|
||||
Repo.all(
|
||||
from(post in Post,
|
||||
where: post.project_id == ^project_id,
|
||||
order_by: [desc: post.updated_at],
|
||||
limit: ^limit,
|
||||
select: %{id: post.id, title: post.title, slug: post.slug, status: post.status}
|
||||
)
|
||||
)
|
||||
posts = Repo.all(from post in Post, where: post.project_id == ^project_id)
|
||||
|
||||
tag_post_count =
|
||||
Enum.count(posts, fn post ->
|
||||
Enum.any?(post.tags || [], &(normalize_term(&1) == term))
|
||||
end)
|
||||
|
||||
category_post_count =
|
||||
Enum.count(posts, fn post ->
|
||||
Enum.any?(post.categories || [], &(normalize_term(&1) == term))
|
||||
end)
|
||||
|
||||
%{
|
||||
is_category: category_post_count > 0,
|
||||
category_post_count: category_post_count,
|
||||
is_tag: tag_post_count > 0,
|
||||
tag_post_count: tag_post_count
|
||||
}
|
||||
end
|
||||
|
||||
def execute("search_posts", arguments, project_id) do
|
||||
project_id = project_id || active_project_id()
|
||||
filters = search_filters(arguments)
|
||||
|
||||
{:ok, result} = Search.search_posts(project_id, arguments["query"] || "", filters)
|
||||
|
||||
%{
|
||||
posts: Enum.map(result.posts, &Queries.post_summary/1),
|
||||
total: result.total,
|
||||
offset: result.offset,
|
||||
limit: result.limit,
|
||||
has_more: result.offset + result.limit < result.total
|
||||
}
|
||||
end
|
||||
|
||||
def execute("read_post_by_slug", arguments, project_id) do
|
||||
project_id = project_id || active_project_id()
|
||||
|
||||
case Repo.get_by(Post, project_id: project_id, slug: arguments["slug"]) do
|
||||
%Post{} = post -> %{post: Queries.post_detail(post)}
|
||||
nil -> %{error: "not_found"}
|
||||
end
|
||||
end
|
||||
|
||||
def execute("list_posts", arguments, project_id) do
|
||||
project_id = project_id || active_project_id()
|
||||
limit = normalize_limit(arguments["limit"])
|
||||
offset = normalize_offset(arguments["offset"])
|
||||
filters = search_filters(arguments) |> Map.merge(%{limit: limit, offset: offset})
|
||||
|
||||
{:ok, result} = Search.search_posts(project_id, "", filters)
|
||||
|
||||
%{
|
||||
posts:
|
||||
Enum.map(result.posts, fn post ->
|
||||
post
|
||||
|> Queries.post_summary()
|
||||
|> Map.put("url", "/posts/#{post.slug}")
|
||||
|> Map.put("updated_at", post.updated_at)
|
||||
end),
|
||||
total: result.total,
|
||||
offset: result.offset,
|
||||
limit: result.limit,
|
||||
has_more: result.offset + result.limit < result.total
|
||||
}
|
||||
end
|
||||
|
||||
def execute("list_media", arguments, project_id) do
|
||||
project_id = project_id || active_project_id()
|
||||
limit = normalize_limit(arguments["limit"])
|
||||
|
||||
Repo.all(
|
||||
@@ -48,12 +109,46 @@ defmodule BDS.AI.ChatTools do
|
||||
id: media.id,
|
||||
title: media.title,
|
||||
mime_type: media.mime_type,
|
||||
filename: media.filename
|
||||
filename: media.filename,
|
||||
updated_at: media.updated_at
|
||||
}
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def execute("list_tags", _arguments, project_id) do
|
||||
project_id = project_id || active_project_id()
|
||||
|
||||
%{
|
||||
tags: counted_terms(project_id, :tags),
|
||||
count: length(counted_terms(project_id, :tags))
|
||||
}
|
||||
end
|
||||
|
||||
def execute("list_categories", _arguments, project_id) do
|
||||
project_id = project_id || active_project_id()
|
||||
|
||||
%{
|
||||
categories: counted_terms(project_id, :categories),
|
||||
count: length(counted_terms(project_id, :categories))
|
||||
}
|
||||
end
|
||||
|
||||
def execute("count_posts", arguments, project_id) do
|
||||
project_id = project_id || active_project_id()
|
||||
group_by = List.wrap(arguments["groupBy"] || arguments["group_by"]) |> Enum.map(&to_string/1)
|
||||
{:ok, result} = Search.search_posts(project_id, "", search_filters(arguments))
|
||||
|
||||
groups =
|
||||
result.posts
|
||||
|> Enum.flat_map(&Queries.group_rows(&1, group_by))
|
||||
|> Enum.group_by(& &1, fn _row -> 1 end)
|
||||
|> Enum.map(fn {row, counts} -> Map.put(row, "count", length(counts)) end)
|
||||
|> Enum.sort_by(&Map.to_list/1)
|
||||
|
||||
%{groups: groups, total_posts: result.total}
|
||||
end
|
||||
|
||||
def execute("render_table", arguments, _project_id) do
|
||||
%{
|
||||
type: "table",
|
||||
@@ -142,19 +237,88 @@ defmodule BDS.AI.ChatTools do
|
||||
"properties" => %{}
|
||||
})
|
||||
},
|
||||
%{
|
||||
name: "check_term",
|
||||
spec:
|
||||
tool_spec(
|
||||
"check_term",
|
||||
"Check whether a term exists as a category, tag, or both. Returns post counts for each. Use before search_posts or list_posts when unsure whether a term is a category or tag.",
|
||||
%{
|
||||
"type" => "object",
|
||||
"properties" => %{"term" => %{"type" => "string"}},
|
||||
"required" => ["term"]
|
||||
}
|
||||
)
|
||||
},
|
||||
%{
|
||||
name: "search_posts",
|
||||
spec:
|
||||
tool_spec(
|
||||
"search_posts",
|
||||
"Search blog posts using full-text search. Can filter by category, tags, language, missing translation language, year, month, or status. Returns paginated concrete post data with titles, slugs, tags, categories, backlinks, and links_to.",
|
||||
post_search_schema(true)
|
||||
)
|
||||
},
|
||||
%{
|
||||
name: "read_post_by_slug",
|
||||
spec:
|
||||
tool_spec(
|
||||
"read_post_by_slug",
|
||||
"Read full content and metadata of a specific blog post by slug. Includes backlinks, links_to, tags, categories, excerpt, status, language, and available languages.",
|
||||
%{
|
||||
"type" => "object",
|
||||
"properties" => %{"slug" => %{"type" => "string"}},
|
||||
"required" => ["slug"]
|
||||
}
|
||||
)
|
||||
},
|
||||
%{
|
||||
name: "list_posts",
|
||||
spec:
|
||||
tool_spec("list_posts", "List recent posts in the active project", limit_schema())
|
||||
tool_spec(
|
||||
"list_posts",
|
||||
"List blog posts with optional filtering by status, category, tags, language, year, or month. Returns paginated concrete post data with titles, slugs, URLs, statuses, tags, categories, backlinks, and links_to. Use for recent, latest, top, or title-list requests.",
|
||||
post_search_schema(false)
|
||||
)
|
||||
},
|
||||
%{
|
||||
name: "list_media",
|
||||
spec:
|
||||
tool_spec(
|
||||
"list_media",
|
||||
"List recent media items in the active project",
|
||||
"List concrete media data in the active project, including titles, filenames, MIME types, and update times.",
|
||||
limit_schema()
|
||||
)
|
||||
},
|
||||
%{
|
||||
name: "list_tags",
|
||||
spec:
|
||||
tool_spec(
|
||||
"list_tags",
|
||||
"List all tags used across blog posts with post counts.",
|
||||
%{
|
||||
"type" => "object",
|
||||
"properties" => %{}
|
||||
}
|
||||
)
|
||||
},
|
||||
%{
|
||||
name: "list_categories",
|
||||
spec:
|
||||
tool_spec(
|
||||
"list_categories",
|
||||
"List all categories used across blog posts with post counts.",
|
||||
%{"type" => "object", "properties" => %{}}
|
||||
)
|
||||
},
|
||||
%{
|
||||
name: "count_posts",
|
||||
spec:
|
||||
tool_spec(
|
||||
"count_posts",
|
||||
"Count posts grouped by dimensions such as year, month, tag, category, or status. Use for analytics, distributions, and heat maps without transferring full post content.",
|
||||
count_posts_schema()
|
||||
)
|
||||
}
|
||||
]
|
||||
else
|
||||
@@ -245,6 +409,47 @@ defmodule BDS.AI.ChatTools do
|
||||
}
|
||||
end
|
||||
|
||||
defp post_search_schema(require_query) do
|
||||
schema = %{
|
||||
"type" => "object",
|
||||
"properties" => %{
|
||||
"query" => %{"type" => "string"},
|
||||
"status" => %{"type" => "string", "enum" => ["draft", "published", "archived"]},
|
||||
"category" => %{"type" => "string"},
|
||||
"tags" => %{"type" => "array", "items" => %{"type" => "string"}},
|
||||
"language" => %{"type" => "string"},
|
||||
"missingTranslationLanguage" => %{"type" => "string"},
|
||||
"year" => %{"type" => "integer"},
|
||||
"month" => %{"type" => "integer", "minimum" => 1, "maximum" => 12},
|
||||
"limit" => %{"type" => "integer", "minimum" => 1, "maximum" => 50},
|
||||
"offset" => %{"type" => "integer", "minimum" => 0}
|
||||
}
|
||||
}
|
||||
|
||||
if require_query, do: Map.put(schema, "required", ["query"]), else: schema
|
||||
end
|
||||
|
||||
defp count_posts_schema do
|
||||
%{
|
||||
"type" => "object",
|
||||
"properties" => %{
|
||||
"groupBy" => %{
|
||||
"type" => "array",
|
||||
"items" => %{
|
||||
"type" => "string",
|
||||
"enum" => ["year", "month", "tag", "category", "status"]
|
||||
}
|
||||
},
|
||||
"year" => %{"type" => "integer"},
|
||||
"month" => %{"type" => "integer", "minimum" => 1, "maximum" => 12},
|
||||
"status" => %{"type" => "string", "enum" => ["draft", "published", "archived"]},
|
||||
"category" => %{"type" => "string"},
|
||||
"tags" => %{"type" => "array", "items" => %{"type" => "string"}}
|
||||
},
|
||||
"required" => ["groupBy"]
|
||||
}
|
||||
end
|
||||
|
||||
defp render_table_schema do
|
||||
%{
|
||||
"type" => "object",
|
||||
@@ -334,6 +539,41 @@ defmodule BDS.AI.ChatTools do
|
||||
defp normalize_limit(value) when is_integer(value) and value > 0 and value <= 50, do: value
|
||||
defp normalize_limit(_value), do: 10
|
||||
|
||||
defp normalize_offset(value) when is_integer(value) and value >= 0, do: value
|
||||
defp normalize_offset(_value), do: 0
|
||||
|
||||
defp search_filters(arguments) do
|
||||
%{}
|
||||
|> maybe_put(:category, arguments["category"])
|
||||
|> maybe_put(:tags, arguments["tags"])
|
||||
|> maybe_put(:language, arguments["language"])
|
||||
|> maybe_put(:missing_translation_language, arguments["missingTranslationLanguage"])
|
||||
|> maybe_put(:year, arguments["year"])
|
||||
|> maybe_put(:month, arguments["month"])
|
||||
|> maybe_put(:status, BDS.BoundedAtoms.post_status(arguments["status"]))
|
||||
|> Map.put(:offset, normalize_offset(arguments["offset"]))
|
||||
|> Map.put(:limit, normalize_limit(arguments["limit"]))
|
||||
end
|
||||
|
||||
defp maybe_put(map, _key, nil), do: map
|
||||
defp maybe_put(map, _key, ""), do: map
|
||||
defp maybe_put(map, key, value), do: Map.put(map, key, value)
|
||||
|
||||
defp counted_terms(project_id, field) do
|
||||
Repo.all(
|
||||
from post in Post, where: post.project_id == ^project_id, select: field(post, ^field)
|
||||
)
|
||||
|> List.flatten()
|
||||
|> Enum.reject(&blank?/1)
|
||||
|> Enum.frequencies()
|
||||
|> Enum.map(fn {term, count} -> %{name: term, count: count} end)
|
||||
|> Enum.sort_by(&String.downcase(to_string(&1.name)))
|
||||
end
|
||||
|
||||
defp blank?(value), do: is_nil(value) or String.trim(to_string(value)) == ""
|
||||
|
||||
defp normalize_term(value), do: value |> to_string() |> String.downcase()
|
||||
|
||||
defp active_project_id do
|
||||
Repo.one(from(project in Project, where: project.is_active == true, select: project.id))
|
||||
end
|
||||
|
||||
@@ -18,7 +18,13 @@ defmodule BDS.MCP.Tools do
|
||||
@proposal_ttl_app_ms 30 * 60 * 1000
|
||||
|
||||
@typedoc "Tool descriptor returned by `list/0`."
|
||||
@type descriptor :: %{name: String.t(), annotations: map()}
|
||||
@type descriptor :: %{
|
||||
name: String.t(),
|
||||
title: String.t(),
|
||||
description: String.t(),
|
||||
inputSchema: map(),
|
||||
annotations: map()
|
||||
}
|
||||
|
||||
@spec list() :: [descriptor()]
|
||||
def list do
|
||||
@@ -75,12 +81,269 @@ defmodule BDS.MCP.Tools do
|
||||
end
|
||||
|
||||
defp tool(name, read_only) do
|
||||
metadata = tool_metadata(name)
|
||||
|
||||
%{
|
||||
name: name,
|
||||
annotations: %{"readOnlyHint" => read_only, "destructiveHint" => false}
|
||||
title: metadata.title,
|
||||
description: metadata.description,
|
||||
inputSchema: metadata.input_schema,
|
||||
annotations: %{
|
||||
"readOnlyHint" => read_only,
|
||||
"destructiveHint" => false,
|
||||
"openWorldHint" => false
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp tool_metadata("check_term") do
|
||||
%{
|
||||
title: "Check Term",
|
||||
description:
|
||||
"Check whether a term exists as a category, tag, or both. Returns post counts for each. Use before search_posts or count_posts when unsure whether a term is a category or tag.",
|
||||
input_schema: object_schema(%{"term" => string_schema("The term to look up")}, ["term"])
|
||||
}
|
||||
end
|
||||
|
||||
defp tool_metadata("search_posts") do
|
||||
%{
|
||||
title: "Search Posts",
|
||||
description:
|
||||
"Search blog posts by query, category, tags, language, translation coverage, date, or status. Returns a paginated envelope with total, offset, limit, hasMore, and posts. Each post includes title, slug, tags, categories, backlinks, and linksTo. When hasMore is true, increase offset by limit. Use check_term first if unsure whether a term is a category or tag.",
|
||||
input_schema: post_query_schema(false)
|
||||
}
|
||||
end
|
||||
|
||||
defp tool_metadata("count_posts") do
|
||||
%{
|
||||
title: "Count Posts",
|
||||
description:
|
||||
"Count posts grouped by year, month, tag, category, or status. Returns aggregated counts without full post data, useful for analytics, distributions, and heat maps. Example: groupBy=[\"month\",\"tag\"] with year=2004.",
|
||||
input_schema:
|
||||
object_schema(
|
||||
Map.merge(group_filter_properties(), %{
|
||||
"groupBy" => %{
|
||||
"type" => "array",
|
||||
"items" => enum_schema(["year", "month", "tag", "category", "status"]),
|
||||
"description" => "Dimensions to group by; one to three dimensions is usually best"
|
||||
}
|
||||
}),
|
||||
["groupBy"]
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
defp tool_metadata("read_post_by_slug") do
|
||||
%{
|
||||
title: "Read Post By Slug",
|
||||
description:
|
||||
"Read full content and metadata for a specific blog post by slug. Includes title, excerpt, content, status, tags, categories, backlinks, linksTo, and available languages. Optionally request a translation by language.",
|
||||
input_schema:
|
||||
object_schema(
|
||||
%{
|
||||
"slug" => string_schema("The slug of the post to read"),
|
||||
"language" => string_schema("Optional language code for a translation")
|
||||
},
|
||||
["slug"]
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
defp tool_metadata("get_post_translations") do
|
||||
%{
|
||||
title: "Get Post Translations",
|
||||
description:
|
||||
"List all translations available for a blog post, including language, title, excerpt, content, and status.",
|
||||
input_schema: object_schema(%{"postId" => string_schema("The post ID")}, ["postId"])
|
||||
}
|
||||
end
|
||||
|
||||
defp tool_metadata("get_media_translations") do
|
||||
%{
|
||||
title: "Get Media Translations",
|
||||
description:
|
||||
"List all available translations for media metadata, including language, title, alt text, and captions.",
|
||||
input_schema: object_schema(%{"mediaId" => string_schema("The media ID")}, ["mediaId"])
|
||||
}
|
||||
end
|
||||
|
||||
defp tool_metadata("upsert_media_translation") do
|
||||
%{
|
||||
title: "Upsert Media Translation",
|
||||
description: "Create or update translated media metadata for a specific language.",
|
||||
input_schema:
|
||||
object_schema(
|
||||
%{
|
||||
"mediaId" => string_schema("The media ID"),
|
||||
"language" => string_schema("Language code to update"),
|
||||
"title" => string_schema("Translated title"),
|
||||
"alt" => string_schema("Translated alt text"),
|
||||
"caption" => string_schema("Translated caption")
|
||||
},
|
||||
["mediaId", "language"]
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
defp tool_metadata("draft_post") do
|
||||
%{
|
||||
title: "Draft Post",
|
||||
description: "Create a new draft blog post for review before publishing.",
|
||||
input_schema:
|
||||
object_schema(
|
||||
%{
|
||||
"title" => string_schema("Post title"),
|
||||
"content" => string_schema("Post content in Markdown"),
|
||||
"excerpt" => string_schema("Short excerpt or summary"),
|
||||
"tags" => string_array_schema("Tags for the post"),
|
||||
"categories" => string_array_schema("Categories for the post"),
|
||||
"author" => string_schema("Post author name")
|
||||
},
|
||||
["title", "content"]
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
defp tool_metadata("propose_script") do
|
||||
%{
|
||||
title: "Propose Script",
|
||||
description: "Propose a new Python script, macro, utility, or transform for review.",
|
||||
input_schema:
|
||||
object_schema(
|
||||
%{
|
||||
"title" => string_schema("Script title"),
|
||||
"kind" => enum_schema(["macro", "utility", "transform"]),
|
||||
"content" => string_schema("Python source code"),
|
||||
"entrypoint" => string_schema("Entry point function name")
|
||||
},
|
||||
["title", "kind", "content"]
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
defp tool_metadata("propose_template") do
|
||||
%{
|
||||
title: "Propose Template",
|
||||
description: "Propose a new Liquid template for review.",
|
||||
input_schema:
|
||||
object_schema(
|
||||
%{
|
||||
"title" => string_schema("Template title"),
|
||||
"kind" => enum_schema(["post", "list", "not-found", "partial"]),
|
||||
"content" => string_schema("Liquid template content")
|
||||
},
|
||||
["title", "kind", "content"]
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
defp tool_metadata("propose_media_metadata") do
|
||||
%{
|
||||
title: "Propose Media Metadata",
|
||||
description:
|
||||
"Propose changes to media metadata such as title, alt text, caption, and tags.",
|
||||
input_schema:
|
||||
object_schema(
|
||||
%{
|
||||
"mediaId" => string_schema("The media ID"),
|
||||
"title" => string_schema("New title"),
|
||||
"alt" => string_schema("New alt text"),
|
||||
"caption" => string_schema("New caption"),
|
||||
"tags" => string_array_schema("New tags")
|
||||
},
|
||||
["mediaId"]
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
defp tool_metadata("propose_post_metadata") do
|
||||
%{
|
||||
title: "Propose Post Metadata",
|
||||
description:
|
||||
"Propose changes to post metadata such as title, excerpt, tags, and categories.",
|
||||
input_schema:
|
||||
object_schema(
|
||||
%{
|
||||
"postId" => string_schema("The post ID"),
|
||||
"title" => string_schema("New title"),
|
||||
"excerpt" => string_schema("New excerpt"),
|
||||
"tags" => string_array_schema("New tags"),
|
||||
"categories" => string_array_schema("New categories")
|
||||
},
|
||||
["postId"]
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
defp tool_metadata("accept_proposal") do
|
||||
%{
|
||||
title: "Accept Proposal",
|
||||
description: "Accept a pending proposal and apply or publish its changes.",
|
||||
input_schema:
|
||||
object_schema(%{"proposalId" => string_schema("The proposal ID")}, ["proposalId"])
|
||||
}
|
||||
end
|
||||
|
||||
defp tool_metadata("discard_proposal") do
|
||||
%{
|
||||
title: "Discard Proposal",
|
||||
description: "Discard a pending proposal and remove any temporary draft artifacts.",
|
||||
input_schema:
|
||||
object_schema(%{"proposalId" => string_schema("The proposal ID")}, ["proposalId"])
|
||||
}
|
||||
end
|
||||
|
||||
defp post_query_schema(query_required) do
|
||||
required = if query_required, do: ["query"], else: []
|
||||
|
||||
object_schema(
|
||||
Map.merge(group_filter_properties(), %{
|
||||
"query" => string_schema("Full-text search query"),
|
||||
"language" => string_schema("Require posts available in this language"),
|
||||
"missingTranslationLanguage" =>
|
||||
string_schema("Require posts missing this translation language"),
|
||||
"offset" => %{"type" => "integer", "minimum" => 0, "description" => "Pagination offset"},
|
||||
"limit" => %{
|
||||
"type" => "integer",
|
||||
"minimum" => 1,
|
||||
"maximum" => 50,
|
||||
"description" => "Maximum results to return"
|
||||
}
|
||||
}),
|
||||
required
|
||||
)
|
||||
end
|
||||
|
||||
defp group_filter_properties do
|
||||
%{
|
||||
"year" => %{"type" => "integer", "description" => "Filter to posts in this year"},
|
||||
"month" => %{
|
||||
"type" => "integer",
|
||||
"minimum" => 1,
|
||||
"maximum" => 12,
|
||||
"description" => "Filter to posts in this month; requires year"
|
||||
},
|
||||
"status" => enum_schema(["draft", "published", "archived"]),
|
||||
"category" => string_schema("Filter by category"),
|
||||
"tags" => string_array_schema("Filter by tags; all must match")
|
||||
}
|
||||
end
|
||||
|
||||
defp object_schema(properties, required) do
|
||||
%{"type" => "object", "properties" => properties}
|
||||
|> maybe_schema_required(required)
|
||||
end
|
||||
|
||||
defp maybe_schema_required(schema, []), do: schema
|
||||
defp maybe_schema_required(schema, required), do: Map.put(schema, "required", required)
|
||||
|
||||
defp string_schema(description), do: %{"type" => "string", "description" => description}
|
||||
|
||||
defp string_array_schema(description),
|
||||
do: %{"type" => "array", "items" => %{"type" => "string"}, "description" => description}
|
||||
|
||||
defp enum_schema(values), do: %{"type" => "string", "enum" => values}
|
||||
|
||||
defp check_term(%{"term" => term}), do: check_term(%{term: term})
|
||||
|
||||
defp check_term(%{term: term}) do
|
||||
|
||||
Reference in New Issue
Block a user