-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
269 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}"}))) |