From 446edbe6a6ef4f58766266a1281f9792da35181d Mon Sep 17 00:00:00 2001 From: Vitaly Kravtsov Date: Sun, 25 Aug 2024 19:47:40 +0600 Subject: [PATCH] feat: python property and class generation --- src/aidbox_sdk/generator/python.clj | 110 ++++++++++++++-------- test/aidbox_sdk/generator/python_test.clj | 50 +++++----- 2 files changed, 96 insertions(+), 64 deletions(-) diff --git a/src/aidbox_sdk/generator/python.clj b/src/aidbox_sdk/generator/python.clj index 5e773d6..6f61e09 100644 --- a/src/aidbox_sdk/generator/python.clj +++ b/src/aidbox_sdk/generator/python.clj @@ -7,7 +7,7 @@ (:import [aidbox_sdk.generator CodeGenerator])) -(defn fhir-type->lang-type [fhir-type] +(defn ->lang-type [fhir-type] (case fhir-type ;; Primitive Types "boolean" "bool" @@ -15,13 +15,13 @@ "time" "str" "date" "str" "dateTime" "str" - "decimal" "str" + "decimal" "float" "integer" "integer" "unsignedInt" "integer" "positiveInt" "integer" - "integer64" "str" + "integer64" "integer" "base64Binary" "str" "uri" "str" @@ -38,13 +38,13 @@ ;; else fhir-type)) -(defn url->resource-type [reference] +(defn url->resource-name [reference] (last (str/split (str reference) #"/"))) (defn class-name "Generate class name from schema url." [url] - (uppercase-first-letter (url->resource-type url))) + (uppercase-first-letter (url->resource-name url))) (defn generate-deps [deps] (->> deps @@ -55,11 +55,12 @@ (str/join "\n"))) (defn package->directory - "Generate directory name from package name. - hl7.fhir.r4.core#4.0.1 -> hl7-fhir-r4-core" + "Generates directory name from package name. + + Example: + hl7.fhir.r4.core -> hl7-fhir-r4-core" [x] - (-> x - (str/replace #"[\.#]" "-"))) + (str/replace x #"[\.#]" "-")) (defn resource-file-path [ir-schema] (io/file (package->directory (:package ir-schema)) @@ -69,49 +70,74 @@ "") (defn generate-property - "Generate class property from schema element." + "Generates class property from schema element." [element] - (let [type (str - () - (fhir-type->lang-type - (:original-type element)) - (when (:array element) "[]") - (when (and (not (:required element)) - (not (:literal element))) "?")) - name (->snake-case (:name element))] + (let [name (->snake-case (:name element)) + lang-type (->lang-type (:type element)) + type (str + (cond + ;; required and array + (and (:required element) + (:array element)) + (format "List[%s]" lang-type) + + ;; not required and array + (and (not (:required element)) + (:array element)) + (format "Optional[List[%s]]" lang-type) + + ;; required and not array + (and (:required element) + (not (:array element))) + lang-type + + ;; not required and not array + (and (not (:required element)) + (not (:array element))) + (format "Optional[%s]" lang-type))) + + default-value (cond + (not (:required element)) + "None" + + (and (:required element) + (:array element)) + "[]" + + :else nil)] + (if (contains? element :choices) (generate-polymorphic-property element) - (str name ": " type (when-not (:required element) " = None"))))) - -(defn generate-class [schema & [inner-classes]] - (let [base-class (url->resource-type (:base schema)) - schema-name (or (:url schema) (:name schema)) - generic (when (= (:type schema) "Bundle") "") + (str name ": " type (when default-value (str " = " default-value)))))) + +(defn generate-class + "Generates Python class from IR (intermediate representation) schema." + [ir-schema & [inner-classes]] + (let [base-class (url->resource-name (:base ir-schema)) + schema-name (or (:url ir-schema) (:name ir-schema)) + generic (when (= (:type ir-schema) "Bundle") "") class-name' (class-name (str schema-name generic)) - elements (->> (:elements schema) + elements (->> (:elements ir-schema) (map #(if (and (= (:base %) "Bundle_Entry") (= (:name %) "resource")) (assoc % :value "T") %))) properties (->> elements + (sort-by :name) (map generate-property) (map u/add-indent) (str/join "\n")) - base-class (cond - (= base-class "DomainResource") "DomainResource, IResource" - :else base-class) base-class-name (when-not (str/blank? base-class) (uppercase-first-letter base-class))] - - (str "class " class-name' "(" base-class-name "):" - (when-not (str/blank? properties) - "\n") - properties - (when (and inner-classes - (seq inner-classes)) - "\n\n") - (str/join "\n\n" (map #(->> % str/split-lines (map u/add-indent) (str/join "\n")) inner-classes)) - "\n}"))) + (str + (str/join "\n\n" (map #(->> % str/split-lines (map u/add-indent) (str/join "\n")) inner-classes)) + "class " class-name' "(" base-class-name "):" + (when-not (str/blank? properties) + "\n") + properties + (when (and inner-classes + (seq inner-classes)) + "\n\n")))) (defn generate-module [& {:keys [deps classes] @@ -122,7 +148,9 @@ (flatten) (str/join "\n"))) -(defn generate-backbone-classes [ir-schema] +(defn generate-backbone-classes + "Generates classes from schema's backbone elements." + [ir-schema] (->> (ir-schema :backbone-elements) (map #(assoc % :base "BackboneElement")) (map generate-class))) @@ -146,7 +174,9 @@ (generate-resource-module [_ ir-schema] {:path (resource-file-path ir-schema) :content (generate-module - :deps [{:module "..base" :members ["*"]}] + :deps [{:module "pydantic" :members ["*"]} + {:module "typing" :members ["Optional" "List"]} + {:module "..base" :members ["*"]}] :classes [(generate-class ir-schema (generate-backbone-classes ir-schema))])}) (generate-search-params [_ search-schemas fhir-schemas]) diff --git a/test/aidbox_sdk/generator/python_test.clj b/test/aidbox_sdk/generator/python_test.clj index 2f7dc59..5b24102 100644 --- a/test/aidbox_sdk/generator/python_test.clj +++ b/test/aidbox_sdk/generator/python_test.clj @@ -22,12 +22,13 @@ (deftest test-generate-property (testing "simple case" - (is (= "active Optional[bool] = None" + (is (= "active: Optional[bool] = None" (gen.python/generate-property {:name "active", :base "Patient", :array false, :required false, - :value "bool"})))) + :value "bool" + :type "boolean"})))) (testing "required" (is (= "type: str" @@ -35,7 +36,8 @@ :base "Patient_Link", :array false, :required true, - :value "string"})))) + :value "string" + :type "string"})))) (testing "array optional" (is (= "address: Optional[List[Address]] = None" @@ -43,7 +45,8 @@ :base "Patient", :array true, :required false, - :value "Address"})))) + :value "Address" + :type "Address"})))) (testing "array required" (is (= "extension: list[Extension] = []" @@ -51,7 +54,8 @@ :base "Element", :array true, :required true, - :value "Extension"})))) + :value "Extension" + :type "Extension"})))) (testing "element with literal" ;; TODO @@ -65,35 +69,33 @@ ;; TODO )) -#_(deftest test-generate-class +(deftest test-generate-class + (testing "base" + (is (= (gen.python/generate-class fixtures/patient-ir-schema) + "class Patient(DomainResource):\n active: Optional[bool] = None\n address: Optional[List[Address]] = None\n birth_date: Optional[str] = None\n communication: Optional[List[BackboneElement]] = None\n contact: Optional[List[BackboneElement]] = None\n \n deceased_boolean: Optional[bool] = None\n deceased_date_time: Optional[str] = None\n gender: Optional[str] = None\n general_practitioner: Optional[List[Reference]] = None\n identifier: Optional[List[Identifier]] = None\n link: Optional[List[BackboneElement]] = None\n managing_organization: Optional[Reference] = None\n marital_status: Optional[CodeableConcept] = None\n \n multiple_birth_boolean: Optional[bool] = None\n multiple_birth_integer: Optional[integer] = None\n name: Optional[List[HumanName]] = None\n photo: Optional[List[Attachment]] = None\n telecom: Optional[List[ContactPoint]] = None")))) - (testing "")) - -#_ -(deftest generate-datatypes - (is - (= (sut/generate-datatypes generator [fixtures/coding-ir-schema]) +#_(deftest generate-datatypes + (is + (= (sut/generate-datatypes generator [fixtures/coding-ir-schema]) - [{:path (io/file "base" "__init__.py"), - :content - "namespace Aidbox.FHIR.Base;\n\npublic class Coding : Element\n{\n public string? Code { get; set; }\n public string? System { get; set; }\n public string? Display { get; set; }\n public string? Version { get; set; }\n public bool? UserSelected { get; set; }\n}"}]))) + [{:path (io/file "base" "__init__.py"), + :content + "namespace Aidbox.FHIR.Base;\n\npublic class Coding : Element\n{\n public string? Code { get; set; }\n public string? System { get; set; }\n public string? Display { get; set; }\n public string? Version { get; set; }\n public bool? UserSelected { get; set; }\n}"}]))) (deftest test-generate-resources (is (= (sut/generate-resource-module generator fixtures/patient-ir-schema) - {:path (io/file "hl7-fhir-r4-core/Patient.cs"), :content "using Aidbox.FHIR.Base;\nusing Aidbox.FHIR.Utils;\n\nnamespace Aidbox.FHIR.R4.Core;\n\npublic class Patient : DomainResource, IResource\n{\n public bool? MultipleBirthBoolean { get; set; }\n public Base.Address[]? Address { get; set; }\n public string? DeceasedDateTime { get; set; }\n public Base.ResourceReference? ManagingOrganization { get; set; }\n public bool? DeceasedBoolean { get; set; }\n public Base.HumanName[]? Name { get; set; }\n public string? BirthDate { get; set; }\n public int? MultipleBirthInteger { get; set; }\n public object? MultipleBirth \n {\n get\n {\n if (MultipleBirthBoolean is not null)\n {\n return MultipleBirthBoolean;\n }\n \n if (MultipleBirthInteger is not null)\n {\n return MultipleBirthInteger;\n }\n \n return null;\n }\n \n set\n {\n if (value?.GetType() == typeof(bool))\n {\n MultipleBirthBoolean = (bool)value;\n return;\n }\n \n if (value?.GetType() == typeof(int))\n {\n MultipleBirthInteger = (int)value;\n return;\n }\n \n throw new ArgumentException(\"Invalid type provided\");\n }\n }\n public object? Deceased \n {\n get\n {\n if (DeceasedDateTime is not null)\n {\n return DeceasedDateTime;\n }\n \n if (DeceasedBoolean is not null)\n {\n return DeceasedBoolean;\n }\n \n return null;\n }\n \n set\n {\n if (value?.GetType() == typeof(string))\n {\n DeceasedDateTime = (string)value;\n return;\n }\n \n if (value?.GetType() == typeof(bool))\n {\n DeceasedBoolean = (bool)value;\n return;\n }\n \n throw new ArgumentException(\"Invalid type provided\");\n }\n }\n public Base.Attachment[]? Photo { get; set; }\n public Patient_Link[]? Link { get; set; }\n public bool? Active { get; set; }\n public Patient_Communication[]? Communication { get; set; }\n public Base.Identifier[]? Identifier { get; set; }\n public Base.ContactPoint[]? Telecom { get; set; }\n public Base.ResourceReference[]? GeneralPractitioner { get; set; }\n public string? Gender { get; set; }\n public Base.CodeableConcept? MaritalStatus { get; set; }\n public Patient_Contact[]? Contact { get; set; }\n\n public class Patient_Link : BackboneElement\n {\n public required string Type { get; set; }\n public required Base.ResourceReference Other { get; set; }\n }\n\n public class Patient_Communication : BackboneElement\n {\n public required Base.CodeableConcept Language { get; set; }\n public bool? Preferred { get; set; }\n }\n\n public class Patient_Contact : BackboneElement\n {\n public Base.HumanName? Name { get; set; }\n public string? Gender { get; set; }\n public Base.Period? Period { get; set; }\n public Base.Address? Address { get; set; }\n public Base.ContactPoint[]? Telecom { get; set; }\n public Base.ResourceReference? Organization { get; set; }\n public Base.CodeableConcept[]? Relationship { get; set; }\n }\n}"}))) -#_ -(deftest generate-search-params - (is - (= (sut/generate-search-params generator fixtures/patient-search-params-schemas - [fixtures/patient-fhir-schema]) - [{:path (io/file "search/PatientSearchParameters.cs"), - :content - "namespace Aidbox.FHIR.Search;\n\npublic class PatientSearchParameters : DomainResourceSearchParameters\n{\n public string? Active { get; set; }\n public string? Address { get; set; }\n public string? AddressCity { get; set; }\n public string? AddressCountry { get; set; }\n public string? AddressPostalcode { get; set; }\n public string? AddressState { get; set; }\n public string? AddressUse { get; set; }\n public string? Birthdate { get; set; }\n public string? DeathDate { get; set; }\n public string? Deceased { get; set; }\n public string? Email { get; set; }\n public string? Ethnicity { get; set; }\n public string? Family { get; set; }\n public string? Gender { get; set; }\n public string? GeneralPractitioner { get; set; }\n public string? Given { get; set; }\n public string? Id { get; set; }\n public string? Identifier { get; set; }\n public string? Language { get; set; }\n public string? Link { get; set; }\n public string? Name { get; set; }\n public string? Organization { get; set; }\n public string? PartAgree { get; set; }\n public string? Phone { get; set; }\n public string? Phonetic { get; set; }\n public string? Race { get; set; }\n public string? Telecom { get; set; }\n}"}]))) +#_(deftest generate-search-params + (is + (= (sut/generate-search-params generator fixtures/patient-search-params-schemas + [fixtures/patient-fhir-schema]) + [{:path (io/file "search/PatientSearchParameters.cs"), + :content + "namespace Aidbox.FHIR.Search;\n\npublic class PatientSearchParameters : DomainResourceSearchParameters\n{\n public string? Active { get; set; }\n public string? Address { get; set; }\n public string? AddressCity { get; set; }\n public string? AddressCountry { get; set; }\n public string? AddressPostalcode { get; set; }\n public string? AddressState { get; set; }\n public string? AddressUse { get; set; }\n public string? Birthdate { get; set; }\n public string? DeathDate { get; set; }\n public string? Deceased { get; set; }\n public string? Email { get; set; }\n public string? Ethnicity { get; set; }\n public string? Family { get; set; }\n public string? Gender { get; set; }\n public string? GeneralPractitioner { get; set; }\n public string? Given { get; set; }\n public string? Id { get; set; }\n public string? Identifier { get; set; }\n public string? Language { get; set; }\n public string? Link { get; set; }\n public string? Name { get; set; }\n public string? Organization { get; set; }\n public string? PartAgree { get; set; }\n public string? Phone { get; set; }\n public string? Phonetic { get; set; }\n public string? Race { get; set; }\n public string? Telecom { get; set; }\n}"}]))) ;; TODO #_(deftest generate-constraints