Paradigm is a model management framework supporting uniform treatment of heterogeneous resources as graph data with conformance and transformation relationships. It provides mathematically-grounded primitives supporting protocol interoperability, model-to-code generation, and rapid integration.
Your Universe of discourse is bootstrapped from a built-in metamodel which conforms to itself. Different Paradigms are introduced as models for objects under scrutiny, such as filesystem contents or data models of a particular format. Graph data is decoupled from its physical form by the graph protocol. So you can have (for example) an XML file specifying the conformance of filesystem objects, or vice versa.
New levels of abstraction are created by introducing transforms at the metamodel level. For example, a schema starts as data of its metamodel, but becomes a model itself in some (hopefully obvious) way. Then we can work with data that conforms to the schema. Some illustrative demos are available at paradigmpro.live.
Paradigm- Top-level data model containerParadigm.Package- Namespace organizationParadigm.Class- Entity definitionsParadigm.Property- Typed attributes and referencesParadigm.PrimitiveType- Basic data typesParadigm.Enumeration- Constrained sets
Paradigm.GraphProtocol - A set of functions for accessing graph nodesParadigm.Graph.Node- Standardized form for individual entity instancesParadigm.Graph.MapGraph- An in-memory graph implementationParadigm.Graph.FilesystemGraph- Provides folder and file nodes from local storageParadigm.Graph.Canonical- Provides methods for switching between Elixir structs and Graphs.
For these examples we'll use the provided Metamodel paradigm:
metamodel_paradigm = Paradigm.Builtin.Metamodel.definition()Paradigm.Abstraction allows movement between paradigm definitions and their graph representations.
- embed - Embeds a
Paradigmstruct into aParadigm.Graph(an empty MapGraph by default) - extract - Reconstructs a Paradigm struct from metamodel-conformant graph data
Any valid Paradigm struct should round-trip:
embedded_metamodel = Paradigm.Abstraction.embed(metamodel_paradigm)
Paradigm.Abstraction.extract(embedded_metamodel) == metamodel_paradigmParadigm.Conformance.check_graph/2 validates that graph data conforms to its paradigm definition. The conformance checker ensures data integrity by validating:
- Class validity - All nodes reference defined classes
- Property completeness - Required properties are present, unknown properties flagged
- Cardinality constraints - List/single value requirements met
- Reference integrity - All references point to existing nodes of correct classes
- Enumeration values - Values match defined enum options
The embedded metamodel validates against itself:
Paradigm.Conformance.check_graph(embedded_metamodel, metamodel_paradigm)Or if 2 graph objects are passed, the module will attempt to use the Abstraction module to produce a paradigm from the 2nd one:
Paradigm.Conformance.check_graph(embedded_metamodel, embedded_metamodel)The Paradigm.Transform protocol defines how transforms are handled.
They are invoked with transform(transformer, source, target, opts).
- transformer implements the transform protocol
- source is a graph
- target is a graph (not necessarily different or empty, just where new nodes will be added)
- opts allows configuration.
A simple helper function handles the configuration-free transform case holding the results in memory:
def transform(transformer, source) do
target = Paradigm.Graph.MapGraph.new()
Paradigm.Transform.transform(transformer, source, target, [])
endThe transform protocol is implemented for Function in the obvious way so that anonymous functions may be used. Here's a simple injection function:
fn source, target ->
{:ok,
Paradigm.Graph.stream_all_nodes(source)
|> Enum.reduce(target, fn node, acc_target ->
Paradigm.Graph.insert_node(acc_target, node)
end)
}
endParadigm.Transform.ClassBasedTransform encapsulates a common pattern:
- Select all nodes of a given type
- For each one, produce 1 or more resulting nodes
- Reduce across the target graph, inserting them all We can get rid of a lot of repeated code with a builder pattern:
import Paradigm.Transform.ClassBasedTransform
new()
|> with_default(fn node -> node end) # Copy all by default
|> rename_class("class1", "class2") # A simple rename helper
|> for_class("strange_type",
fn node ->
%{node | data: %{}} # Copy over with blanked data
end)
|> for_class("multi_type", # Return a list of nodes
fn node ->
[
%Node{id: node.id <> "_1", ...},
%Node{id: node.id <> "_2", ...}
]
end)
|> for_class("insufficient_context_type",
fn node, graph -> # Function can take 2 args
#Pull in additional information to build the node
end
)Here you can see the flexibility, as the class-based transform function has access to the node and the full graph, and returns an arbitrary list of nodes.
Paradigm.Transform.PipelineTransform allows transforms to be composed arbitrarily.
PipelineTransform.new([transform1, transform2, transform3])Note that intermediate steps automatically target a MapGraph.new().
This means memory should be considered, and "cumulative" effects need to be explicitly carried forward by each step.
The Paradigm.Builtin.Universe paradigm is a system-level model treating Paradigm.Graph and Paradigm.Transform objects as primitive types. The Paradigm.Universe module provides helper functions for working with Universe graphs, including content-addressed (inner) graphs.
Paradigm.Universe.bootstrap/0sets up the builtin metamodel self-realization relationship.Paradigm.Universe.apply_propagate/1applies a propagation transform that looks for places to apply conformance checks or internal transforms.
The result is all the embedding, conformance checking and transforms above are achieved more ergonomically internal to a Universe-conformant graph:
Paradigm.Universe.bootstrap()
|> Paradigm.Universe.register_transform_by_name(Paradigm.Transform.Identity, "Metamodel", "Metamodel")
|> Paradigm.Universe.apply_propagate()
|> Paradigm.Conformance.conforms?(Paradigm.Builtin.Universe.definition())If available in Hex, add paradigm to your list of dependencies in mix.exs:
def deps do
[
{:paradigm, "~> 0.3.0"}
]
endOr install directly from GitHub:
def deps do
[
{:paradigm, github: "ParadigmaticSystems/paradigm"}
]
endThen run:
mix deps.getHere's a basic example using the builtin metamodel:
# Get the metamodel paradigm
paradigm = Paradigm.Builtin.Metamodel.definition()
# Embed it into a graph for manipulation
graph = Paradigm.Abstraction.embed(paradigm)
# Validate that the embedded graph conforms to the metamodel
Paradigm.Conformance.check_graph(graph, paradigm)
# => %Paradigm.Conformance.Result{issues: []}
# Extract back to a Paradigm struct
extracted_paradigm = Paradigm.Abstraction.extract(graph)
# extracted_paradigm == paradigm