Current section
Files
Jump to
Current section
Files
lib/abstraction.ex
defmodule Paradigm.Abstraction do
@moduledoc """
Functions for moving between Paradigm structs and corresponding graph data.
"""
alias Paradigm.Graph.Node
alias Paradigm.Graph.Node.Ref
@doc """
Takes any `Paradigm` and produces the corresponding graph that is invariant against the Metamodel paradigm.
"""
def embed(paradigm, graph \\ nil) do
graph =
graph || Paradigm.Graph.MapGraph.new(name: paradigm.name, description: paradigm.description)
graph
|> add_primitive_types(paradigm)
|> add_packages(paradigm)
|> add_classes(paradigm)
|> add_enumerations(paradigm)
|> add_properties(paradigm)
end
defp add_primitive_types(graph, paradigm) do
Enum.reduce(paradigm.primitive_types, graph, fn {id, primitive_type}, acc ->
data = %{
"name" => primitive_type.name
}
node = %Node{
id: id,
class: "primitive_type",
data: data
}
Paradigm.Graph.insert_node(acc, node)
end)
end
defp add_packages(graph, paradigm) do
Enum.reduce(paradigm.packages, graph, fn {id, package}, acc ->
data = %{
"name" => package.name,
"uri" => package.uri,
"nested_packages" =>
Enum.map(package.nested_packages || [], &%Ref{id: &1, composite: true}),
"owned_types" => Enum.map(package.owned_types || [], &%Ref{id: &1, composite: true})
}
node = %Node{
id: id,
class: "package",
data: data
}
Paradigm.Graph.insert_node(acc, node)
end)
end
defp add_classes(graph, paradigm) do
Enum.reduce(paradigm.classes, graph, fn {id, class}, acc ->
# Convert properties map to owned_attributes list for graph representation, sorted by position
owned_attributes =
class.properties
|> Map.values()
|> Enum.sort_by(& &1.position)
|> Enum.map(&%Ref{id: "#{id}_#{&1.name}", composite: true})
data = %{
"name" => class.name,
"is_abstract" => class.is_abstract,
"owned_attributes" => owned_attributes,
"super_classes" => Enum.map(class.super_classes || [], &%Ref{id: &1})
}
owner_package_id =
Enum.find_value(paradigm.packages, fn {package_id, package} ->
if Enum.member?(package.owned_types || [], id) do
package_id
else
nil
end
end)
node = %Node{
id: id,
class: "class",
data: data,
owned_by: owner_package_id
}
Paradigm.Graph.insert_node(acc, node)
end)
end
defp add_enumerations(graph, paradigm) do
Enum.reduce(paradigm.enumerations, graph, fn {id, enum}, acc ->
data = %{
"name" => enum.name,
"literals" => enum.literals
}
node = %Node{
id: id,
class: "enumeration",
data: data
}
Paradigm.Graph.insert_node(acc, node)
end)
end
defp add_properties(graph, paradigm) do
Enum.reduce(paradigm.classes, graph, fn {class_id, class}, acc ->
Enum.reduce(class.properties, acc, fn {property_name, property}, inner_acc ->
property_id = "#{class_id}_#{property_name}"
data = %{
"name" => property.name,
"is_ordered" => property.is_ordered,
"type" =>
if(property.type,
do: %Ref{id: property.type, composite: property.is_composite},
else: nil
),
"is_composite" => property.is_composite,
"lower_bound" => property.lower_bound,
"upper_bound" => property.upper_bound,
"default_value" => property.default_value,
"position" => property.position
}
node = %Node{
id: property_id,
class: "property",
data: data,
owned_by: class_id
}
Paradigm.Graph.insert_node(inner_acc, node)
end)
end)
end
@doc """
Takes a graph of metamodel type, and returns a top-level `Paradigm` object.
"""
def extract(graph) do
primitive_types =
Paradigm.Graph.get_all_nodes_of_class(graph, "primitive_type")
|> Enum.map(fn id -> {id, Paradigm.Graph.get_node(graph, id)} end)
|> Enum.map(fn {id, node} ->
{id,
%Paradigm.PrimitiveType{
name: node.data["name"]
}}
end)
|> Map.new()
packages =
Paradigm.Graph.get_all_nodes_of_class(graph, "package")
|> Enum.map(fn id -> {id, Paradigm.Graph.get_node(graph, id)} end)
|> Enum.map(fn {id, node} ->
{id,
%Paradigm.Package{
name: node.data["name"],
uri: node.data["uri"],
nested_packages: extract_ref_ids(node.data["nested_packages"]),
owned_types: extract_ref_ids(node.data["owned_types"])
}}
end)
|> Map.new()
# First get all properties to build the property map
all_properties =
Paradigm.Graph.get_all_nodes_of_class(graph, "property")
|> Enum.map(fn id -> {id, Paradigm.Graph.get_node(graph, id)} end)
|> Enum.map(fn {id, node} ->
{id,
%Paradigm.Property{
name: node.data["name"],
is_ordered: node.data["is_ordered"],
type: extract_ref_id(node.data["type"]),
is_composite: node.data["is_composite"],
lower_bound: node.data["lower_bound"],
upper_bound: node.data["upper_bound"],
default_value: node.data["default_value"],
position: node.data["position"] || 0
}}
end)
|> Map.new()
classes =
Paradigm.Graph.get_all_nodes_of_class(graph, "class")
|> Enum.map(fn id -> {id, Paradigm.Graph.get_node(graph, id)} end)
|> Enum.map(fn {id, node} ->
# Get properties for this class from owned_attributes, preserving order
property_ids = extract_ref_ids(node.data["owned_attributes"])
properties =
property_ids
|> Enum.with_index()
|> Enum.map(fn {prop_id, index} ->
case Map.get(all_properties, prop_id) do
nil ->
nil
property ->
# If position wasn't set in the original property, use the index from owned_attributes
updated_property = %{property | position: property.position || index}
{property.name, updated_property}
end
end)
|> Enum.filter(&(&1 != nil))
|> Map.new()
{id,
%Paradigm.Class{
name: node.data["name"],
is_abstract: node.data["is_abstract"],
properties: properties,
super_classes: extract_ref_ids(node.data["super_classes"])
}}
end)
|> Map.new()
enumerations =
Paradigm.Graph.get_all_nodes_of_class(graph, "enumeration")
|> Enum.map(fn id -> {id, Paradigm.Graph.get_node(graph, id)} end)
|> Enum.map(fn {id, node} ->
{id,
%Paradigm.Enumeration{
name: node.data["name"],
literals: node.data["literals"]
}}
end)
|> Map.new()
%Paradigm{
name: Paradigm.Graph.get_name(graph),
description: Paradigm.Graph.get_description(graph),
primitive_types: primitive_types,
packages: packages,
classes: classes,
enumerations: enumerations
}
end
defp extract_ref_ids(nil), do: []
defp extract_ref_ids(refs) when is_list(refs) do
Enum.map(refs, fn
%Ref{id: id} -> id
id when is_binary(id) -> id
end)
end
defp extract_ref_id(nil), do: nil
defp extract_ref_id(%Ref{id: id}), do: id
defp extract_ref_id(id) when is_binary(id), do: id
end