Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat format decorator #38

Merged
merged 4 commits into from
Apr 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 44 additions & 3 deletions django_typomatic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .mappings import mappings

from rest_framework import serializers
from rest_framework.serializers import BaseSerializer
from rest_framework.fields import empty

from django.db.models.enums import Choices
Expand Down Expand Up @@ -81,6 +82,14 @@ def decorator(cls):
return decorator


def ts_format(format):
def decorator(f):
f.format = format
return f

return decorator


def __get_trimmed_name(name, trim_serializer_output):
key = "Serializer"
return name[:-len(key)] if trim_serializer_output and name.endswith(key) else name
Expand Down Expand Up @@ -137,7 +146,7 @@ def __map_choices_to_enum_values(enum_name, field_type, choices):


def __process_field(field_name, field, context, serializer, trim_serializer_output, camelize,
enum_choices, enum_values):
enum_choices, enum_values, annotations):
'''
Generates and returns a tuple representing the Typescript field name and Type.
'''
Expand Down Expand Up @@ -175,6 +184,7 @@ def __process_field(field_name, field, context, serializer, trim_serializer_outp
field_function = getattr(serializer, f'get_{field_name}')
return_type = get_type_hints(field_function).get('return')
is_generic_type = hasattr(return_type, '__origin__')
is_serializer_type = False
many = False

# TODO type pass recursively to represent something like a list from a list e.g. List[List[int]]
Expand All @@ -195,12 +205,40 @@ def __process_field(field_name, field, context, serializer, trim_serializer_outp
field_name, field_type, return_type, enum_choices, enum_values, many
)
types.append(ts_type)
elif return_type:
ts_type, ts_enum, ts_enum_value = __process_method_field(
field_name, field_type, return_type, enum_choices, enum_values, many
)

if isinstance(return_type, BaseSerializer):
many = return_type.many
return_type = return_type.child.__class__

if issubclass(return_type, BaseSerializer):
is_external_serializer = not return_type.__module__.replace('.serializers', '') == context
is_serializer_type = True

if is_external_serializer and return_type not in __serializers.get(context, []):
# TODO import external interface, not duplicate
# Include external Interface
ts_interface(context=context)(return_type)
# For duplicate interface, set not exported
setattr(return_type, '__exported__', False)

if is_serializer_type:
ts_type = __get_trimmed_name(return_type.__name__, trim_serializer_output)
is_many = many

types.append(ts_type)
else:
ts_type, ts_enum, ts_enum_value = __process_method_field(
field_name, field_type, return_type, enum_choices, enum_values, many
)
types.append(ts_type)

if hasattr(field_function, 'format'):
field.format = field_function.format

# Clear duplicate types
types = list(dict.fromkeys(types))
ts_type = " | ".join(types)
Expand Down Expand Up @@ -283,7 +321,7 @@ def __get_ts_interface_and_enums(serializer, context, trim_serializer_output, ca
enums = []
for key, value in fields:
ts_property, ts_type, ts_enum, ts_enum_value = __process_field(
key, value, context, serializer, trim_serializer_output, camelize, enum_choices, enum_values)
key, value, context, serializer, trim_serializer_output, camelize, enum_choices, enum_values, annotations)

if ts_enum_value is not None:
enums.append(ts_enum_value)
Expand All @@ -304,7 +342,8 @@ def __get_ts_interface_and_enums(serializer, context, trim_serializer_output, ca

ts_fields.append(f" {ts_property}: {ts_type};")
collapsed_fields = '\n'.join(ts_fields)
return f'export interface {name} {{\n{collapsed_fields}\n}}\n\n', enums
exported = getattr(serializer, '__exported__', True)
return f'{"export " if exported else ""}interface {name} {{\n{collapsed_fields}\n}}\n\n', enums


def __generate_interfaces_and_enums(context, trim_serializer_output, camelize, enum_choices, enum_values, annotations):
Expand Down Expand Up @@ -361,6 +400,8 @@ def __get_annotations(field, ts_type):

if field_type in format_mappings:
annotations.append(f' * @format {format_mappings[field_type]}')
elif hasattr(field, 'format'):
annotations.append(f' * @format {field.format}')

annotations.append(' */')

Expand Down
20 changes: 20 additions & 0 deletions django_typomatic/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from typing import Literal

FORMATS = Literal[
'email',
'url',
'uuid',
'date-time',
'date',
'time',
'double',
]

def ts_format(format: FORMATS): ...
def ts_field(ts_type: str, context='default'): ...
def ts_interface(context='default', mapping_overrides=None): ...
def generate_ts(output_path, context='default', trim_serializer_output=False, camelize=False,
enum_choices=False, enum_values=False, annotations=False): ...
def get_ts(context='default', trim_serializer_output=False, camelize=False, enum_choices=False, enum_values=False,
annotations=False): ...

30 changes: 26 additions & 4 deletions django_typomatic/test__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import io
import random
from typing import List

import pytest
from rest_framework import serializers
from django.db import models
from unittest.mock import patch, mock_open, MagicMock
from . import ts_interface, generate_ts, get_ts
from . import ts_interface, get_ts, ts_format


@ts_interface(context='internal')
Expand Down Expand Up @@ -45,6 +42,11 @@ class OtherSerializer(serializers.Serializer):
url_field = serializers.URLField(default='https://google.com')
float_field = serializers.FloatField()
empty_annotation = serializers.CharField()
custom_format = serializers.SerializerMethodField()

@ts_format('email')
def get_custom_format(self, instance) -> str:
return 'test@email.com'


class ActionType(models.TextChoices):
Expand Down Expand Up @@ -77,6 +79,12 @@ class FileSerializer(serializers.Serializer):
file = serializers.FileField()


@ts_interface('methodFields')
class MethodFieldsNestedSerializer(serializers.Serializer):
name = serializers.CharField(max_length=15)
description = serializers.CharField(max_length=100)


@ts_interface('methodFields')
class MethodFieldsSerializer(serializers.Serializer):
integer_field = serializers.SerializerMethodField()
Expand All @@ -85,6 +93,7 @@ class MethodFieldsSerializer(serializers.Serializer):
choice_field = serializers.SerializerMethodField()
multiple_return = serializers.SerializerMethodField()
various_type_return = serializers.SerializerMethodField()
serializer_type_return = serializers.SerializerMethodField()

def get_integer_field(self) -> int:
return 5
Expand All @@ -104,6 +113,9 @@ def get_multiple_return(self) -> List[int]:
def get_various_type_return(self) -> [int, str]:
return random.choice([1, 'test'])

def get_serializer_type_return(self) -> MethodFieldsNestedSerializer:
return MethodFieldsNestedSerializer(name='test', description='Test')


def test_get_ts():
expected = """export interface FooSerializer {
Expand Down Expand Up @@ -267,6 +279,10 @@ def test_annotations():
*/
float_field: number;
empty_annotation: string;
/**
* @format email
*/
custom_format?: string;
}

"""
Expand Down Expand Up @@ -311,13 +327,19 @@ def test_method_fields_serializer():
}


export interface MethodFieldsNestedSerializer {
name: string;
description: string;
}

export interface MethodFieldsSerializer {
integer_field?: number;
string_field?: string;
float_field?: number;
choice_field?: ChoiceFieldChoiceEnum;
multiple_return?: number[];
various_type_return?: number | string;
serializer_type_return?: MethodFieldsNestedSerializer;
}

"""
Expand Down