chore: refactored mcp.ex
This commit is contained in:
668
lib/bds/mcp.ex
668
lib/bds/mcp.ex
@@ -1,677 +1,27 @@
|
||||
defmodule BDS.MCP do
|
||||
@moduledoc false
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias BDS.Media
|
||||
alias BDS.Media.Media, as: MediaAsset
|
||||
alias BDS.Metadata
|
||||
alias BDS.MCP.ProposalStore
|
||||
alias BDS.PostLinks
|
||||
alias BDS.Posts
|
||||
alias BDS.Posts.Post
|
||||
alias BDS.Posts.Translation, as: PostTranslation
|
||||
alias BDS.Projects
|
||||
alias BDS.Repo
|
||||
alias BDS.Scripts
|
||||
alias BDS.Search
|
||||
alias BDS.Tags
|
||||
alias BDS.Templates
|
||||
|
||||
@page_size 50
|
||||
@proposal_ttl_app_ms 30 * 60 * 1000
|
||||
alias BDS.MCP.Resources
|
||||
alias BDS.MCP.Tools
|
||||
|
||||
@typedoc "Tool descriptor returned by `list_tools/0`."
|
||||
@type tool_descriptor :: %{name: String.t(), annotations: map()}
|
||||
@type tool_descriptor :: Tools.descriptor()
|
||||
|
||||
@typedoc "Resource descriptor returned by `list_resources/0`."
|
||||
@type resource_descriptor :: %{name: String.t(), uri: String.t()}
|
||||
@type resource_descriptor :: Resources.descriptor()
|
||||
|
||||
@spec list_tools() :: [tool_descriptor()]
|
||||
def list_tools do
|
||||
[
|
||||
tool("check_term", true),
|
||||
tool("search_posts", true),
|
||||
tool("count_posts", true),
|
||||
tool("read_post_by_slug", true),
|
||||
tool("draft_post", false),
|
||||
tool("propose_script", false),
|
||||
tool("propose_template", false),
|
||||
tool("propose_media_metadata", false),
|
||||
tool("propose_post_metadata", false),
|
||||
tool("accept_proposal", false),
|
||||
tool("discard_proposal", false)
|
||||
]
|
||||
end
|
||||
defdelegate list_tools(), to: Tools, as: :list
|
||||
|
||||
@spec list_resources() :: [resource_descriptor()]
|
||||
def list_resources do
|
||||
[
|
||||
%{name: "posts", uri: "bds://posts"},
|
||||
%{name: "media", uri: "bds://media"},
|
||||
%{name: "tags", uri: "bds://tags"},
|
||||
%{name: "categories", uri: "bds://categories"}
|
||||
]
|
||||
end
|
||||
defdelegate list_resources(), to: Resources, as: :list
|
||||
|
||||
@spec call_tool(String.t(), map()) :: {:ok, term()} | {:error, term()}
|
||||
def call_tool(name, params) when is_binary(name) and is_map(params) do
|
||||
ProposalStore.ensure_started()
|
||||
|
||||
case name do
|
||||
"check_term" -> {:ok, check_term(params)}
|
||||
"search_posts" -> {:ok, search_posts(params)}
|
||||
"count_posts" -> {:ok, count_posts(params)}
|
||||
"read_post_by_slug" -> read_post_by_slug(params)
|
||||
"draft_post" -> draft_post(params)
|
||||
"propose_script" -> propose_script(params)
|
||||
"propose_template" -> propose_template(params)
|
||||
"propose_media_metadata" -> propose_media_metadata(params)
|
||||
"propose_post_metadata" -> propose_post_metadata(params)
|
||||
"accept_proposal" -> accept_proposal(params)
|
||||
"discard_proposal" -> discard_proposal(params)
|
||||
_other -> {:error, :unknown_tool}
|
||||
end
|
||||
end
|
||||
defdelegate call_tool(name, params), to: Tools, as: :call
|
||||
|
||||
@spec read_resource(String.t()) :: {:ok, term()} | {:error, term()}
|
||||
def read_resource(uri) when is_binary(uri) do
|
||||
ProposalStore.ensure_started()
|
||||
|
||||
case URI.parse(uri) do
|
||||
%URI{scheme: "bds", host: "posts", path: nil} -> {:ok, posts_resource(0)}
|
||||
%URI{scheme: "bds", host: "media", path: nil} -> {:ok, media_resource(0)}
|
||||
%URI{scheme: "bds", host: "tags", path: nil} -> {:ok, tags_resource()}
|
||||
%URI{scheme: "bds", host: "categories", path: nil} -> {:ok, categories_resource()}
|
||||
%URI{scheme: "bds", host: "posts", path: "/" <> id} -> read_post_resource(id)
|
||||
%URI{scheme: "bds", host: "media", path: "/" <> id} -> read_media_resource(id)
|
||||
_other -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
defdelegate read_resource(uri), to: Resources, as: :read
|
||||
|
||||
@spec validate_template(String.t()) :: {:ok, %{valid: boolean(), errors: [String.t()]}}
|
||||
def validate_template(source) when is_binary(source) do
|
||||
case Liquex.parse(source) do
|
||||
{:ok, _ast} -> {:ok, %{valid: true, errors: []}}
|
||||
{:error, reason, line} -> {:ok, %{valid: false, errors: ["#{inspect(reason)} at line #{line}"]}}
|
||||
end
|
||||
end
|
||||
|
||||
defp tool(name, read_only) do
|
||||
%{
|
||||
name: name,
|
||||
annotations: %{"readOnlyHint" => read_only, "destructiveHint" => false}
|
||||
}
|
||||
end
|
||||
|
||||
defp active_project! do
|
||||
case Enum.find(Projects.list_projects(), & &1.is_active) do
|
||||
nil -> raise "no active project"
|
||||
project -> project
|
||||
end
|
||||
end
|
||||
|
||||
defp check_term(%{"term" => term}), do: check_term(%{term: term})
|
||||
|
||||
defp check_term(%{term: term}) do
|
||||
project = active_project!()
|
||||
normalized = normalize_term(term)
|
||||
|
||||
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) == normalized))
|
||||
end)
|
||||
|
||||
category_post_count =
|
||||
Enum.count(posts, fn post ->
|
||||
Enum.any?(post.categories || [], &(normalize_term(&1) == normalized))
|
||||
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
|
||||
|
||||
defp search_posts(params) do
|
||||
project = active_project!()
|
||||
filters = search_filters(params)
|
||||
query = map_get(params, :query, "")
|
||||
{:ok, result} = Search.search_posts(project.id, query, filters)
|
||||
|
||||
posts = Enum.map(result.posts, &post_summary/1)
|
||||
|
||||
%{
|
||||
"posts" => posts,
|
||||
"total" => result.total,
|
||||
"offset" => result.offset,
|
||||
"limit" => result.limit,
|
||||
"has_more" => result.offset + result.limit < result.total
|
||||
}
|
||||
end
|
||||
|
||||
defp count_posts(params) do
|
||||
project = active_project!()
|
||||
group_by = map_get(params, :groupBy, []) |> Enum.map(&to_string/1)
|
||||
filters = search_filters(params)
|
||||
{:ok, result} = Search.search_posts(project.id, "", filters)
|
||||
|
||||
groups =
|
||||
result.posts
|
||||
|> Enum.flat_map(&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
|
||||
|
||||
defp read_post_by_slug(%{"slug" => slug} = params), do: read_post_by_slug(Map.put_new(params, :slug, slug))
|
||||
|
||||
defp read_post_by_slug(%{slug: slug} = params) do
|
||||
project = active_project!()
|
||||
|
||||
case Repo.get_by(Post, project_id: project.id, slug: slug) do
|
||||
nil -> {:error, :not_found}
|
||||
%Post{} = post ->
|
||||
payload =
|
||||
case map_get(params, :language) do
|
||||
nil -> post_detail(post)
|
||||
"" -> post_detail(post)
|
||||
language ->
|
||||
if normalize_term(language) == normalize_term(post.language) do
|
||||
post_detail(post)
|
||||
else
|
||||
translated_post_detail(post, language)
|
||||
end
|
||||
end
|
||||
|
||||
{:ok, %{"post" => payload}}
|
||||
end
|
||||
end
|
||||
|
||||
defp draft_post(params) do
|
||||
project = active_project!()
|
||||
|
||||
attrs = %{
|
||||
project_id: project.id,
|
||||
title: map_get(params, :title, ""),
|
||||
content: map_get(params, :content, ""),
|
||||
excerpt: map_get(params, :excerpt),
|
||||
tags: map_get(params, :tags, []),
|
||||
categories: map_get(params, :categories, []),
|
||||
author: map_get(params, :author)
|
||||
}
|
||||
|
||||
with {:ok, post} <- Posts.create_post(attrs) do
|
||||
proposal =
|
||||
ProposalStore.create("draft_post", %{"post_id" => post.id},
|
||||
entity_id: post.id,
|
||||
ttl_ms: @proposal_ttl_app_ms
|
||||
)
|
||||
|
||||
{:ok, %{"proposal_id" => proposal.id, "post" => sanitize(post)}}
|
||||
end
|
||||
end
|
||||
|
||||
defp propose_script(params) do
|
||||
project = active_project!()
|
||||
content = map_get(params, :content, "")
|
||||
|
||||
with validation <- script_validation(content),
|
||||
{:ok, script} <-
|
||||
Scripts.create_script(%{
|
||||
project_id: project.id,
|
||||
title: map_get(params, :title, ""),
|
||||
kind: parse_script_kind(map_get(params, :kind)),
|
||||
content: content,
|
||||
entrypoint: map_get(params, :entrypoint)
|
||||
}) do
|
||||
proposal =
|
||||
ProposalStore.create("propose_script", %{"script_id" => script.id},
|
||||
entity_id: script.id,
|
||||
ttl_ms: @proposal_ttl_app_ms
|
||||
)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
"proposal_id" => proposal.id,
|
||||
"script" => sanitize(script),
|
||||
"preview" => %{
|
||||
"title" => script.title,
|
||||
"kind" => Atom.to_string(script.kind),
|
||||
"content_length" => String.length(content),
|
||||
"syntax_valid" => validation.valid,
|
||||
"syntax_errors" => validation.errors
|
||||
}
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
defp propose_template(params) do
|
||||
project = active_project!()
|
||||
content = map_get(params, :content, "")
|
||||
{:ok, validation} = validate_template(content)
|
||||
|
||||
with {:ok, template} <-
|
||||
Templates.create_template(%{
|
||||
project_id: project.id,
|
||||
title: map_get(params, :title, ""),
|
||||
kind: parse_template_kind(map_get(params, :kind)),
|
||||
content: content
|
||||
}) do
|
||||
proposal =
|
||||
ProposalStore.create("propose_template", %{"template_id" => template.id},
|
||||
entity_id: template.id,
|
||||
ttl_ms: @proposal_ttl_app_ms
|
||||
)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
"proposal_id" => proposal.id,
|
||||
"template" => sanitize(template),
|
||||
"preview" => %{
|
||||
"title" => template.title,
|
||||
"kind" => Atom.to_string(template.kind),
|
||||
"content_length" => String.length(content),
|
||||
"syntax_valid" => validation.valid,
|
||||
"syntax_errors" => validation.errors
|
||||
}
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
defp propose_media_metadata(params) do
|
||||
media_id = map_get(params, :mediaId)
|
||||
|
||||
case Repo.get(MediaAsset, media_id) do
|
||||
nil -> {:error, :not_found}
|
||||
%MediaAsset{} = media ->
|
||||
changes =
|
||||
%{}
|
||||
|> maybe_put("title", map_get(params, :title))
|
||||
|> maybe_put("alt", map_get(params, :alt))
|
||||
|> maybe_put("caption", map_get(params, :caption))
|
||||
|> maybe_put("tags", map_get(params, :tags))
|
||||
|
||||
proposal =
|
||||
ProposalStore.create(
|
||||
"propose_media_metadata",
|
||||
%{"media_id" => media_id, "changes" => changes},
|
||||
entity_id: media_id,
|
||||
ttl_ms: @proposal_ttl_app_ms
|
||||
)
|
||||
|
||||
{:ok, %{"proposal_id" => proposal.id, "current" => sanitize(media), "proposed" => changes}}
|
||||
end
|
||||
end
|
||||
|
||||
defp propose_post_metadata(params) do
|
||||
post_id = map_get(params, :postId)
|
||||
|
||||
case Repo.get(Post, post_id) do
|
||||
nil -> {:error, :not_found}
|
||||
%Post{} = post ->
|
||||
changes =
|
||||
%{}
|
||||
|> maybe_put("title", map_get(params, :title))
|
||||
|> maybe_put("excerpt", map_get(params, :excerpt))
|
||||
|> maybe_put("tags", map_get(params, :tags))
|
||||
|> maybe_put("categories", map_get(params, :categories))
|
||||
|
||||
proposal =
|
||||
ProposalStore.create(
|
||||
"propose_post_metadata",
|
||||
%{"post_id" => post_id, "changes" => changes},
|
||||
entity_id: post_id,
|
||||
ttl_ms: @proposal_ttl_app_ms
|
||||
)
|
||||
|
||||
{:ok, %{"proposal_id" => proposal.id, "current" => sanitize(post), "proposed" => changes}}
|
||||
end
|
||||
end
|
||||
|
||||
defp accept_proposal(params) do
|
||||
proposal_id = map_get(params, :proposalId)
|
||||
|
||||
case ProposalStore.get(proposal_id) do
|
||||
nil -> {:error, :not_found}
|
||||
proposal ->
|
||||
result =
|
||||
case proposal.kind do
|
||||
"draft_post" ->
|
||||
proposal.data["post_id"] |> Posts.publish_post()
|
||||
|
||||
"propose_script" ->
|
||||
proposal.data["script_id"] |> Scripts.publish_script()
|
||||
|
||||
"propose_template" ->
|
||||
proposal.data["template_id"] |> Templates.publish_template()
|
||||
|
||||
"propose_media_metadata" ->
|
||||
Media.update_media(proposal.data["media_id"], proposal.data["changes"] || %{})
|
||||
|
||||
"propose_post_metadata" ->
|
||||
Posts.update_post(proposal.data["post_id"], proposal.data["changes"] || %{})
|
||||
|
||||
_other ->
|
||||
{:error, :unsupported_proposal}
|
||||
end
|
||||
|
||||
case result do
|
||||
{:ok, value} ->
|
||||
_ = ProposalStore.mark_accepted(proposal_id)
|
||||
{:ok, %{"success" => true, "message" => "accepted", "result" => sanitize(value)}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp discard_proposal(params) do
|
||||
proposal_id = map_get(params, :proposalId)
|
||||
|
||||
case ProposalStore.get(proposal_id) do
|
||||
nil -> {:error, :not_found}
|
||||
proposal ->
|
||||
result =
|
||||
case proposal.kind do
|
||||
"draft_post" -> Posts.delete_post(proposal.data["post_id"])
|
||||
"propose_script" -> Scripts.delete_script(proposal.data["script_id"])
|
||||
"propose_template" -> Templates.delete_template(proposal.data["template_id"])
|
||||
_other -> {:ok, :discarded}
|
||||
end
|
||||
|
||||
case result do
|
||||
{:ok, _value} ->
|
||||
_ = ProposalStore.mark_discarded(proposal_id)
|
||||
{:ok, %{"success" => true, "message" => "discarded"}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp posts_resource(offset) do
|
||||
project = active_project!()
|
||||
{:ok, result} = Search.search_posts(project.id, "", %{offset: offset, limit: @page_size})
|
||||
|
||||
%{
|
||||
"items" => Enum.map(result.posts, &post_summary/1),
|
||||
"total" => result.total,
|
||||
"offset" => result.offset,
|
||||
"limit" => result.limit,
|
||||
"has_more" => result.offset + result.limit < result.total
|
||||
}
|
||||
end
|
||||
|
||||
defp media_resource(offset) do
|
||||
project = active_project!()
|
||||
{:ok, result} = Search.search_media(project.id, "", %{offset: offset, limit: @page_size})
|
||||
|
||||
%{
|
||||
"items" => Enum.map(result.media, &sanitize/1),
|
||||
"total" => result.total,
|
||||
"offset" => result.offset,
|
||||
"limit" => result.limit,
|
||||
"has_more" => result.offset + result.limit < result.total
|
||||
}
|
||||
end
|
||||
|
||||
defp tags_resource do
|
||||
project = active_project!()
|
||||
tags = Tags.list_tags(project.id)
|
||||
|
||||
%{
|
||||
"items" =>
|
||||
Enum.map(tags, fn tag ->
|
||||
%{
|
||||
"id" => tag.id,
|
||||
"name" => tag.name,
|
||||
"color" => tag.color,
|
||||
"post_count" => tag_post_count(project.id, tag.name)
|
||||
}
|
||||
end)
|
||||
}
|
||||
end
|
||||
|
||||
defp categories_resource do
|
||||
project = active_project!()
|
||||
{:ok, metadata} = Metadata.get_project_metadata(project.id)
|
||||
|
||||
%{
|
||||
"items" =>
|
||||
Enum.map(metadata.categories, fn category ->
|
||||
%{"name" => category, "post_count" => category_post_count(project.id, category)}
|
||||
end)
|
||||
}
|
||||
end
|
||||
|
||||
defp read_post_resource(id) do
|
||||
case Repo.get(Post, id) do
|
||||
%Post{} = post -> {:ok, post_detail(post)}
|
||||
nil -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
defp read_media_resource(id) do
|
||||
case Repo.get(MediaAsset, id) do
|
||||
%MediaAsset{} = media -> {:ok, sanitize(media)}
|
||||
nil -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
defp post_detail(%Post{} = post) do
|
||||
post
|
||||
|> sanitize()
|
||||
|> Map.put("content", post_body(post))
|
||||
|> Map.put("backlinks", linked_posts(post.id, :incoming))
|
||||
|> Map.put("links_to", linked_posts(post.id, :outgoing))
|
||||
|> Map.put("available_languages", available_languages(post.id, post.language))
|
||||
end
|
||||
|
||||
defp translated_post_detail(%Post{} = post, language) do
|
||||
normalized_language = normalize_term(language)
|
||||
|
||||
case Repo.get_by(PostTranslation, translation_for: post.id, language: normalized_language) do
|
||||
nil -> post_detail(post)
|
||||
%PostTranslation{} = translation ->
|
||||
post_detail(post)
|
||||
|> Map.put("title", translation.title)
|
||||
|> Map.put("excerpt", translation.excerpt)
|
||||
|> Map.put("content", translation_body(translation))
|
||||
|> Map.put("language", translation.language)
|
||||
|> Map.put("canonical_language", post.language)
|
||||
end
|
||||
end
|
||||
|
||||
defp linked_posts(post_id, :incoming) do
|
||||
PostLinks.list_incoming_links(post_id)
|
||||
|> Enum.map(&load_linked_post(&1.source_post_id))
|
||||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
defp linked_posts(post_id, :outgoing) do
|
||||
PostLinks.list_outgoing_links(post_id)
|
||||
|> Enum.map(&load_linked_post(&1.target_post_id))
|
||||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
defp load_linked_post(post_id) do
|
||||
case Repo.get(Post, post_id) do
|
||||
%Post{} = post -> %{"id" => post.id, "title" => post.title, "slug" => post.slug}
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp post_summary(%Post{} = post) do
|
||||
%{
|
||||
"id" => post.id,
|
||||
"title" => post.title,
|
||||
"slug" => post.slug,
|
||||
"status" => Atom.to_string(post.status),
|
||||
"tags" => post.tags || [],
|
||||
"categories" => post.categories || [],
|
||||
"created_at" => post.created_at,
|
||||
"backlinks" => linked_posts(post.id, :incoming),
|
||||
"links_to" => linked_posts(post.id, :outgoing)
|
||||
}
|
||||
end
|
||||
|
||||
defp available_languages(post_id, canonical_language) do
|
||||
languages =
|
||||
Repo.all(
|
||||
from translation in PostTranslation,
|
||||
where: translation.translation_for == ^post_id,
|
||||
select: translation.language
|
||||
)
|
||||
|
||||
([canonical_language] ++ languages)
|
||||
|> Enum.reject(&(&1 in [nil, ""]))
|
||||
|> Enum.uniq()
|
||||
end
|
||||
|
||||
defp post_body(%Post{content: content}) when is_binary(content), do: content
|
||||
|
||||
defp post_body(%Post{} = post) do
|
||||
project = Projects.get_project!(post.project_id)
|
||||
full_path = Path.join(Projects.project_data_dir(project), post.file_path || "")
|
||||
|
||||
case File.read(full_path) do
|
||||
{:ok, contents} ->
|
||||
case String.split(contents, "\n---\n", parts: 2) do
|
||||
[_frontmatter, body] -> String.trim_trailing(body, "\n")
|
||||
_parts -> contents
|
||||
end
|
||||
|
||||
{:error, _reason} -> ""
|
||||
end
|
||||
end
|
||||
|
||||
defp translation_body(%PostTranslation{content: content}) when is_binary(content), do: content
|
||||
|
||||
defp translation_body(%PostTranslation{} = translation) do
|
||||
project = Projects.get_project!(translation.project_id)
|
||||
full_path = Path.join(Projects.project_data_dir(project), translation.file_path || "")
|
||||
|
||||
case File.read(full_path) do
|
||||
{:ok, contents} ->
|
||||
case String.split(contents, "\n---\n", parts: 2) do
|
||||
[_frontmatter, body] -> String.trim_trailing(body, "\n")
|
||||
_parts -> contents
|
||||
end
|
||||
|
||||
{:error, _reason} -> ""
|
||||
end
|
||||
end
|
||||
|
||||
defp tag_post_count(project_id, tag_name) do
|
||||
Repo.all(from post in Post, where: post.project_id == ^project_id)
|
||||
|> Enum.count(&(tag_name in (&1.tags || [])))
|
||||
end
|
||||
|
||||
defp category_post_count(project_id, category) do
|
||||
Repo.all(from post in Post, where: post.project_id == ^project_id)
|
||||
|> Enum.count(&(category in (&1.categories || [])))
|
||||
end
|
||||
|
||||
defp group_rows(_post, []), do: [%{}]
|
||||
|
||||
defp group_rows(post, [dimension | rest]) do
|
||||
values = group_values(post, dimension)
|
||||
|
||||
for value <- values,
|
||||
tail <- group_rows(post, rest) do
|
||||
Map.put(tail, dimension, value)
|
||||
end
|
||||
end
|
||||
|
||||
defp group_values(post, "year") do
|
||||
[BDS.Persistence.from_unix_ms!(post.created_at).year]
|
||||
end
|
||||
|
||||
defp group_values(post, "month") do
|
||||
[BDS.Persistence.from_unix_ms!(post.created_at).month]
|
||||
end
|
||||
|
||||
defp group_values(post, "tag") do
|
||||
if Enum.empty?(post.tags || []), do: [nil], else: post.tags
|
||||
end
|
||||
|
||||
defp group_values(post, "category") do
|
||||
if Enum.empty?(post.categories || []), do: [nil], else: post.categories
|
||||
end
|
||||
|
||||
defp group_values(post, "status"), do: [Atom.to_string(post.status)]
|
||||
|
||||
defp search_filters(params) do
|
||||
%{}
|
||||
|> maybe_put(:category, map_get(params, :category))
|
||||
|> maybe_put(:tags, map_get(params, :tags))
|
||||
|> maybe_put(:language, map_get(params, :language))
|
||||
|> maybe_put(:missing_translation_language, map_get(params, :missingTranslationLanguage))
|
||||
|> maybe_put(:year, map_get(params, :year))
|
||||
|> maybe_put(:month, map_get(params, :month))
|
||||
|> maybe_put(:status, parse_status(map_get(params, :status)))
|
||||
|> Map.put(:offset, map_get(params, :offset, 0))
|
||||
|> Map.put(:limit, map_get(params, :limit, @page_size))
|
||||
end
|
||||
|
||||
defp parse_status(nil), do: nil
|
||||
defp parse_status(status) when is_atom(status), do: status
|
||||
defp parse_status(status) when is_binary(status), do: String.to_existing_atom(status)
|
||||
|
||||
defp script_validation(content) do
|
||||
case BDS.Scripting.validate(content) do
|
||||
:ok -> %{valid: true, errors: []}
|
||||
{:error, reason} -> %{valid: false, errors: [inspect(reason)]}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_script_kind(value) when is_atom(value), do: value
|
||||
defp parse_script_kind("macro"), do: :macro
|
||||
defp parse_script_kind("utility"), do: :utility
|
||||
defp parse_script_kind("transform"), do: :transform
|
||||
|
||||
defp parse_template_kind(value) when is_atom(value), do: value
|
||||
defp parse_template_kind("post"), do: :post
|
||||
defp parse_template_kind("list"), do: :list
|
||||
defp parse_template_kind("not-found"), do: :not_found
|
||||
defp parse_template_kind("not_found"), do: :not_found
|
||||
defp parse_template_kind("partial"), do: :partial
|
||||
|
||||
defp sanitize(%_struct{} = struct) do
|
||||
struct
|
||||
|> Map.from_struct()
|
||||
|> Map.drop([:__meta__, :post, :project, :media])
|
||||
|> sanitize()
|
||||
end
|
||||
|
||||
defp sanitize(map) when is_map(map) do
|
||||
Map.new(map, fn {key, value} -> {to_string(key), sanitize(value)} end)
|
||||
end
|
||||
|
||||
defp sanitize(list) when is_list(list), do: Enum.map(list, &sanitize/1)
|
||||
defp sanitize(value) when is_atom(value), do: Atom.to_string(value)
|
||||
defp sanitize(value), do: value
|
||||
|
||||
defp normalize_term(nil), do: ""
|
||||
defp normalize_term(value), do: value |> to_string() |> String.downcase()
|
||||
|
||||
defp maybe_put(map, _key, nil), do: map
|
||||
defp maybe_put(map, key, value), do: Map.put(map, key, value)
|
||||
|
||||
defp map_get(map, key, default \\ nil) do
|
||||
cond do
|
||||
Map.has_key?(map, key) -> Map.get(map, key)
|
||||
Map.has_key?(map, Atom.to_string(key)) -> Map.get(map, Atom.to_string(key))
|
||||
true -> default
|
||||
end
|
||||
end
|
||||
defdelegate validate_template(source), to: Tools
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user