Current section
Files
Jump to
Current section
Files
lib/boxic/dmn/xml/writer.ex
defmodule Boxic.DMN.XML.Writer do
@moduledoc false
alias Boxic.DMN.Compatibility
alias Boxic.DMN.Model
alias Boxic.DMN.Model.{
Binding,
BusinessKnowledgeModel,
ConditionalExpression,
ContextEntry,
ContextExpression,
Decision,
DecisionRule,
DecisionService,
DecisionTable,
FilterExpression,
FunctionDefinition,
InformationRequirement,
InputClause,
InputData,
Invocation,
Import,
ItemComponent,
ItemDefinition,
IteratorExpression,
ListExpression,
LiteralExpression,
OutputClause,
Relation,
RelationColumn,
Variable
}
alias Boxic.DMN.SerializationError
alias Boxic.DMN.XML.{Element, Emitter, SerializationValidator}
@spec encode(Model.t(), keyword()) ::
{:ok, String.t()} | {:error, [SerializationError.t()]}
def encode(model, opts \\ []) do
with :ok <- SerializationValidator.validate(model, opts) do
try do
opts = Keyword.merge(default_options(), opts)
xml = model |> definitions_element() |> Emitter.emit(opts) |> IO.iodata_to_binary()
{:ok, xml}
rescue
error ->
{:error,
[
%SerializationError{
code: :xml_encoding_failed,
path: [],
message: "The normalized model could not be encoded as XML.",
details: %{exception: error.__struct__}
}
]}
end
end
end
@spec validate_serializable(Model.t(), keyword()) ::
:ok | {:error, [SerializationError.t()]}
def validate_serializable(model, opts \\ []),
do: SerializationValidator.validate(model, opts)
defp default_options, do: [format: :pretty, xml_declaration: true]
defp definitions_element(%Model{} = model) do
profile = Compatibility.pinned_profile()
definitions = model.definitions
element(
"definitions",
attrs([
{"xmlns", profile.model_namespace},
{"id", definitions.id},
{"name", definitions.name},
{"namespace", definitions.namespace},
{"expressionLanguage", definitions.expression_language},
{"typeLanguage", definitions.type_language}
]),
imports(model.imports) ++
sorted_values(model.item_definitions, &item_definition/1) ++
sorted_values(model.input_data, &input_data/1) ++
sorted_values(model.bkms, &business_knowledge_model/1) ++
sorted_values(model.decisions, &decision/1) ++
sorted_values(model.decision_services, &decision_service/1)
)
end
defp imports(imports) do
imports
|> Enum.sort_by(fn {namespace, import} -> {namespace || "", import.name || ""} end)
|> Enum.map(fn {_namespace, %Import{} = import} ->
element(
"import",
attrs([
{"name", import.name},
{"namespace", import.namespace},
{"locationURI", import.location_uri},
{"importType", import.import_type}
])
)
end)
end
defp item_definition(%ItemDefinition{} = item) do
element(
"itemDefinition",
attrs([
{"id", item.id},
{"name", item.name},
{"isCollection", boolean_attribute(item.is_collection)}
]),
optional_text_value("typeRef", item.type_ref) ++
optional_text_container("allowedValues", item.allowed_values) ++
Enum.map(item.components, &item_component/1)
)
end
defp item_component(%ItemComponent{} = item) do
element(
"itemComponent",
attrs([
{"id", item.id},
{"name", item.name},
{"isCollection", boolean_attribute(item.is_collection)}
]),
optional_text_value("typeRef", item.type_ref) ++
optional_text_container("allowedValues", item.allowed_values) ++
Enum.map(item.components, &item_component/1)
)
end
defp input_data(%InputData{} = input) do
element(
"inputData",
attrs([{"id", input.id}, {"name", input.name}]),
optional(input.variable, &variable/1)
)
end
defp decision(%Decision{} = decision) do
element(
"decision",
attrs([{"id", decision.id}, {"name", decision.name}]),
optional(decision.variable, &variable/1) ++
Enum.map(decision.requirements, &requirement/1) ++
optional(decision.expression, &expression/1)
)
end
defp business_knowledge_model(%BusinessKnowledgeModel{} = bkm) do
logic_children =
Enum.map(bkm.parameters, &formal_parameter/1) ++ optional(bkm.expression, &expression/1)
element(
"businessKnowledgeModel",
attrs([{"id", bkm.id}, {"name", bkm.name}]),
optional(bkm.variable, &variable/1) ++
Enum.map(bkm.requirements, &requirement/1) ++
[element("encapsulatedLogic", [], logic_children)]
)
end
defp decision_service(%DecisionService{} = service) do
children =
optional(service.variable, &variable/1) ++
Enum.map(service.output_decisions, &reference_element("outputDecision", &1)) ++
Enum.map(service.input_decisions, &reference_element("inputDecision", &1)) ++
Enum.map(service.input_data, &reference_element("inputData", &1))
element("decisionService", attrs([{"id", service.id}, {"name", service.name}]), children)
end
defp variable(%Variable{} = variable) do
element(
"variable",
attrs([{"id", variable.id}, {"name", variable.name}, {"typeRef", variable.type_ref}])
)
end
defp formal_parameter(%Variable{} = parameter) do
element(
"formalParameter",
attrs([{"id", parameter.id}, {"name", parameter.name}, {"typeRef", parameter.type_ref}])
)
end
defp requirement(%InformationRequirement{kind: :input_data, href: href}) do
element("informationRequirement", [], [reference_element("requiredInput", href)])
end
defp requirement(%InformationRequirement{kind: :decision, href: href}) do
element("informationRequirement", [], [reference_element("requiredDecision", href)])
end
defp requirement(%InformationRequirement{kind: :knowledge, href: href}) do
element("knowledgeRequirement", [], [reference_element("requiredKnowledge", href)])
end
defp requirement(%InformationRequirement{}) do
element("informationRequirement")
end
defp reference_element(name, href),
do: element(name, attrs([{"href", reference_href(href)}]))
defp reference_href(nil), do: nil
defp reference_href(href) when is_binary(href) do
if String.contains?(href, "#"), do: href, else: "#" <> href
end
defp expression(%LiteralExpression{} = expression) do
element(
"literalExpression",
attrs([
{"id", expression.id},
{"typeRef", expression.type_ref},
{"expressionLanguage", expression_language(expression.expression_language)}
]),
optional_text_element(expression.text)
)
end
defp expression(%DecisionTable{} = table), do: decision_table(table)
defp expression(%ContextExpression{} = context) do
element("context", attrs([{"id", context.id}]), Enum.map(context.entries, &context_entry/1))
end
defp expression(%Invocation{} = invocation) do
element(
"invocation",
attrs([{"id", invocation.id}, {"typeRef", invocation.type_ref}]),
optional(invocation.function, &expression/1) ++
Enum.map(invocation.bindings, &binding_element/1)
)
end
defp expression(%FunctionDefinition{} = function) do
element(
"functionDefinition",
attrs([{"id", function.id}]),
Enum.map(function.parameters, &formal_parameter/1) ++ optional(function.body, &expression/1)
)
end
defp expression(%Relation{} = relation) do
element(
"relation",
attrs([{"id", relation.id}]),
Enum.map(relation.columns, &relation_column/1) ++
Enum.map(relation.rows, fn row -> element("row", [], Enum.map(row, &expression/1)) end)
)
end
defp expression(%ListExpression{} = list) do
element("list", attrs([{"id", list.id}]), Enum.map(list.items, &expression/1))
end
defp expression(%ConditionalExpression{} = conditional) do
element(
"conditional",
attrs([{"id", conditional.id}]),
boxed_children([
{"if", conditional.condition},
{"then", conditional.then_branch},
{"else", conditional.else_branch}
])
)
end
defp expression(%FilterExpression{} = filter) do
element(
"filter",
attrs([{"id", filter.id}]),
boxed_children([{"in", filter.source}, {"match", filter.match}])
)
end
defp expression(%IteratorExpression{} = iterator) do
{name, body_name} =
case iterator.kind do
:for -> {"for", "return"}
:some -> {"some", "satisfies"}
:every -> {"every", "satisfies"}
end
element(
name,
attrs([{"id", iterator.id}, {"iteratorVariable", iterator_variable(iterator.variable)}]),
boxed_children([{"in", iterator.source}, {body_name, iterator.body}])
)
end
defp iterator_variable(%Variable{name: name}), do: name
defp iterator_variable(name) when is_binary(name), do: name
defp iterator_variable(nil), do: nil
defp boxed_children(pairs) do
Enum.map(pairs, fn {name, value} -> element(name, [], optional(value, &expression/1)) end)
end
defp context_entry(%ContextEntry{} = entry) do
element(
"contextEntry",
attrs([{"id", entry.id}]),
optional(entry.variable, &variable/1) ++ optional(entry.expression, &expression/1)
)
end
defp binding_element(%Binding{} = binding) do
element(
"binding",
[],
[element("parameter", attrs([{"name", binding.parameter}]))] ++
optional(binding.expression, &expression/1)
)
end
defp relation_column(%RelationColumn{} = column) do
element(
"column",
attrs([{"id", column.id}, {"name", column.name}, {"typeRef", column.type_ref}])
)
end
defp decision_table(%DecisionTable{} = table) do
element(
"decisionTable",
attrs([
{"id", table.id},
{"hitPolicy", emitted_hit_policy(table.hit_policy)},
{"aggregation", table.aggregation},
{"outputLabel", table.output_label}
]),
Enum.map(table.inputs, &input_clause/1) ++
Enum.map(table.outputs, &output_clause/1) ++ Enum.map(table.rules, &decision_rule/1)
)
end
defp input_clause(%InputClause{} = input) do
input_expression =
element(
"inputExpression",
attrs([{"typeRef", input.type_ref}]),
optional_text_element(input.expression)
)
element(
"input",
attrs([{"id", input.id}, {"label", input.label}]),
[input_expression] ++ optional_text_container("inputValues", input.allowed_values)
)
end
defp output_clause(%OutputClause{} = output) do
element(
"output",
attrs([
{"id", output.id},
{"name", output.name},
{"label", output.label},
{"typeRef", output.type_ref}
]),
optional_text_container("outputValues", output.allowed_values) ++
optional_text_container("defaultOutputEntry", output.default_output)
)
end
defp decision_rule(%DecisionRule{} = rule) do
element(
"rule",
attrs([{"id", rule.id}]),
Enum.map(rule.input_entries, &text_container("inputEntry", &1)) ++
Enum.map(rule.output_entries, &text_container("outputEntry", &1))
)
end
defp expression_language(nil), do: nil
defp expression_language("feel"), do: nil
defp expression_language(value), do: value
defp emitted_hit_policy(nil), do: nil
defp emitted_hit_policy("UNIQUE"), do: nil
defp emitted_hit_policy(value), do: String.replace(value, "_", " ")
defp boolean_attribute(true), do: "true"
defp boolean_attribute(false), do: nil
defp boolean_attribute(nil), do: nil
defp optional(nil, _fun), do: []
defp optional(value, fun), do: [fun.(value)]
defp optional_text_element(nil), do: []
defp optional_text_element(value), do: [element("text", [], [{:text, value}])]
defp optional_text_value(_name, nil), do: []
defp optional_text_value(name, value), do: [element(name, [], [{:text, value}])]
defp optional_text_container(_name, nil), do: []
defp optional_text_container(name, value), do: [text_container(name, value)]
defp text_container(name, value),
do: element(name, [], [element("text", [], [{:text, value}])])
defp attrs(values), do: Enum.reject(values, fn {_name, value} -> is_nil(value) end)
defp sorted_values(map, mapper) do
map
|> Enum.sort_by(fn {key, value} -> {value.id || "", key || ""} end)
|> Enum.map(fn {_key, value} -> mapper.(value) end)
end
defp element(name, attrs \\ [], children \\ []),
do: %Element{name: name, attrs: attrs, children: children}
end