Skip to content

Commit

Permalink
Fixes #54
Browse files Browse the repository at this point in the history
  • Loading branch information
Semprini committed Jan 13, 2024
1 parent 0b560a5 commit 2a05826
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 47 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ A single logical model is rich enough to generate API schemas, DB schemas, POJOs

The tool parses your models into generic UML classes (see metamodel below) which are then passed to jinja2 templates for generation.

My current favorite generation recipie is Hasura for a GraphQL API and generating DB migrations via Django. See sample_recipies/sparxdb/config-sparxdb-graphql.yaml

Quickstart and docs can be found here: [readthedocs](https://pymdg.readthedocs.io/en/latest/index.html)

## Test
Expand Down
Empty file.
15 changes: 4 additions & 11 deletions mdg/templates/Django/app/models.py.jinja
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
from django.utils.translation import gettext_lazy as _
from django.db import models
from simple_history.models import HistoricalRecords

from {{ package.root_package.name | snakecase }}.validators import validate_even

{% for cls in package.classes %}{% if cls.generalization != None %}{% if cls.generalization.package != package %}
from {{ cls.generalization.package.name | case_package }}.models import {{ cls.generalization.name | case_class }}{% endif %}{% endif %}{% endfor %}
Expand All @@ -16,14 +13,13 @@ from {{ cls.generalization.package.name | case_package }}.models import {{ cls.g
{% if attr.classification %} {{ attr.name | snakecase }} = models.CharField( max_length=100, choices=ENUM_{{ attr.classification.name}}.choices, blank=True, null=True )
{% else %} {{ attr.name | snakecase }} = models.{% if attr.stereotype == "auto" %}AutoField{% else %}{{ attr.dest_type }}{% endif %}( {% if attr.is_id %}primary_key=True, {% else %}blank=True, null=True, {% endif %}{% if attr.length %}max_length={{ attr.length }}{% endif %} )
{% endif %}{% endfor %}
{% if cls.is_abstract %} class Meta:
class Meta:
abstract = True
{% endif %}{% endif %}
{% endfor %}
{% endif %}{% endfor %}

{% for cls in package.classes %}{% if not cls.is_abstract %}class {{ cls.name | case_class }}( {% if cls.generalization %}{{ cls.generalization.name }}{% else %}models.Model{% endif %} ):
{% for attr in cls.attributes %}
{% if attr.classification %} {{ attr.name | snakecase }} = models.CharField( max_length=100, choices=ENUM_{{ attr.classification.name}}.choices, blank=True, null=True )
{% if attr.classification %} {{ attr.name | snakecase }} = models.CharField( max_length=100, choices=ENUM_{{ attr.classification.name | case_class }}.choices, blank=True, null=True )
{% else %} {{ attr.name | snakecase }} = models.{% if attr.stereotype == "auto" %}AutoField{% else %}{{ attr.dest_type }}{% endif %}( {% if attr.is_id %}primary_key=True, {% else %}blank=True, null=True, {% endif %}{% if attr.dest_type == "DecimalField" %}max_digits=10, decimal_places=2, {% endif %}{% if attr.length %}max_length={{ attr.length }}{% endif %}{% if attr.validations != [] %}validators=[validate_even]{% endif %} )
{% endif %}{% endfor %}

Expand All @@ -42,7 +38,4 @@ from {{ cls.generalization.package.name | case_package }}.models import {{ cls.g
{% if 'auditable' in cls.stereotypes %}
history = HistoricalRecords(){% endif%}

{% if cls.is_abstract %} class Meta:
abstract = True
{% endif %}{% endif %}
{% endfor %}
{% endif %}{% endfor %}
44 changes: 32 additions & 12 deletions mdg/templates/hasura.json.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,52 @@
"version": 3,
"sources": [
{
"name": "default",
"name": "{{ package.name | snakecase }}",
"kind": "postgres",
"tables": [{% set classes = package.get_all_classes() %}{% for cls in classes %}{% if cls.is_abstract == False and cls.id_attribute %}
{ {% set objects = cls.get_object_relationships() %}{% set array_objects = cls.get_array_relationships() %}
"tables": [{% set classes = package.get_all_classes() %}{% for cls in classes %}{% if cls.is_abstract == False and cls.id_attribute %}{% set objects = cls.get_object_relationships() %}{% set array_objects = cls.get_array_relationships() %}
{% for obj in array_objects %}{% set assoc = cls.get_class_association(obj) %}{% if assoc.cardinality.name == "MANY_TO_MANY" and assoc.source == cls %}{
"table":{
"name": "{{ assoc.source.package.name | camelcase }}_{{ assoc.source.name | snakecase }}_{{ assoc.destination.name | snakecase }}s",
"schema": "public"
},
"object_relationships": [
{
"name": "{{ assoc.source.package.name | camelcase }}_{{ assoc.source.name | snakecase }}",
"using": {
"foreign_key_constraint_on": "{{ assoc.source.name | snakecase }}_{{ assoc.source.id_attribute.name | snakecase }}"
}
},
{
"name": "{{ assoc.destination.package.name | camelcase }}_{{ assoc.destination.name | snakecase }}",
"using": {
"foreign_key_constraint_on": "{{ assoc.destination.name | snakecase }}_{{ assoc.destination.id_attribute.name | snakecase }}"
}
}
]
},{% endif %}{% endfor %}
{
"table":{
"name": "{{ cls.package.name | snakecase }}__{{ cls.name | snakecase }}",
"name": "{{ cls.package.name | camelcase }}_{{ cls.name | snakecase }}",
"schema": "public"
}{% if objects|count > 0 %},
"object_relationships": [{% for obj in objects %}
{
"name": "obj_{{ obj.package.name | snakecase }}__{{ obj.name | snakecase }}",
"name": "obj_{{ obj.package.name | camelcase }}_{{ obj.name | snakecase }}",
"using": {
"foreign_key_constraint_on": "{{ obj.id_attribute.name | snakecase }}"
"foreign_key_constraint_on": "{{ package.name | snakecase }}_{{ obj.id_attribute.name | snakecase }}"
}
}{% if loop.index != objects|count %},{% endif %}{% endfor %}
]{% endif %}{% if array_objects|count > 0 %},
"array_relationships": [{% for obj in array_objects %}
{
"name": "list_{{ obj.package.name | snakecase }}__{{ obj.name | snakecase }}",
"name": "list_{{ obj.package.name | camelcase }}_{{ obj.name | snakecase }}",
"using": {
"foreign_key_constraint_on": {
"column": "{{ obj.id_attribute.name | snakecase }}",
"column": "{{ package.name | snakecase }}_{{ obj.id_attribute.name | snakecase }}",
"table": {
"name": "{{ obj.package.name | snakecase }}__{{ obj.name | snakecase }}",
{% set assoc = cls.get_class_association(obj) %}{% if assoc.cardinality.name == "MANY_TO_MANY" %}
"name": "{{ assoc.source.package.name | camelcase }}_{{ assoc.source.name | snakecase }}_{{ assoc.destination.name | snakecase }}s",{% else %}
"name": "{{ obj.package.name | camelcase }}_{{ obj.name | snakecase }}",{% endif %}
"schema": "public"
}
}
Expand All @@ -38,9 +60,7 @@
],
"configuration": {
"connection_info": {
"database_url": {
"from_env": "HASURA_GRAPHQL_METADATA_DATABASE_URL"
},
"database_url": "postgres://postgres:postgrespassword@postgres:5432/customer",
"isolation_level": "read-committed",
"use_prepared_statements": false
}
Expand Down
40 changes: 28 additions & 12 deletions mdg/uml/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,33 +317,44 @@ def __str__(self) -> str:
return f"{self.name}"

def get_name(self) -> str:
""" Returns the name or alias if the instace has one defined.
"""
if self.alias and settings['use_alias']:
return f"{self.alias}"
else:
return f"{self.name}"

def get_object_relationships(self) -> list:
""" Returns all related classes where this class is singular:
- associations from: one to one, one to many
- associations to: one to one, many to one
- generalization
def get_class_association(self, cls: UMLClass) -> Union[UMLAssociation, None]:
for assoc in self.associations_from:
if assoc.destination == cls:
return assoc
for assoc in self.associations_to:
if assoc.source == cls:
return assoc
return None

def get_array_relationships(self) -> list:
""" Returns all related classes where this class is part of an array:
- associations from: many to one
- associations to: one to many
"""
result = []
if self.generalization is not None:
result.append(self.generalization)
for assoc in self.associations_from:
if assoc.cardinality in [Cardinality.ONE_TO_MANY, Cardinality.ONE_TO_ONE]:
if assoc.cardinality in [Cardinality.ONE_TO_MANY, Cardinality.ONE_TO_ONE, Cardinality.MANY_TO_MANY]:
result.append(assoc.destination)
for assoc in self.associations_to:
if assoc.cardinality in [Cardinality.MANY_TO_ONE, Cardinality.ONE_TO_ONE]:
if assoc.cardinality in [Cardinality.MANY_TO_ONE, Cardinality.ONE_TO_ONE, Cardinality.MANY_TO_MANY]:
result.append(assoc.source)

return result

def get_array_relationships(self) -> list:
""" Returns all related classes where this class is part of an array:
- associations from: many to one
- associations to: one to many
def get_object_relationships(self) -> list:
""" Returns all related classes where this class is singular:
- associations from: one to one, one to many
- associations to: one to one, many to one
- generalization
"""
result = []
for assoc in self.associations_from:
Expand Down Expand Up @@ -414,14 +425,17 @@ def __str__(self) -> str:
return f"{self.name}"

def get_type(self, translator: Optional[str] = None) -> str:
""" Returns either the attribute type or a translated type if the dialect is specified in the args
"""
if not translator:
return f"{self.dest_type}"
if self.type in generation_fields[translator].keys():
return generation_fields[translator][f"{self.type}"]
return f"{self.type}"

def set_type(self, source_type: str):
# Allow setting of length or scale and precision either: <type> (<precision>,<scale>) or <type> (<length>)
""" Sets the type and allows setting of length or scale and precision either: <type> (<precision>,<scale>) or <type> (<length>)
"""
split: List[str] = source_type.split('(')
if len(split) > 1:
source_type = split[0].strip()
Expand All @@ -442,6 +456,8 @@ def set_type(self, source_type: str):
self.dest_type = source_type

def get_name(self) -> str:
""" Returns the name or alias if the instace has one defined.
"""
if self.alias and settings['use_alias']:
return f"{self.alias}"
else:
Expand Down
20 changes: 9 additions & 11 deletions sample_recipes/sparxdb/config-sparxdb-graphql.yaml
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
model_package: "{D6D3BF36-E897-4a8b-8CA9-62ADAAD696ED}"
test_package: "{C2E30D48-F8AB-4c6f-9FA3-AFE44653D5EB}"
model_package: "{AEB30CD7-DECA-4310-BFD6-9225F9251D9A}"
source: sqlite:///./sample_recipes/sparxdb/sample.qea
parser: sparxdb
dest_root: ./build/sample_sparxdb_graphql
templates_folder: ./mdg/templates
generation_type: default
generation_type: django
model_templates:
# Hasura Schema
- dest: "{{package.children[0].name | snakecase}}__metadata.json"
- dest: "{{package.name | camelcase}}-hasura_metadata.json"
level: root
source: "hasura.json.jinja"
# PostgreSQL Schema
- dest: "{{package.children[0].name | snakecase}}.sql"
level: root
source: "postgresql.sql.jinja"
test_templates:
- dest: "build/test/{{ins.package.path}}/{{ins.stereotype}}.json"
format: json

# Django app
- dest: "{{package.root_package.name | camelcase}}/{{package.name | camelcase}}/models.py"
level: package
source: "django/app/models.py.jinja"
filter: "{% if package.classes != [] %}True{% else %}False{% endif %}"
Binary file modified sample_recipes/sparxdb/sample.qea
Binary file not shown.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def find_packages(srcdir):

setuptools.setup(
name='pymdg',
version='0.9a2',
version='0.9a3',
author='Semprini',
author_email='[email protected]',
description='Model driven genration - from UML to Code & Docs',
Expand Down

0 comments on commit 2a05826

Please sign in to comment.