Skip to content

Add --to-camel option to convert from snake_case to camelCase #35

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
54 changes: 39 additions & 15 deletions pydantic2ts/cli/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,14 @@ def clean_output_file(output_filename: str) -> None:
with open(output_filename, "r") as f:
lines = f.readlines()

start, end = None, None
for i, line in enumerate(lines):
if line.rstrip("\r\n") == "export interface _Master_ {":
start = i
elif (start is not None) and line.rstrip("\r\n") == "}":
end = i
break
start, end = 0, 0
if len(lines) > 1:
for i, line in enumerate(lines):
if line.rstrip("\r\n") == "export interface _Master_ {":
start = i
elif (start is not None) and line.rstrip("\r\n") == "}":
end = i
break

banner_comment_lines = [
"/* tslint:disable */\n",
Expand All @@ -118,13 +119,16 @@ def clean_output_file(output_filename: str) -> None:
"*/\n\n",
]

new_lines = banner_comment_lines + lines[:start] + lines[(end + 1) :]
try:
new_lines = banner_comment_lines + lines[:start] + lines[(end + 1) :]
except TypeError as err:
raise TypeError(f"{err}: output_filename: {output_filename}; {lines}")

with open(output_filename, "w") as f:
f.writelines(new_lines)


def clean_schema(schema: Dict[str, Any]) -> None:
def clean_schema(schema: Dict[str, Any], to_camel: bool) -> None:
"""
Clean up the resulting JSON schemas by:

Expand All @@ -134,14 +138,25 @@ def clean_schema(schema: Dict[str, Any]) -> None:
2) Getting rid of the useless "An enumeration." description applied to Enums
which don't have a docstring.
"""
for prop in schema.get("properties", {}).values():
prop.pop("title", None)
update_props = {}
for name, value in schema.get("properties", {}).items():
value.pop("title", None)
if to_camel and ("_" in name):
name = "".join(
[
word.capitalize() if i != 0 else word
for i, word in enumerate(name.split("_"))
]
)
update_props[name] = value

schema["properties"] = update_props

if "enum" in schema and schema.get("description") == "An enumeration.":
del schema["description"]


def generate_json_schema(models: List[Type[BaseModel]]) -> str:
def generate_json_schema(models: List[Type[BaseModel]], to_camel: bool) -> str:
"""
Create a top-level '_Master_' model with references to each of the actual models.
Generate the schema for this model, which will include the schemas for all the
Expand All @@ -168,7 +183,7 @@ def generate_json_schema(models: List[Type[BaseModel]]) -> str:
schema = json.loads(master_model.schema_json())

for d in schema.get("definitions", {}).values():
clean_schema(d)
clean_schema(d, to_camel)

return json.dumps(schema, indent=2)

Expand All @@ -179,7 +194,11 @@ def generate_json_schema(models: List[Type[BaseModel]]) -> str:


def generate_typescript_defs(
module: str, output: str, exclude: Tuple[str] = (), json2ts_cmd: str = "json2ts"
module: str,
output: str,
exclude: Tuple[str] = (),
to_camel: bool = False,
json2ts_cmd: str = "json2ts",
) -> None:
"""
Convert the pydantic models in a python module into typescript interfaces.
Expand All @@ -205,7 +224,7 @@ def generate_typescript_defs(

logger.info("Generating JSON schema from pydantic models...")

schema = generate_json_schema(models)
schema = generate_json_schema(models, to_camel)
schema_dir = mkdtemp()
schema_file_path = os.path.join(schema_dir, "schema.json")

Expand Down Expand Up @@ -254,6 +273,11 @@ def parse_cli_args(args: List[str] = None) -> argparse.Namespace:
help="name of a pydantic model which should be omitted from the results.\n"
"This option can be defined multiple times.",
)
parser.add_argument(
"--to-camel",
action="store_true",
help="flag to convert model field names from snake_case to CamelCase.",
)
parser.add_argument(
"--json2ts-cmd",
dest="json2ts_cmd",
Expand Down
20 changes: 20 additions & 0 deletions tests/expected_results/single_module_to_camel/input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from pydantic import BaseModel
from typing import Optional, List


class LoginCredentials(BaseModel):
username: str
password: str


class Profile(BaseModel):
username: str
age: Optional[int]
hobbies: List[str]
programming_languages: List[str]


class LoginResponseData(BaseModel):
token: str
profile: Profile
session_data: Dict[str, Any]
22 changes: 22 additions & 0 deletions tests/expected_results/single_module_to_camel/output.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* tslint:disable */
/* eslint-disable */
/**
/* This file was automatically generated from pydantic models by running pydantic2ts.
/* Do not modify it by hand - just update the pydantic models and then re-run the script
*/

export interface LoginCredentials {
username: string;
password: string;
}
export interface LoginResponseData {
token: string;
profile: Profile;
sessionData: {[key:string]: unknown}
}
export interface Profile {
username: string;
age?: number;
hobbies: string[];
programmingLanguages: string[];
}
4 changes: 4 additions & 0 deletions tests/test_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ def test_single_module(tmpdir):
run_test(tmpdir, "single_module")


def test_single_module_to_camel(tmpdir):
run_test(tmpdir, "single_module_to_camel")


@pytest.mark.skipif(
sys.version_info < (3, 8),
reason="Literal requires python 3.8 or higher (Ref.: PEP 586)",
Expand Down