defmodule Paddle.SchemaParser do
@moduledoc ~S"""
Module used to parse *.schema files and generate Paddle classes.
"""
require Logger
for file <- Paddle.config(:schema_files) do
@external_resource file
end
@definitions Paddle.config(:schema_files)
|> Enum.flat_map(fn file ->
Logger.info("Loading #{file}")
{:ok, lexed, _num} =
file
|> File.read!()
|> String.to_charlist()
|> :schema_lexer.string()
{:ok, ast} = :schema_parser.parse(lexed)
ast
end)
@object_definitions Enum.filter(
@definitions,
fn {type, _attrs} -> type == :object_class end
)
@attribute_definitions Enum.filter_map(
@definitions,
fn {type, _attrs} -> type == :attribute_type end,
fn {:attribute_type, attrs} -> Keyword.get(attrs, :name) end
) ++
[["uid", "userid"]]
@spec attributes(binary | [binary]) :: [atom]
@doc ~S"""
Get the attributes names of an object class or a list of object classes.
Example:
iex> Paddle.SchemaParser.attributes "account"
[:description, :seeAlso, :l, :o, :ou, :host, :uid]
iex> Paddle.SchemaParser.attributes ["posixAccount", "account"]
[:userPassword, :loginShell, :gecos, :description, :cn, :uid, :uidNumber,
:gidNumber, :homeDirectory, :seeAlso, :l, :o, :ou, :host]
"""
def attributes(object_classes) do
@object_definitions
|> filter_definitions(object_classes)
|> Enum.flat_map(&attributes_from/1)
|> Enum.map(&replace_alias/1)
|> Enum.map(&String.to_atom/1)
|> Enum.uniq()
end
defp attributes_from({:object_class, description}) do
mays(description) ++ musts(description)
end
@spec required_attributes(binary | [binary]) :: [atom]
@doc ~S"""
Get the required attributes names of an object class or a list of object
classes.
Example:
iex> Paddle.SchemaParser.required_attributes "account"
[:uid]
iex> Paddle.SchemaParser.required_attributes ["posixAccount", "account"]
[:cn, :uid, :uidNumber, :gidNumber, :homeDirectory]
"""
def required_attributes(object_classes) do
@object_definitions
|> filter_definitions(object_classes)
|> Enum.flat_map(&required_attributes_from/1)
|> Enum.map(&replace_alias/1)
|> Enum.map(&String.to_atom/1)
|> Enum.uniq()
end
defp required_attributes_from({:object_class, description}) do
musts(description)
end
defp mays(description) do
description
|> Keyword.get(:may, [])
end
defp musts(description) do
description
|> Keyword.get(:must, [])
end
defp filter_definitions(definitions, object_class) when is_binary(object_class) do
filter_definitions(definitions, [object_class])
end
defp filter_definitions(definitions, object_classes) when is_list(object_classes) do
object_classes =
object_classes
|> Enum.map(fn class -> {class, :notfound} end)
|> Enum.into(%{})
filter_definitions(definitions, object_classes, [])
end
defp filter_definitions([], object_classes, filtered) when is_map(object_classes) do
not_found =
object_classes
|> Enum.filter_map(
fn {_class, status} -> status == :notfound end,
fn {class, _status} -> class end
)
case not_found do
[] -> filtered
_ -> raise "Missing object classe(s) definition(s): " <> Enum.join(not_found, ", ")
end
end
defp filter_definitions([{:object_class, attrs} = class | rest], object_classes, filtered)
when is_map(object_classes) do
name = attrs |> Keyword.get(:name) |> hd
if Map.has_key?(object_classes, name) do
if object_classes[name] == :notfound do
filter_definitions(rest, Map.put(object_classes, name, :found), [class | filtered])
else
IO.warn("Multiple definitions of the \"#{name}\" object class", [])
filter_definitions(rest, object_classes, filtered)
end
else
filter_definitions(rest, object_classes, filtered)
end
end
defp replace_alias(field) do
Enum.find_value(@attribute_definitions, fn aliases ->
if field in aliases, do: hd(aliases)
end) || field
end
end