diff --git a/rendercv/data_model.py b/rendercv/data_model.py index c2dcd75..a8003d3 100644 --- a/rendercv/data_model.py +++ b/rendercv/data_model.py @@ -15,6 +15,7 @@ from functools import cached_property import urllib.request import os from importlib.resources import files +import json from pydantic import ( BaseModel, @@ -26,6 +27,7 @@ from pydantic import ( EmailStr, PastDate, ) +from pydantic.json_schema import GenerateJsonSchema from pydantic.functional_validators import AfterValidator from pydantic_extra_types.phone_numbers import PhoneNumber from pydantic_extra_types.color import Color @@ -57,7 +59,7 @@ dictionary = [ "dc", "grammarly", "css", - "html" + "html", ] @@ -216,6 +218,63 @@ def format_date(date: Date) -> str: return date_string +def generate_json_schema(output_directory: str) -> str: + """Generate the JSON schema of the data model and save it to a file. + + Args: + output_directory (str): The output directory to save the schema. + """ + + class RenderCVSchemaGenerator(GenerateJsonSchema): + def generate(self, schema, mode="validation"): + json_schema = super().generate(schema, mode=mode) + json_schema["title"] = "RenderCV Input" + + # remove the description of the class (RenderCVDataModel) + del json_schema["description"] + + # add $id + json_schema[ + "$id" + ] = "https://raw.githubusercontent.com/sinaatalay/rendercv/main/schema.json" + + # add $schema + json_schema["$schema"] = "http://json-schema.org/draft-07/schema#" + + # Loop through $defs and remove docstring descriptions and fix optional + # fields + for key, value in json_schema["$defs"].items(): + if "This class" in value["description"]: + del value["description"] + + null_type_dict = {} + null_type_dict["type"] = "null" + for field in value["properties"].values(): + if "anyOf" in field: + if ( + len(field["anyOf"]) == 2 + and null_type_dict in field["anyOf"] + ): + field["allOf"] = [field["anyOf"][0]] + del field["anyOf"] + + return json_schema + + schema = RenderCVDataModel.model_json_schema( + schema_generator=RenderCVSchemaGenerator + ) + schema = json.dumps(schema, indent=2) + + # Change all anyOf to oneOf + schema = schema.replace('"anyOf"', '"oneOf"') + + path_to_schema = os.path.join(output_directory, "schema.json") + with open(path_to_schema, "w") as f: + f.write(schema) + + return path_to_schema + + # ====================================================================================== # ====================================================================================== # ====================================================================================== @@ -230,7 +289,6 @@ LaTeXDimension = Annotated[ str, Field( pattern=r"\d+\.?\d* *(cm|in|pt|mm|ex|em)", - examples=["1.35 cm", "1 in", "12 pt", "14 mm", "2 ex", "3 em"], ), ] SpellCheckedString = Annotated[str, AfterValidator(check_spelling)] @@ -250,22 +308,22 @@ class ClassicThemePageMargins(BaseModel): top: LaTeXDimension = Field( default="1.35 cm", title="Top Margin", - description="The top margin of the page.", + description="The top margin of the page with units.", ) bottom: LaTeXDimension = Field( default="1.35 cm", title="Bottom Margin", - description="The bottom margin of the page.", + description="The bottom margin of the page with units.", ) left: LaTeXDimension = Field( default="1.35 cm", title="Left Margin", - description="The left margin of the page.", + description="The left margin of the page with units.", ) right: LaTeXDimension = Field( default="1.35 cm", title="Right Margin", - description="The right margin of the page.", + description="The right margin of the page with units.", ) @@ -336,18 +394,22 @@ class ClassicThemeMargins(BaseModel): page: ClassicThemePageMargins = Field( default=ClassicThemePageMargins(), title="Page Margins", + description="Page margins for the classic theme.", ) section_title: ClassicThemeSectionTitleMargins = Field( default=ClassicThemeSectionTitleMargins(), title="Section Title Margins", + description="Section title margins for the classic theme.", ) entry_area: ClassicThemeEntryAreaMargins = Field( default=ClassicThemeEntryAreaMargins(), title="Entry Area Margins", + description="Entry area margins for the classic theme.", ) highlights_area: ClassicThemeHighlightsAreaMargins = Field( default=ClassicThemeHighlightsAreaMargins(), title="Highlights Area Margins", + description="Highlights area margins for the classic theme.", ) @@ -375,7 +437,6 @@ class ClassicThemeOptions(BaseModel): default="3.6 cm", title="Date and Location Column Width", description="The width of the date and location column.", - examples=["1.35 cm", "1 in", "12 pt", "14 mm", "2 ex", "3 em"], ) show_timespan_in: list[str] = Field( @@ -385,7 +446,6 @@ class ClassicThemeOptions(BaseModel): "The time span will be shown in the date and location column in these" " sections. The input should be a list of strings." ), - examples=[["Education", "Experience"]], ) show_last_updated_date: bool = Field( @@ -416,19 +476,16 @@ class Design(BaseModel): default="SourceSans3", title="Font", description="The font of the CV.", - examples=["SourceSans3", "Roboto", "EBGaramond"], ) font_size: Literal["10pt", "11pt", "12pt"] = Field( default="10pt", title="Font Size", description="The font size of the CV. It can be 10pt, 11pt, or 12pt.", - examples=["10pt", "11pt", "12pt"], ) page_size: Literal["a4paper", "letterpaper"] = Field( default="a4paper", title="Page Size", description="The page size of the CV. It can be a4paper or letterpaper.", - examples=["a4paper", "letterpaper"], ) options: Optional[ClassicThemeOptions] = Field( default=None, @@ -534,7 +591,7 @@ class Event(BaseModel): " and end date should be provided instead. All of them can't be provided at" " the same time." ), - examples=["2020-09-24"], + examples=["2020-09-24", "My Custom Date"], ) highlights: Optional[list[SpellCheckedString]] = Field( default=[], @@ -760,12 +817,10 @@ class ExperienceEntry(Event): company: str = Field( title="Company", description="The company name. It will be shown as bold text.", - examples=["CERN", "Apple"], ) position: str = Field( title="Position", description="The position. It will be shown as normal text.", - examples=["Software Engineer", "Mechanical Engineer"], ) @@ -775,12 +830,11 @@ class EducationEntry(Event): institution: str = Field( title="Institution", description="The institution name. It will be shown as bold text.", - examples=["Massachusetts Institute of Technology", "Bogazici University"], + examples=["Bogazici University"], ) area: str = Field( title="Area", description="The area of study. It will be shown as normal text.", - examples=["Mechanical Engineering", "Computer Science"], ) study_type: Optional[str] = Field( default=None, @@ -792,7 +846,6 @@ class EducationEntry(Event): default=None, title="GPA", description="The GPA of the degree.", - examples=["4.00/4.00", "3.80/4.00"], ) transcript_url: Optional[HttpUrl] = Field( default=None, @@ -826,37 +879,30 @@ class PublicationEntry(Event): title: str = Field( title="Title of the Publication", description="The title of the publication. It will be shown as bold text.", - examples=["My Awesome Paper", "My Awesome Book"], ) authors: list[str] = Field( title="Authors", description="The authors of the publication in order as a list of strings.", - examples=["John Doe", "Jane Doe"], ) doi: str = Field( title="DOI", description="The DOI of the publication.", - examples=["10.1103/PhysRevB.76.054309"], + examples=["10.48550/arXiv.2310.03138"], ) date: str = Field( title="Publication Date", description="The date of the publication.", - examples=[2021, 2022], + examples=["2021-10-31"], ) cited_by: Optional[int] = Field( default=None, title="Cited By", description="The number of citations of the publication.", - examples=[10, 100], ) journal: Optional[str] = Field( default=None, title="Journal", description="The journal or the conference name.", - examples=[ - "Physical Review B", - "ASME International Mechanical Engineering Congress and Exposition", - ], ) @field_validator("doi") @@ -886,12 +932,10 @@ class SocialNetwork(BaseModel): network: Literal["LinkedIn", "GitHub", "Instagram"] = Field( title="Social Network", description="The social network name.", - examples=["LinkedIn", "GitHub", "Instagram"], ) username: str = Field( title="Username", description="The username of the social network. The link will be generated.", - examples=["johndoe", "johndoe123"], ) @@ -947,7 +991,7 @@ class Section(BaseModel): title: str = Field( title="Section Title", description="The title of the section.", - examples=["Awards", "My Custom Section", "Languages"], + examples=["My Custom Section"], ) entry_type: Literal[ "OneLineEntry", @@ -988,19 +1032,16 @@ class CurriculumVitae(BaseModel): name: str = Field( title="Name", description="The name of the person.", - examples=["John Doe", "Jane Doe"], ) label: Optional[str] = Field( default=None, title="Label", description="The label of the person.", - examples=["Software Engineer", "Mechanical Engineer"], ) location: Optional[str] = Field( default=None, title="Location", description="The location of the person. This is not rendered currently.", - examples=["Istanbul, Turkey", "Boston, MA, USA"], ) email: Optional[EmailStr] = Field( default=None, @@ -1027,7 +1068,6 @@ class CurriculumVitae(BaseModel): description=( "The order of sections in the CV. The section title should be used." ), - examples=[["Education", "Work Experience", "Skills"]], ) education: Optional[list[EducationEntry]] = Field( default=None, @@ -1250,7 +1290,11 @@ class RenderCVDataModel(BaseModel): title="Design", description="The design of the CV.", ) - cv: CurriculumVitae + cv: CurriculumVitae = Field( + default=CurriculumVitae(name="John Doe"), + title="Curriculum Vitae", + description="The data of the CV.", + ) @model_validator(mode="after") @classmethod diff --git a/run_rendercv.py b/run_rendercv.py index 3dcc487..c01413e 100644 --- a/run_rendercv.py +++ b/run_rendercv.py @@ -1,18 +1,15 @@ -import rendercv.__main__ as rendercv +import os +from rendercv.__main__ import main as rendercv_main +from rendercv.data_model import generate_json_schema -# input_file_path = "personal.yaml" -# rendercv.main(input_file_path) +input_file_path = "personal.yaml" +rendercv_main(input_file_path) # This script is equivalent to running the following command in the terminal: # python -m rendercv personal.yaml # or # rendercv personal.yaml -from rendercv.data_model import RenderCVDataModel - -jsoan = RenderCVDataModel.model_json_schema() -import json -# write json to file -with open("json_schema.json", "w") as f: - f.write(json.dumps(jsoan)) +# Generate schema.json +# generate_json_schema(os.path.join(os.path.dirname(__file__))) diff --git a/schema.json b/schema.json new file mode 100644 index 0000000..498fe8b --- /dev/null +++ b/schema.json @@ -0,0 +1,1364 @@ +{ + "$defs": { + "ClassicThemeEntryAreaMargins": { + "properties": { + "left": { + "default": "0.2 cm", + "description": "The left margin of entry areas.", + "pattern": "\\d+\\.?\\d* *(cm|in|pt|mm|ex|em)", + "title": "Left Margin", + "type": "string" + }, + "right": { + "default": "0.2 cm", + "description": "The right margin of entry areas.", + "pattern": "\\d+\\.?\\d* *(cm|in|pt|mm|ex|em)", + "title": "Right Margin", + "type": "string" + }, + "vertical_between": { + "default": "0.12 cm", + "description": "The vertical margin between entry areas.", + "pattern": "\\d+\\.?\\d* *(cm|in|pt|mm|ex|em)", + "title": "Vertical Margin Between Entry Areas", + "type": "string" + } + }, + "title": "ClassicThemeEntryAreaMargins", + "type": "object" + }, + "ClassicThemeHighlightsAreaMargins": { + "properties": { + "top": { + "default": "0.12 cm", + "description": "The top margin of highlights areas.", + "pattern": "\\d+\\.?\\d* *(cm|in|pt|mm|ex|em)", + "title": "Top Margin", + "type": "string" + }, + "left": { + "default": "0.6 cm", + "description": "The left margin of highlights areas.", + "pattern": "\\d+\\.?\\d* *(cm|in|pt|mm|ex|em)", + "title": "Left Margin", + "type": "string" + }, + "vertical_between_bullet_points": { + "default": "0.07 cm", + "description": "The vertical margin between bullet points.", + "pattern": "\\d+\\.?\\d* *(cm|in|pt|mm|ex|em)", + "title": "Vertical Margin Between Bullet Points", + "type": "string" + } + }, + "title": "ClassicThemeHighlightsAreaMargins", + "type": "object" + }, + "ClassicThemeMargins": { + "properties": { + "page": { + "allOf": [ + { + "$ref": "#/$defs/ClassicThemePageMargins" + } + ], + "default": { + "bottom": "1.35 cm", + "left": "1.35 cm", + "right": "1.35 cm", + "top": "1.35 cm" + }, + "description": "Page margins for the classic theme.", + "title": "Page Margins" + }, + "section_title": { + "allOf": [ + { + "$ref": "#/$defs/ClassicThemeSectionTitleMargins" + } + ], + "default": { + "bottom": "0.13 cm", + "top": "0.13 cm" + }, + "description": "Section title margins for the classic theme.", + "title": "Section Title Margins" + }, + "entry_area": { + "allOf": [ + { + "$ref": "#/$defs/ClassicThemeEntryAreaMargins" + } + ], + "default": { + "left": "0.2 cm", + "right": "0.2 cm", + "vertical_between": "0.12 cm" + }, + "description": "Entry area margins for the classic theme.", + "title": "Entry Area Margins" + }, + "highlights_area": { + "allOf": [ + { + "$ref": "#/$defs/ClassicThemeHighlightsAreaMargins" + } + ], + "default": { + "left": "0.6 cm", + "top": "0.12 cm", + "vertical_between_bullet_points": "0.07 cm" + }, + "description": "Highlights area margins for the classic theme.", + "title": "Highlights Area Margins" + } + }, + "title": "ClassicThemeMargins", + "type": "object" + }, + "ClassicThemeOptions": { + "properties": { + "primary_color": { + "default": "rgb(0,79,144)", + "description": "The primary color of Classic Theme. It is used for the section titles, heading, and the links.\nThe color can be specified either with their [name](https://www.w3.org/TR/SVG11/types.html#ColorKeywords), hexadecimal value, RGB value, or HSL value.", + "examples": [ + "Black", + "7fffd4", + "rgb(0,79,144)", + "hsl(270, 60%, 70%)" + ], + "format": "color", + "title": "Primary Color", + "type": "string" + }, + "date_and_location_width": { + "default": "3.6 cm", + "description": "The width of the date and location column.", + "pattern": "\\d+\\.?\\d* *(cm|in|pt|mm|ex|em)", + "title": "Date and Location Column Width", + "type": "string" + }, + "show_timespan_in": { + "default": [], + "description": "The time span will be shown in the date and location column in these sections. The input should be a list of strings.", + "items": { + "type": "string" + }, + "title": "Show Time Span in These Sections", + "type": "array" + }, + "show_last_updated_date": { + "default": true, + "description": "If this option is set to true, then the last updated date will be shown in the header.", + "title": "Show Last Updated Date", + "type": "boolean" + }, + "margins": { + "allOf": [ + { + "$ref": "#/$defs/ClassicThemeMargins" + } + ], + "default": { + "entry_area": { + "left": "0.2 cm", + "right": "0.2 cm", + "vertical_between": "0.12 cm" + }, + "highlights_area": { + "left": "0.6 cm", + "top": "0.12 cm", + "vertical_between_bullet_points": "0.07 cm" + }, + "page": { + "bottom": "1.35 cm", + "left": "1.35 cm", + "right": "1.35 cm", + "top": "1.35 cm" + }, + "section_title": { + "bottom": "0.13 cm", + "top": "0.13 cm" + } + }, + "description": "Page, section title, entry field, and highlights field margins.", + "title": "Margins" + } + }, + "title": "ClassicThemeOptions", + "type": "object" + }, + "ClassicThemePageMargins": { + "properties": { + "top": { + "default": "1.35 cm", + "description": "The top margin of the page with units.", + "pattern": "\\d+\\.?\\d* *(cm|in|pt|mm|ex|em)", + "title": "Top Margin", + "type": "string" + }, + "bottom": { + "default": "1.35 cm", + "description": "The bottom margin of the page with units.", + "pattern": "\\d+\\.?\\d* *(cm|in|pt|mm|ex|em)", + "title": "Bottom Margin", + "type": "string" + }, + "left": { + "default": "1.35 cm", + "description": "The left margin of the page with units.", + "pattern": "\\d+\\.?\\d* *(cm|in|pt|mm|ex|em)", + "title": "Left Margin", + "type": "string" + }, + "right": { + "default": "1.35 cm", + "description": "The right margin of the page with units.", + "pattern": "\\d+\\.?\\d* *(cm|in|pt|mm|ex|em)", + "title": "Right Margin", + "type": "string" + } + }, + "title": "ClassicThemePageMargins", + "type": "object" + }, + "ClassicThemeSectionTitleMargins": { + "properties": { + "top": { + "default": "0.13 cm", + "description": "The top margin of section titles.", + "pattern": "\\d+\\.?\\d* *(cm|in|pt|mm|ex|em)", + "title": "Top Margin", + "type": "string" + }, + "bottom": { + "default": "0.13 cm", + "description": "The bottom margin of section titles.", + "pattern": "\\d+\\.?\\d* *(cm|in|pt|mm|ex|em)", + "title": "Bottom Margin", + "type": "string" + } + }, + "title": "ClassicThemeSectionTitleMargins", + "type": "object" + }, + "CurriculumVitae": { + "properties": { + "name": { + "description": "The name of the person.", + "title": "Name", + "type": "string" + }, + "label": { + "default": null, + "description": "The label of the person.", + "title": "Label", + "allOf": [ + { + "type": "string" + } + ] + }, + "location": { + "default": null, + "description": "The location of the person. This is not rendered currently.", + "title": "Location", + "allOf": [ + { + "type": "string" + } + ] + }, + "email": { + "default": null, + "description": "The email of the person. It will be rendered in the heading.", + "title": "Email", + "allOf": [ + { + "format": "email", + "type": "string" + } + ] + }, + "phone": { + "default": null, + "title": "Phone", + "allOf": [ + { + "maxLength": 64, + "minLength": 7, + "type": "string" + } + ] + }, + "website": { + "default": null, + "title": "Website", + "allOf": [ + { + "format": "uri", + "maxLength": 2083, + "minLength": 1, + "type": "string" + } + ] + }, + "social_networks": { + "default": null, + "description": "The social networks of the person. They will be rendered in the heading.", + "title": "Social Networks", + "allOf": [ + { + "items": { + "$ref": "#/$defs/SocialNetwork" + }, + "type": "array" + } + ] + }, + "summary": { + "default": null, + "description": "The summary of the person.", + "title": "Summary", + "allOf": [ + { + "type": "string" + } + ] + }, + "section_order": { + "default": null, + "description": "The order of sections in the CV. The section title should be used.", + "title": "Section Order", + "allOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "education": { + "default": null, + "description": "The education entries of the person.", + "title": "Education", + "allOf": [ + { + "items": { + "$ref": "#/$defs/EducationEntry" + }, + "type": "array" + } + ] + }, + "work_experience": { + "default": null, + "description": "The work experience entries of the person.", + "title": "Work Experience", + "allOf": [ + { + "items": { + "$ref": "#/$defs/ExperienceEntry" + }, + "type": "array" + } + ] + }, + "academic_projects": { + "default": null, + "description": "The academic project entries of the person.", + "title": "Academic Projects", + "allOf": [ + { + "items": { + "$ref": "#/$defs/NormalEntry" + }, + "type": "array" + } + ] + }, + "personal_projects": { + "default": null, + "description": "The personal project entries of the person.", + "title": "Personal Projects", + "allOf": [ + { + "items": { + "$ref": "#/$defs/NormalEntry" + }, + "type": "array" + } + ] + }, + "publications": { + "default": null, + "description": "The publication entries of the person.", + "title": "Publications", + "allOf": [ + { + "items": { + "$ref": "#/$defs/PublicationEntry" + }, + "type": "array" + } + ] + }, + "certificates": { + "default": null, + "description": "The certificate entries of the person.", + "title": "Certificates", + "allOf": [ + { + "items": { + "$ref": "#/$defs/NormalEntry" + }, + "type": "array" + } + ] + }, + "extracurricular_activities": { + "default": null, + "description": "The extracurricular activity entries of the person.", + "title": "Extracurricular Activities", + "allOf": [ + { + "items": { + "$ref": "#/$defs/ExperienceEntry" + }, + "type": "array" + } + ] + }, + "test_scores": { + "default": null, + "description": "The test score entries of the person.", + "title": "Test Scores", + "allOf": [ + { + "items": { + "$ref": "#/$defs/OneLineEntry" + }, + "type": "array" + } + ] + }, + "skills": { + "default": null, + "description": "The skill entries of the person.", + "title": "Skills", + "allOf": [ + { + "items": { + "$ref": "#/$defs/OneLineEntry" + }, + "type": "array" + } + ] + }, + "custom_sections": { + "default": null, + "description": "Custom sections with custom section titles can be rendered as well.", + "title": "Custom Sections", + "allOf": [ + { + "items": { + "$ref": "#/$defs/Section" + }, + "type": "array" + } + ] + } + }, + "required": [ + "name" + ], + "title": "CurriculumVitae", + "type": "object" + }, + "Design": { + "properties": { + "theme": { + "const": "classic", + "default": "classic", + "description": "The only option is \"Classic\" for now.", + "title": "Theme name" + }, + "font": { + "default": "SourceSans3", + "description": "The font of the CV.", + "enum": [ + "SourceSans3", + "Roboto", + "EBGaramond" + ], + "title": "Font", + "type": "string" + }, + "font_size": { + "default": "10pt", + "description": "The font size of the CV. It can be 10pt, 11pt, or 12pt.", + "enum": [ + "10pt", + "11pt", + "12pt" + ], + "title": "Font Size", + "type": "string" + }, + "page_size": { + "default": "a4paper", + "description": "The page size of the CV. It can be a4paper or letterpaper.", + "enum": [ + "a4paper", + "letterpaper" + ], + "title": "Page Size", + "type": "string" + }, + "options": { + "default": null, + "description": "The options of the theme.", + "title": "Theme Options", + "allOf": [ + { + "$ref": "#/$defs/ClassicThemeOptions" + } + ] + } + }, + "title": "Design", + "type": "object" + }, + "EducationEntry": { + "properties": { + "start_date": { + "default": null, + "description": "The start date of the event in YYYY-MM-DD format.", + "examples": [ + "2020-09-24" + ], + "title": "Start Date", + "allOf": [ + { + "format": "date", + "type": "string" + } + ] + }, + "end_date": { + "oneOf": [ + { + "format": "date", + "type": "string" + }, + { + "const": "present" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The end date of the event in YYYY-MM-DD format. If the event is still ongoing, then the value should be \"present\".", + "examples": [ + "2020-09-24", + "present" + ], + "title": "End Date" + }, + "date": { + "oneOf": [ + { + "type": "string" + }, + { + "format": "date", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If the event is a one-day event, then this field should be filled in YYYY-MM-DD format. If the event is a multi-day event, then the start date and end date should be provided instead. All of them can't be provided at the same time.", + "examples": [ + "2020-09-24", + "My Custom Date" + ], + "title": "Date" + }, + "highlights": { + "default": [], + "description": "The highlights of the event. It will be rendered as bullet points.", + "examples": [ + "Did this.", + "Did that." + ], + "title": "Highlights", + "allOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "location": { + "default": null, + "description": "The location of the event. It will be shown with the date in the same column.", + "examples": [ + "Istanbul, Turkey" + ], + "title": "Location", + "allOf": [ + { + "type": "string" + } + ] + }, + "url": { + "default": null, + "title": "Url", + "allOf": [ + { + "format": "uri", + "maxLength": 2083, + "minLength": 1, + "type": "string" + } + ] + }, + "institution": { + "description": "The institution name. It will be shown as bold text.", + "examples": [ + "Bogazici University" + ], + "title": "Institution", + "type": "string" + }, + "area": { + "description": "The area of study. It will be shown as normal text.", + "title": "Area", + "type": "string" + }, + "study_type": { + "default": null, + "description": "The type of the degree.", + "examples": [ + "BS", + "BA", + "PhD", + "MS" + ], + "title": "Study Type", + "allOf": [ + { + "type": "string" + } + ] + }, + "gpa": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The GPA of the degree.", + "title": "GPA" + }, + "transcript_url": { + "default": null, + "description": "The URL of the transcript. It will be shown as a link next to the GPA.", + "examples": [ + "https://example.com/transcript.pdf" + ], + "title": "Transcript URL", + "allOf": [ + { + "format": "uri", + "maxLength": 2083, + "minLength": 1, + "type": "string" + } + ] + } + }, + "required": [ + "institution", + "area" + ], + "title": "EducationEntry", + "type": "object" + }, + "ExperienceEntry": { + "properties": { + "start_date": { + "default": null, + "description": "The start date of the event in YYYY-MM-DD format.", + "examples": [ + "2020-09-24" + ], + "title": "Start Date", + "allOf": [ + { + "format": "date", + "type": "string" + } + ] + }, + "end_date": { + "oneOf": [ + { + "format": "date", + "type": "string" + }, + { + "const": "present" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The end date of the event in YYYY-MM-DD format. If the event is still ongoing, then the value should be \"present\".", + "examples": [ + "2020-09-24", + "present" + ], + "title": "End Date" + }, + "date": { + "oneOf": [ + { + "type": "string" + }, + { + "format": "date", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If the event is a one-day event, then this field should be filled in YYYY-MM-DD format. If the event is a multi-day event, then the start date and end date should be provided instead. All of them can't be provided at the same time.", + "examples": [ + "2020-09-24", + "My Custom Date" + ], + "title": "Date" + }, + "highlights": { + "default": [], + "description": "The highlights of the event. It will be rendered as bullet points.", + "examples": [ + "Did this.", + "Did that." + ], + "title": "Highlights", + "allOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "location": { + "default": null, + "description": "The location of the event. It will be shown with the date in the same column.", + "examples": [ + "Istanbul, Turkey" + ], + "title": "Location", + "allOf": [ + { + "type": "string" + } + ] + }, + "url": { + "default": null, + "title": "Url", + "allOf": [ + { + "format": "uri", + "maxLength": 2083, + "minLength": 1, + "type": "string" + } + ] + }, + "company": { + "description": "The company name. It will be shown as bold text.", + "title": "Company", + "type": "string" + }, + "position": { + "description": "The position. It will be shown as normal text.", + "title": "Position", + "type": "string" + } + }, + "required": [ + "company", + "position" + ], + "title": "ExperienceEntry", + "type": "object" + }, + "NormalEntry": { + "properties": { + "start_date": { + "default": null, + "description": "The start date of the event in YYYY-MM-DD format.", + "examples": [ + "2020-09-24" + ], + "title": "Start Date", + "allOf": [ + { + "format": "date", + "type": "string" + } + ] + }, + "end_date": { + "oneOf": [ + { + "format": "date", + "type": "string" + }, + { + "const": "present" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The end date of the event in YYYY-MM-DD format. If the event is still ongoing, then the value should be \"present\".", + "examples": [ + "2020-09-24", + "present" + ], + "title": "End Date" + }, + "date": { + "oneOf": [ + { + "type": "string" + }, + { + "format": "date", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If the event is a one-day event, then this field should be filled in YYYY-MM-DD format. If the event is a multi-day event, then the start date and end date should be provided instead. All of them can't be provided at the same time.", + "examples": [ + "2020-09-24", + "My Custom Date" + ], + "title": "Date" + }, + "highlights": { + "default": [], + "description": "The highlights of the event. It will be rendered as bullet points.", + "examples": [ + "Did this.", + "Did that." + ], + "title": "Highlights", + "allOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "location": { + "default": null, + "description": "The location of the event. It will be shown with the date in the same column.", + "examples": [ + "Istanbul, Turkey" + ], + "title": "Location", + "allOf": [ + { + "type": "string" + } + ] + }, + "url": { + "default": null, + "title": "Url", + "allOf": [ + { + "format": "uri", + "maxLength": 2083, + "minLength": 1, + "type": "string" + } + ] + }, + "name": { + "description": "The name of the entry. It will be shown as bold text.", + "title": "Name", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "NormalEntry", + "type": "object" + }, + "OneLineEntry": { + "properties": { + "start_date": { + "default": null, + "description": "The start date of the event in YYYY-MM-DD format.", + "examples": [ + "2020-09-24" + ], + "title": "Start Date", + "allOf": [ + { + "format": "date", + "type": "string" + } + ] + }, + "end_date": { + "oneOf": [ + { + "format": "date", + "type": "string" + }, + { + "const": "present" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The end date of the event in YYYY-MM-DD format. If the event is still ongoing, then the value should be \"present\".", + "examples": [ + "2020-09-24", + "present" + ], + "title": "End Date" + }, + "date": { + "oneOf": [ + { + "type": "string" + }, + { + "format": "date", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If the event is a one-day event, then this field should be filled in YYYY-MM-DD format. If the event is a multi-day event, then the start date and end date should be provided instead. All of them can't be provided at the same time.", + "examples": [ + "2020-09-24", + "My Custom Date" + ], + "title": "Date" + }, + "highlights": { + "default": [], + "description": "The highlights of the event. It will be rendered as bullet points.", + "examples": [ + "Did this.", + "Did that." + ], + "title": "Highlights", + "allOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "location": { + "default": null, + "description": "The location of the event. It will be shown with the date in the same column.", + "examples": [ + "Istanbul, Turkey" + ], + "title": "Location", + "allOf": [ + { + "type": "string" + } + ] + }, + "url": { + "default": null, + "title": "Url", + "allOf": [ + { + "format": "uri", + "maxLength": 2083, + "minLength": 1, + "type": "string" + } + ] + }, + "name": { + "description": "The name of the entry. It will be shown as bold text.", + "title": "Name", + "type": "string" + }, + "details": { + "description": "The details of the entry. It will be shown as normal text.", + "title": "Details", + "type": "string" + } + }, + "required": [ + "name", + "details" + ], + "title": "OneLineEntry", + "type": "object" + }, + "PublicationEntry": { + "properties": { + "start_date": { + "default": null, + "description": "The start date of the event in YYYY-MM-DD format.", + "examples": [ + "2020-09-24" + ], + "title": "Start Date", + "allOf": [ + { + "format": "date", + "type": "string" + } + ] + }, + "end_date": { + "oneOf": [ + { + "format": "date", + "type": "string" + }, + { + "const": "present" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The end date of the event in YYYY-MM-DD format. If the event is still ongoing, then the value should be \"present\".", + "examples": [ + "2020-09-24", + "present" + ], + "title": "End Date" + }, + "date": { + "description": "The date of the publication.", + "examples": [ + "2021-10-31" + ], + "title": "Publication Date", + "type": "string" + }, + "highlights": { + "default": [], + "description": "The highlights of the event. It will be rendered as bullet points.", + "examples": [ + "Did this.", + "Did that." + ], + "title": "Highlights", + "allOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "location": { + "default": null, + "description": "The location of the event. It will be shown with the date in the same column.", + "examples": [ + "Istanbul, Turkey" + ], + "title": "Location", + "allOf": [ + { + "type": "string" + } + ] + }, + "url": { + "default": null, + "title": "Url", + "allOf": [ + { + "format": "uri", + "maxLength": 2083, + "minLength": 1, + "type": "string" + } + ] + }, + "title": { + "description": "The title of the publication. It will be shown as bold text.", + "title": "Title of the Publication", + "type": "string" + }, + "authors": { + "description": "The authors of the publication in order as a list of strings.", + "items": { + "type": "string" + }, + "title": "Authors", + "type": "array" + }, + "doi": { + "description": "The DOI of the publication.", + "examples": [ + "10.48550/arXiv.2310.03138" + ], + "title": "DOI", + "type": "string" + }, + "cited_by": { + "default": null, + "description": "The number of citations of the publication.", + "title": "Cited By", + "allOf": [ + { + "type": "integer" + } + ] + }, + "journal": { + "default": null, + "description": "The journal or the conference name.", + "title": "Journal", + "allOf": [ + { + "type": "string" + } + ] + } + }, + "required": [ + "date", + "title", + "authors", + "doi" + ], + "title": "PublicationEntry", + "type": "object" + }, + "Section": { + "properties": { + "title": { + "description": "The title of the section.", + "examples": [ + "My Custom Section" + ], + "title": "Section Title", + "type": "string" + }, + "entry_type": { + "description": "The type of the entries in the section.", + "enum": [ + "OneLineEntry", + "NormalEntry", + "ExperienceEntry", + "EducationEntry", + "PublicationEntry" + ], + "title": "Entry Type", + "type": "string" + }, + "link_text": { + "default": null, + "description": "If the section has a link, then what should be the text of the link? If this field is not provided, then the link text will be generated automatically based on the URL.", + "examples": [ + "view on GitHub", + "view on LinkedIn" + ], + "title": "Link Text", + "allOf": [ + { + "type": "string" + } + ] + }, + "entries": { + "description": "The entries of the section. The format depends on the entry type.", + "items": { + "oneOf": [ + { + "$ref": "#/$defs/NormalEntry" + }, + { + "$ref": "#/$defs/OneLineEntry" + }, + { + "$ref": "#/$defs/ExperienceEntry" + }, + { + "$ref": "#/$defs/EducationEntry" + }, + { + "$ref": "#/$defs/PublicationEntry" + } + ] + }, + "title": "Entries", + "type": "array" + } + }, + "required": [ + "title", + "entry_type", + "entries" + ], + "title": "Section", + "type": "object" + }, + "SocialNetwork": { + "properties": { + "network": { + "description": "The social network name.", + "enum": [ + "LinkedIn", + "GitHub", + "Instagram" + ], + "title": "Social Network", + "type": "string" + }, + "username": { + "description": "The username of the social network. The link will be generated.", + "title": "Username", + "type": "string" + } + }, + "required": [ + "network", + "username" + ], + "title": "SocialNetwork", + "type": "object" + } + }, + "properties": { + "design": { + "allOf": [ + { + "$ref": "#/$defs/Design" + } + ], + "default": { + "font": "SourceSans3", + "font_size": "10pt", + "options": { + "date_and_location_width": "3.6 cm", + "margins": { + "entry_area": { + "left": "0.2 cm", + "right": "0.2 cm", + "vertical_between": "0.12 cm" + }, + "highlights_area": { + "left": "0.6 cm", + "top": "0.12 cm", + "vertical_between_bullet_points": "0.07 cm" + }, + "page": { + "bottom": "1.35 cm", + "left": "1.35 cm", + "right": "1.35 cm", + "top": "1.35 cm" + }, + "section_title": { + "bottom": "0.13 cm", + "top": "0.13 cm" + } + }, + "primary_color": "#004f90", + "show_last_updated_date": true, + "show_timespan_in": [] + }, + "page_size": "a4paper", + "theme": "classic" + }, + "description": "The design of the CV.", + "title": "Design" + }, + "cv": { + "allOf": [ + { + "$ref": "#/$defs/CurriculumVitae" + } + ], + "default": { + "academic_projects": null, + "certificates": null, + "connections": [], + "custom_sections": null, + "education": null, + "email": null, + "extracurricular_activities": null, + "label": null, + "location": null, + "name": "John Doe", + "personal_projects": null, + "phone": null, + "publications": null, + "section_order": null, + "sections": [], + "skills": null, + "social_networks": null, + "summary": null, + "test_scores": null, + "website": null, + "work_experience": null + }, + "description": "The data of the CV.", + "title": "Curriculum Vitae" + } + }, + "title": "RenderCV Input", + "type": "object", + "$id": "https://raw.githubusercontent.com/sinaatalay/rendercv/main/schema.json", + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/tests/test_data_model.py b/tests/test_data_model.py index 43e06db..c5f0449 100644 --- a/tests/test_data_model.py +++ b/tests/test_data_model.py @@ -1,4 +1,6 @@ import unittest +import os +import json from rendercv import data_model @@ -781,3 +783,25 @@ class TestDataModel(unittest.TestCase): with self.subTest(msg="custom sections with duplicate titles"): with self.assertRaises(ValidationError): data_model.CurriculumVitae(**input) + + def test_if_json_schema_is_the_latest(self): + tests_directory = os.path.dirname(__file__) + path_to_generated_schema = data_model.generate_json_schema(tests_directory) + + # Read the generated JSON schema: + with open(path_to_generated_schema, "r") as f: + generated_json_schema = json.load(f) + + # Remove the generated JSON schema: + os.remove(path_to_generated_schema) + + # Read the repository's current JSON schema: + path_to_schema = os.path.join( + os.path.dirname(tests_directory), + "schema.json" + ) + with open(path_to_schema, "r") as f: + current_json_schema = json.load(f) + + # Compare the two JSON schemas: + self.assertEqual(generated_json_schema, current_json_schema) \ No newline at end of file