diff --git a/src/aidbox_sdk/converter.clj b/src/aidbox_sdk/converter.clj index e168cb4..5db1720 100644 --- a/src/aidbox_sdk/converter.clj +++ b/src/aidbox_sdk/converter.clj @@ -250,6 +250,9 @@ (->> all-schemas (map (fn [schema] {:name (:id schema) + :deps (if-let [base (:base schema)] + [(->pascal-case (url->resource-name base))] + []) :base (when-let [base (:base schema)] (->pascal-case (url->resource-name base))) :elements (->> (resolve-elements search-params-schemas (:id schema)) diff --git a/src/aidbox_sdk/generator/dotnet.clj b/src/aidbox_sdk/generator/dotnet.clj index 5547c78..7c89242 100644 --- a/src/aidbox_sdk/generator/dotnet.clj +++ b/src/aidbox_sdk/generator/dotnet.clj @@ -8,7 +8,39 @@ (:import [aidbox_sdk.generator CodeGenerator])) -(defn polymorphic-element->property [element] +(defn ->lang-type [fhir-type] + (case fhir-type + ;; Primitive Types + "boolean" "bool" + "instant" "string" + "time" "string" + "date" "string" + "dateTime" "string" + "decimal" "number" + + "integer" "int" + "unsignedInt" "long" + "positiveInt" "long" + + "integer64" "long" + "base64Binary" "string" + + "uri" "string" + "url" "string" + "canonical" "string" + "oid" "string" + "uuid" "string" + + "string" "string" + "code" "string" + "markdown" "string" + "id" "string" + "xhtml" "string" + + ;; else + fhir-type)) + +(defn generate-polymorphic-property [element] (str "public object?" " " (uppercase-first-letter (:name element)) @@ -81,39 +113,39 @@ (defn url->resource-name [url] (last (str/split (str url) #"/"))) +(defn ->backbone-type [element] + (str/replace (str (:base element) (uppercase-first-letter (:name element))) "[-_]" "")) + (defn generate-property "Generates class property from schema element." - [element] - (let [name (uppercase-first-letter (:name element)) - type (str - ;; TODO this is not enough - ;; In order to properly put "new" modifier we must know if - ;; there is a same property in ancestor classes - (when (:meta element) - "new ") - (when (and (:required element) - (not (:meta element))) "required ") - (or (:value element) (:type element)) - (:generic element) - (when (:array element) "[]") - (when (and (not (:required element)) - (not (:literal element))) "?")) - accessor (if (or (:meta element) - (:codeable-concept-pattern element)) - "{ get; }" - "{ get; set; }")] - (if (contains? element :choices) - (polymorphic-element->property element) - (str "public " type " " name " " accessor - (when (and (:required element) - (:codeable-concept-pattern element)) " = new()") - (:meta element))))) + [{:keys [name array required value type choices] :as element}] + (let [name (uppercase-first-letter name) + lang-type (str/replace (or value type "") #"_" "") + type (str + (when required "required ") + lang-type + (:generic element) + (when array "[]") + (when (and (not required) + (not (:literal element))) "?"))] + (cond choices + (generate-polymorphic-property element) + + (= (:type element) "Meta") + (if (:profile element) + (format "public new Meta Meta { get; } = new() { Profile = [\"%s\"] };" (:profile element)) + (format "public %s Meta { get; set; }" name)) + + :else + (str "public " type " " name " { get; set; }" + (when (and (:required element) + (:codeable-concept-pattern element)) " = new()") + (:meta element))))) (defn class-name "Generate class name from schema url." - [url] - (let [n (uppercase-first-letter - (url->resource-name url))] + [resource-name] + (let [n (->pascal-case resource-name)] (cond (= n "Expression") "ResourceExpression" (= n "Reference") "ResourceReference" @@ -123,7 +155,10 @@ (let [base-class (url->resource-name (:base schema)) schema-name (or (:url schema) (:name schema)) generic (when (= (:type schema) "Bundle") "") - class-name' (class-name (str schema-name generic)) + class-name' (class-name (str (or (:resource-name schema) + ;; need for BackboneElement + (:name schema) + "") generic)) elements (->> (:elements schema) (map #(if (and (= (:base %) "Bundle_Entry") (= (:name %) "resource")) @@ -135,9 +170,6 @@ (map u/add-indent) (str/join "\n")) - base-class (cond (= base-class "Resource") "Base.Resource" - (= base-class "DomainResource") "DomainResource, IResource" - :else base-class) base-class-name (when-not (str/blank? base-class) (str " : " (uppercase-first-letter base-class)))] @@ -160,9 +192,7 @@ enums [] delegates []}}] (->> (conj [] - (if deps - (apply u/using (map :module deps)) - []) + (if deps (apply u/using (map :module deps)) []) (u/namespace name') classes interfaces @@ -170,6 +200,7 @@ enums delegates) (flatten) + (remove str/blank?) (str/join "\n\n"))) (defn package->directory @@ -222,10 +253,8 @@ [{:path (datatypes-file-path) :content (generate-module :name "Aidbox.FHIR.Base" - :classes (map (fn [ir-schema] - (generate-class ir-schema - (map generate-class (:backbone-elements ir-schema)))) - ir-schemas))}]) + :deps [] + :classes (map generate-class ir-schemas))}]) (generate-resource-module [_ ir-schema] {:path (resource-file-path ir-schema) @@ -238,24 +267,25 @@ (generate-search-params [_ ir-schemas] (map (fn [ir-schema] - {:path (io/file "search" (str (:name ir-schema) "SearchParameters.cs")) - :content - (generate-module - :name "Aidbox.FHIR.Search" - :classes (generate-class - {:name (str (:name ir-schema) "SearchParameters") - :base (when (:base ir-schema) - (str (:base ir-schema) "SearchParameters")) - :elements (map (fn [el] (update el :name ->pascal-case)) - (:elements ir-schema))}))}) - ir-schemas)) + {:path (io/file "search" (str (:name ir-schema) "SearchParameters.cs")) + :content + (generate-module + :name "Aidbox.FHIR.Search" + :classes (generate-class + {:name (str (:name ir-schema) "SearchParameters") + :resource-name (str (:name ir-schema) "SearchParameters") + :base (when (:base ir-schema) + (str (:base ir-schema) "SearchParameters")) + :elements (map (fn [el] (update el :name ->pascal-case)) + (:elements ir-schema))}))}) + ir-schemas)) (generate-constraints [_ constraint-ir-schemas] (mapv (fn [[name' schema]] - {:path (constraint-file-path schema name') - :content (generate-constraint-module - (assoc schema :url name'))}) - constraint-ir-schemas)) + {:path (constraint-file-path schema name') + :content (generate-constraint-module + (assoc schema :url name'))}) + constraint-ir-schemas)) (generate-sdk-files [_] (generator/prepare-sdk-files :dotnet))) diff --git a/src/aidbox_sdk/generator/typescript.clj b/src/aidbox_sdk/generator/typescript.clj index dab11bc..3233efc 100644 --- a/src/aidbox_sdk/generator/typescript.clj +++ b/src/aidbox_sdk/generator/typescript.clj @@ -92,13 +92,15 @@ (generate-polymorphic-property element) (= type "Meta") - (format "%s: Meta & { profile: [\"%s\"] }" name profile) + (if profile + (format "%s: Meta & { profile: [\"%s\"] };" name profile) + (format "%s: Meta;" name)) :else (let [type' (if (= "BackboneElement" type) (->backbone-type element) (->lang-type (:type element)))] - (str name (when-not required "?") ": " type' (when array "[]") ";")))) + (str (->camel-case name) (when-not required "?") ": " type' (when array "[]") ";")))) (defn generate-class "Generates TypeScript type from IR (intermediate representation) schema." @@ -192,11 +194,11 @@ :classes [(generate-class ir-schema (map generate-class (:backbone-elements ir-schema)))]})}) - (generate-search-params [_ ir-schemas] [] + (generate-search-params [_ ir-schemas] (map (fn [ir-schema] {:path (search-param-filepath ir-schema) :content (generate-module - :deps (:deps ir-schema) + :deps (map #(format "%sSearchParameters" %) (:deps ir-schema)) :classes [(generate-class {:name (format "%sSearchParameters" (:name ir-schema)) :base (when (:base ir-schema) diff --git a/test/aidbox_sdk/generator/dotnet_test.clj b/test/aidbox_sdk/generator/dotnet_test.clj index 75e83a3..63239db 100644 --- a/test/aidbox_sdk/generator/dotnet_test.clj +++ b/test/aidbox_sdk/generator/dotnet_test.clj @@ -4,8 +4,8 @@ [aidbox-sdk.generator :as sut] [aidbox-sdk.generator.dotnet :refer [generator] :as gen.dotnet] [clojure.java.io :as io] - [clojure.test :refer [deftest is testing]])) - + [clojure.test :refer [deftest is testing]] + [clojure.string :as str])) (deftest test-generate-property (testing "simple case" @@ -14,6 +14,7 @@ :base "Patient", :array false, :required false, + :type "boolean" :value "bool"})))) (testing "required" @@ -22,6 +23,7 @@ :base "Patient_Link", :array false, :required true, + :type "string" :value "string"})))) (testing "array optional" @@ -30,6 +32,7 @@ :base "Patient", :array true, :required false, + :type "Address" :value "Base.Address"})))) (testing "array required" @@ -46,12 +49,68 @@ ) (testing "element with meta" - ;; TODO - ) + (is (= "public new Meta Meta { get; } = new() { Profile = [\"http://hl7.org/fhir/StructureDefinition/vitalsigns\"] };" + (gen.dotnet/generate-property {:name "meta", + :required true, + :value "Meta", + :profile "http://hl7.org/fhir/StructureDefinition/vitalsigns", + :type "Meta"})))) (testing "element with choices" - ;; TODO - )) + (is (= (str/join "\n" + ["public object? Value " + " {" + " get" + " {" + " if (ValueReference is not null)" + " {" + " return ValueReference;" + " }" + " " + " if (ValueInteger is not null)" + " {" + " return ValueInteger;" + " }" + " " + " return null;" + " }" + " " + " set" + " {" + " if (value?.GetType() == typeof(Base.ResourceReference))" + " {" + " ValueReference = (Base.ResourceReference)value;" + " return;" + " }" + " " + " if (value?.GetType() == typeof(int))" + " {" + " ValueInteger = (int)value;" + " return;" + " }" + " " + " throw new ArgumentException(\"Invalid type provided\");" + " }" + " }"]) + (gen.dotnet/generate-property {:name "value", + :choices + [{:name "valueReference" + :base "Observation" + :array false + :required false + :value "Base.ResourceReference" + :type "Reference" + :choice-option true} + {:name "valueInteger" + :base "Observation" + :array false + :required false + :value "int" + :type "integer" + :choice-option true}], + :base "Observation", + :array false, + :required false}))))) #_(deftest test-generate-class @@ -69,14 +128,50 @@ (= (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}"}))) + "using Aidbox.FHIR.Base;\nusing Aidbox.FHIR.Utils;\n\nnamespace Aidbox.FHIR.R4.Core;\n\npublic class Patient : DomainResource\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 PatientLink[]? Link { get; set; }\n public bool? Active { get; set; }\n public PatientCommunication[]? 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 PatientContact[]? Contact { get; set; }\n\n public class PatientLink : BackboneElement\n {\n public required string Type { get; set; }\n public required Base.ResourceReference Other { get; set; }\n }\n\n public class PatientCommunication : BackboneElement\n {\n public required Base.CodeableConcept Language { get; set; }\n public bool? Preferred { get; set; }\n }\n\n public class PatientContact : 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 test-generate-search-params (is (= (sut/generate-search-params generator fixtures/patient-search-params-ir-schemas) [{:path (io/file "search/PatientSearchParameters.cs"), :content - "namespace Aidbox.FHIR.Search;\n\npublic class PatientSearchParameters : DomainResourceSearchParameters\n{\n public string? Id { get; set; }\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? Age { get; set; }\n public string? BirthOrderBoolean { 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? Identifier { get; set; }\n public string? Language { get; set; }\n public string? Link { get; set; }\n public string? MothersMaidenName { 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}"}]))) + (str/join "\n" ["namespace Aidbox.FHIR.Search;" + "" + "public class PatientSearchParameters : DomainResourceSearchParameters" + "{" + " public string? Id { get; set; }" + " public string? Active { get; set; }" + " public string? Address { get; set; }" + " public string? AddressCity { get; set; }" + " public string? AddressCountry { get; set; }" + " public string? AddressPostalcode { get; set; }" + " public string? AddressState { get; set; }" + " public string? AddressUse { get; set; }" + " public string? Age { get; set; }" + " public string? BirthOrderBoolean { get; set; }" + " public string? Birthdate { get; set; }" + " public string? DeathDate { get; set; }" + " public string? Deceased { get; set; }" + " public string? Email { get; set; }" + " public string? Ethnicity { get; set; }" + " public string? Family { get; set; }" + " public string? Gender { get; set; }" + " public string? GeneralPractitioner { get; set; }" + " public string? Given { get; set; }" + " public string? Identifier { get; set; }" + " public string? Language { get; set; }" + " public string? Link { get; set; }" + " public string? MothersMaidenName { get; set; }" + " public string? Name { get; set; }" + " public string? Organization { get; set; }" + " public string? PartAgree { get; set; }" + " public string? Phone { get; set; }" + " public string? Phonetic { get; set; }" + " public string? Race { get; set; }" + " public string? Telecom { get; set; }" + "}"])}]))) ;; TODO #_(deftest generate-constraints diff --git a/test/aidbox_sdk/generator/typescript_test.clj b/test/aidbox_sdk/generator/typescript_test.clj index 8345ee2..5810c6a 100644 --- a/test/aidbox_sdk/generator/typescript_test.clj +++ b/test/aidbox_sdk/generator/typescript_test.clj @@ -65,11 +65,17 @@ ) (testing "element with meta" - (is (= "meta: Meta & { profile: [\"http://hl7.org/fhir/StructureDefinition/vitalsigns\"] }" - (gen.typescript/generate-property {:name "meta", - :required true, - :value "Meta", - :profile "http://hl7.org/fhir/StructureDefinition/vitalsigns", + (is (= "meta: Meta & { profile: [\"http://hl7.org/fhir/StructureDefinition/vitalsigns\"] };" + (gen.typescript/generate-property {:name "meta" + :required true + :value "Meta" + :profile "http://hl7.org/fhir/StructureDefinition/vitalsigns" + :type "Meta"}))) + + (is (= "meta: Meta;" + (gen.typescript/generate-property {:name "meta" + :required true + :value "Meta" :type "Meta"})))) (testing "element with choices"