Skip to content

Commit

Permalink
feat: python property and class generation
Browse files Browse the repository at this point in the history
  • Loading branch information
krvital committed Aug 25, 2024
1 parent 37ae2b1 commit d399805
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 64 deletions.
110 changes: 70 additions & 40 deletions src/aidbox_sdk/generator/python.clj
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,21 @@
(:import
[aidbox_sdk.generator CodeGenerator]))

(defn fhir-type->lang-type [fhir-type]
(defn ->lang-type [fhir-type]
(case fhir-type
;; Primitive Types
"boolean" "bool"
"instant" "str"
"time" "str"
"date" "str"
"dateTime" "str"
"decimal" "str"
"decimal" "float"

"integer" "integer"
"unsignedInt" "integer"
"positiveInt" "integer"

"integer64" "str"
"integer64" "integer"
"base64Binary" "str"

"uri" "str"
Expand All @@ -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
Expand All @@ -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))
Expand All @@ -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") "<T>")
(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") "<T>")
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]
Expand All @@ -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)))
Expand All @@ -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])
Expand Down
50 changes: 26 additions & 24 deletions test/aidbox_sdk/generator/python_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -22,36 +22,40 @@

(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"
(gen.python/generate-property {:name "type",
:base "Patient_Link",
:array false,
:required true,
:value "string"}))))
:value "string"
:type "string"}))))

(testing "array optional"
(is (= "address: Optional[List[Address]] = None"
(gen.python/generate-property {:name "address",
:base "Patient",
:array true,
:required false,
:value "Address"}))))
:value "Address"
:type "Address"}))))

(testing "array required"
(is (= "extension: list[Extension] = []"
(gen.python/generate-property {:name "extension",
:base "Element",
:array true,
:required true,
:value "Extension"}))))
:value "Extension"
:type "Extension"}))))

(testing "element with literal"
;; TODO
Expand All @@ -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
Expand Down

0 comments on commit d399805

Please sign in to comment.