Skip to content

Commit

Permalink
feat: add python generation
Browse files Browse the repository at this point in the history
  • Loading branch information
krvital committed Aug 23, 2024
1 parent 3e7bf46 commit 7da17e8
Show file tree
Hide file tree
Showing 4 changed files with 269 additions and 2 deletions.
8 changes: 6 additions & 2 deletions src/aidbox_sdk/converter.clj
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,11 @@
:array (boolean (:array v))
:required (.contains required (name k))
:value (transform-element
(str (url->resource-name parent-name) "_" (uppercase-first-letter (name k))) v (.contains required (name k)))}))
(str (url->resource-name parent-name) "_" (uppercase-first-letter (name k)))
v
(.contains required (name k)))

:original-type (:type v)}))

(defn- get-typings-and-imports [parent-name required data]
(reduce (fn [acc item]
Expand Down Expand Up @@ -187,5 +191,5 @@
(compile-elements)
(combine-elements)
(map (fn [schema] (update schema
:backbone-elements #(resolve-choices (flatten-backbones % [])))))
:backbone-elements #(resolve-choices (flatten-backbones % [])))))
(resolve-choices)))
3 changes: 3 additions & 0 deletions src/aidbox_sdk/generator/helpers.clj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
(defn ->pascal-case [s]
(str/join (map str/capitalize (words s))))

(defn ->snake-case [s]
(str/join "_" (map str/lower-case (words s))))

(defn uppercase-first-letter
"NOTE: Do not confuse with `capitalize` and `->pascal-case` functions.
Capitalize function lowercasing all letters after first.
Expand Down
156 changes: 156 additions & 0 deletions src/aidbox_sdk/generator/python.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
(ns aidbox-sdk.generator.python
(:require
[aidbox-sdk.generator.helpers :refer [->pascal-case ->snake-case uppercase-first-letter]]
[aidbox-sdk.generator.utils :as u]
[clojure.java.io :as io]
[clojure.string :as str])
(:import
[aidbox_sdk.generator CodeGenerator]))

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

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

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

"uri" "str"
"url" "str"
"canonical" "str"
"oid" "str"
"uuid" "str"

"string" "str"
"code" "str"
"markdown" "str"
"id" "str"

;; else
fhir-type))

(defn url->resource-type [reference]
(last (str/split (str reference) #"/")))

(defn class-name
"Generate class name from schema url."
[url]
(uppercase-first-letter (url->resource-type url)))

(defn generate-deps [deps]
(->> deps
(map (fn [{:keys [module members]}]
(if (seq members)
(str "from " module " import " (str/join ", " members))
(str "import " module))))
(str/join "\n")))

(defn package->directory
"Generate directory name from package name.
hl7.fhir.r4.core#4.0.1 -> hl7-fhir-r4-core"
[x]
(-> x
(str/replace #"[\.#]" "-")))

(defn resource-file-path [ir-schema]
(io/file (package->directory (:package ir-schema))
(str (->pascal-case (:name ir-schema)) ".py")))

(defn generate-polymorphic-property [element]
"")

(defn generate-property
"Generate 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))]
(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>")
class-name' (class-name (str schema-name generic))
elements (->> (:elements schema)
(map #(if (and (= (:base %) "Bundle_Entry")
(= (:name %) "resource"))
(assoc % :value "T")
%)))
properties (->> elements
(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}")))

(defn generate-module
[& {:keys [deps classes]
:or {classes []}}]
(->> (conj []
(generate-deps deps)
classes)
(flatten)
(str/join "\n")))

(defn generate-backbone-classes [ir-schema]
(->> (ir-schema :backbone-elements)
(map #(assoc % :base "BackboneElement"))
(map generate-class)))

(defrecord PythonCodeGenerator []
CodeGenerator
(generate-datatypes [_ ir-schemas]
(let [file-name "__init__.py"
file-path (io/file "base" file-name)
module (generate-module
:name ""
:deps [{:module "..base" :members []}]
:classes [(map (fn [ir-schema]
(generate-class ir-schema
(generate-backbone-classes ir-schema)))
ir-schemas)])]

[:path file-path
:content module]))

(generate-resource-module [_ ir-schema]
{:path (resource-file-path ir-schema)
:content (generate-module
:deps [{:module "..base" :members ["*"]}]
:classes [(generate-class ir-schema
(generate-backbone-classes ir-schema))])})
(generate-search-params [_ search-schemas fhir-schemas])
(generate-constraints [_ _schemas all-schemas] "")
(generate-sdk-files [this] ""))

(def generator (->PythonCodeGenerator))
104 changes: 104 additions & 0 deletions test/aidbox_sdk/generator/python_test.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
(ns aidbox-sdk.generator.python-test
(:require
[aidbox-sdk.fixtures.schemas :as fixtures]
[aidbox-sdk.generator :as sut]
[aidbox-sdk.generator.python :refer [generator] :as gen.python]
[clojure.java.io :as io]
[clojure.test :refer [deftest is testing]]))

(deftest test-generate-deps
(testing "no members"
(is (= (gen.python/generate-deps [{:module "pydantic" :members []}])
"import pydantic")))

(testing "with members"
(is (= (gen.python/generate-deps [{:module "pydantic" :members ["*"]}])
"from pydantic import *")))

(testing "multiple deps"
(is (= (gen.python/generate-deps [{:module "pydantic" :members ["*"]}
{:module "typing" :members ["Optional" "List"]}])
"from pydantic import *\nfrom typing import Optional, List"))))

(deftest test-generate-property
(testing "simple case"
(is (= "active Optional[bool] = None"
(gen.python/generate-property {:name "active",
:base "Patient",
:array false,
:required false,
:value "bool"}))))

(testing "required"
(is (= "type: str"
(gen.python/generate-property {:name "type",
:base "Patient_Link",
:array false,
:required true,
:value "string"}))))

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

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

(testing "element with literal"
;; TODO
)

(testing "element with meta"
;; TODO
)

(testing "element with choices"
;; TODO
))

#_(deftest test-generate-class

(testing ""))

#_
(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}"}])))

(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}"}])))

;; TODO
#_(deftest generate-constraints
(is
(= (sut/generate-constraints generator fixtures/some-sonstraint-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;return;\n }\n \n if (value?.GetType() == typeof(int))\n {\n MultipleBirthInteger = (int)value;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;return;\n }\n \n if (value?.GetType() == typeof(bool))\n {\n DeceasedBoolean = (bool)value;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}"})))

0 comments on commit 7da17e8

Please sign in to comment.