diff --git a/bin/node b/bin/node index a050acaf2..7ed24e84a 100755 --- a/bin/node +++ b/bin/node @@ -28,5 +28,5 @@ case $1 in advanced) advanced ;; cherry-none) cherry-none ;; cherry-advanced) cherry-advanced ;; - *) none; advanced; cherry; cherry-advanced ;; + *) none; advanced; cherry-none; cherry-advanced ;; esac diff --git a/perf/malli/perf/core.cljc b/perf/malli/perf/core.cljc index c23a00f20..ae73c22b4 100644 --- a/perf/malli/perf/core.cljc +++ b/perf/malli/perf/core.cljc @@ -3,7 +3,7 @@ [clj-async-profiler.core :as prof])) (defn serve! [] - (with-out-str (prof/serve-files 8080)) + (with-out-str (prof/serve-ui 8080)) nil) (defn clear! [] diff --git a/perf/malli/perf/protobuf3_schema_perf_test.cljc b/perf/malli/perf/protobuf3_schema_perf_test.cljc new file mode 100644 index 000000000..94584e632 --- /dev/null +++ b/perf/malli/perf/protobuf3_schema_perf_test.cljc @@ -0,0 +1,24 @@ +(ns malli.perf.protobuf3-schema-perf-test + (:require [malli.protobuf3-schema :as protobuf] + [malli.perf.core :as p])) + +(defn transform-perf [] + ;; 28.656211 µs + (p/bench (protobuf/transform [:map + [:id string?] + [:metadata [:map + [:created_at inst?] + [:tags [:vector string?]]]] + [:data [:vector [:map + [:name string?] + [:details [:map + [:type [:enum :type-a :type-b :type-c]] + [:properties [:vector [:map + [:key string?] + [:value [:or string? int? boolean?]] + [:nested [:vector [:map + [:sub_key string?] + [:sub_value any?]]]]]]]]]]]]]))) + +(comment + (transform-perf)) diff --git a/src/malli/protobuf3_schema.cljc b/src/malli/protobuf3_schema.cljc new file mode 100644 index 000000000..794515aeb --- /dev/null +++ b/src/malli/protobuf3_schema.cljc @@ -0,0 +1,173 @@ +(ns malli.protobuf3-schema + (:require [clojure.string :as str])) + +(defn to-snake-case [s] + (-> (name s) + (str/replace #"([a-z0-9])([A-Z])" "$1_$2") + (str/replace #"([A-Z]+)([A-Z][a-z])" "$1_$2") + (str/replace #"-" "_") + str/lower-case)) + +(defn to-pascal-case [s] + (as-> s $ + (name $) + (str/replace $ #"[-_\s]+" " ") + (str/split $ #"\s+") + (map str/capitalize $) + (str/join $))) + +(defn malli-type->protobuf-type [malli-type] + (cond + (= malli-type clojure.core/string?) "string" + (= malli-type clojure.core/int?) "int32" + (= malli-type clojure.core/boolean?) "bool" + (= malli-type clojure.core/double?) "double" + (= malli-type :string) "string" + (= malli-type :int) "int32" + (= malli-type :double) "double" + (= malli-type :boolean) "bool" + (= malli-type :keyword) "string" + (= malli-type :symbol) "string" + (= malli-type :uuid) "string" + (= malli-type :uri) "string" + (= malli-type :inst) "google.protobuf.Timestamp" + (= malli-type :nil) "google.protobuf.NullValue" + :else "bytes")) + +(defn to-proto-name + "Converts a Clojure-style name (possibly with hyphens) to a Protocol Buffer-compatible name." + [s] + (-> (name s) + (str/replace "-" "_"))) + +(declare transform-schema) + +(defn transform-map-schema [schema parent-name] + (let [fields (rest schema) + message-name (to-pascal-case parent-name) + transformed-fields (map-indexed + (fn [idx [field-name field-schema]] + (let [field-type (transform-schema field-schema (to-pascal-case (name field-name)))] + {:name (to-snake-case field-name) + :type field-type + :index (inc idx)})) + fields)] + {:type "message" + :name message-name + :fields transformed-fields})) + +(defn transform-vector-schema [schema parent-name] + (let [item-schema (second schema) + item-type (transform-schema item-schema parent-name)] + {:type "repeated" + :value-type item-type})) + +(defn transform-enum-schema [schema parent-name] + (let [enum-name (to-pascal-case parent-name) + values (drop 1 schema)] + {:type "enum" + :name enum-name + :values (map-indexed + (fn [idx value] + {:name (-> (name value) + str/upper-case + to-proto-name) + :index idx}) + values)})) + +(defn transform-schema [schema parent-name] + (let [schema-type (if (vector? schema) (first schema) schema)] + (case schema-type + :map (transform-map-schema schema parent-name) + :vector (transform-vector-schema schema parent-name) + :set (transform-vector-schema schema parent-name) + :enum (transform-enum-schema schema parent-name) + + (cond + (fn? schema) {:name (malli-type->protobuf-type schema)} + (keyword? schema) {:name (malli-type->protobuf-type schema)} + :else {:name (malli-type->protobuf-type schema)})))) + +(defn generate-field + "Generate a Protocol Buffer field definition." + [{:keys [type name index]}] + (let [field-type (cond + (string? type) type + (map? type) (case (:type type) + "repeated" (str "repeated " (if (map? (:value-type type)) + (:name (:value-type type)) + (:value-type type))) + "enum" (:name type) + (:name type)) + :else (str type))] + (str " " field-type " " name " = " index ";"))) + +(defn generate-message + "Generate a Protocol Buffer message definition." + [{:keys [name fields]}] + (str "message " name " {\n" + (str/join "\n" (map generate-field fields)) + "\n}")) + +(defn generate-enum + "Generate a Protocol Buffer enum definition." + [{:keys [name values]}] + (str "enum " name " {\n" + (str/join "\n" (map (fn [{:keys [name index]}] + (str " " name " = " index ";")) + values)) + "\n}")) + +(defn generate-definition + "Generate a Protocol Buffer definition (message or enum)." + [definition] + (case (:type definition) + "enum" (generate-enum definition) + "message" (generate-message definition))) + +(defn sort-definitions + "Sort definitions to ensure proper order (enums first, then nested messages, then main message)." + [definitions] + (let [enums (filter #(= (:type %) "enum") definitions) + messages (filter #(= (:type %) "message") definitions) + main-message (first (filter #(= (:name %) "Message") messages)) + other-messages (remove #(= % main-message) messages)] + (concat enums other-messages [main-message]))) + +(defn collect-definitions [schema] + (loop [stack [schema] + acc []] + (if-let [s (first stack)] + (let [rest-stack (rest stack)] + (cond + (map? s) + (case (:type s) + "message" (let [new-acc (conj acc s)] + (recur (into (map #(-> % :type) (:fields s)) rest-stack) + new-acc)) + "repeated" (recur (cons (:value-type s) rest-stack) acc) + "enum" (recur rest-stack (conj acc s)) + (recur rest-stack acc)) + :else (recur rest-stack acc))) + acc))) + + +(defn generate-protobuf3 + "Generate a complete Protocol Buffer 3 definition from a transformed schema." + [transformed-schema] + (let [all-definitions (collect-definitions transformed-schema) + sorted-definitions (sort-definitions all-definitions) + definitions-str (str/join "\n\n" (map generate-definition (drop-last sorted-definitions))) + main-message (last sorted-definitions)] + (str "syntax = \"proto3\";\n\n" + definitions-str + "\n\n" + (generate-message (assoc main-message :name "Message"))))) + +;; +;; public api +;; + +(defn transform [schema] + (let [transformed-schema (transform-schema schema "Message")] + (generate-protobuf3 transformed-schema))) diff --git a/test/malli/protobuf3_test.cljc b/test/malli/protobuf3_test.cljc new file mode 100644 index 000000000..f799e5516 --- /dev/null +++ b/test/malli/protobuf3_test.cljc @@ -0,0 +1,584 @@ +(ns malli.protobuf3-test + (:require [clojure.test :refer [deftest is testing are]] + [malli.protobuf3-schema :as pbuf] + [clojure.string :as str])) + +(deftest test-to-snake-case + (testing "to-snake-case function" + (are [input expected] (= expected (pbuf/to-snake-case input)) + "camelCase" "camel_case" + "PascalCase" "pascal_case" + "snake_case" "snake_case" + "UPPER_CASE" "upper_case" + "mixedCASE_with_underscores" "mixed_case_with_underscores" + "alreadysnakecase" "alreadysnakecase"))) + +(deftest test-to-pascal-case + (testing "to-pascal-case function" + (are [input expected] (= expected (pbuf/to-pascal-case input)) + "camel_case" "CamelCase" + "pascal_case" "PascalCase" + "snake_case" "SnakeCase" + "UPPER_CASE" "UpperCase" + "mixed_CASE_with_underscores" "MixedCaseWithUnderscores" + "alreadypascalcase" "Alreadypascalcase"))) + +(deftest test-malli-type->protobuf-type + (testing "malli-type->protobuf-type function" + (are [input expected] (= expected (pbuf/malli-type->protobuf-type input)) + clojure.core/string? "string" + clojure.core/int? "int32" + clojure.core/boolean? "bool" + :string "string" + :int "int32" + :double "double" + :boolean "bool" + :keyword "string" + :symbol "string" + :uuid "string" + :uri "string" + :inst "google.protobuf.Timestamp" + :nil "google.protobuf.NullValue" + :unknown "bytes"))) + +(deftest test-transform-map-schema + (testing "transform-map-schema function" + (let [schema [:map + [:name string?] + [:age int?]] + result (pbuf/transform-map-schema schema "TestMessage")] + (is (= "message" (:type result))) + (is (= "Testmessage" (:name result))) + (is (= 2 (count (:fields result)))) + (is (= {:name "name" :type {:name "string"} :index 1} (first (:fields result)))) + (is (= {:name "age" :type {:name "int32"} :index 2} (second (:fields result))))))) + +(deftest test-transform-vector-schema + (testing "transform-vector-schema function" + (let [schema [:vector string?] + result (pbuf/transform-vector-schema schema "TestVector")] + (is (= "repeated" (:type result))) + (is (= {:name "string"} (:value-type result)))))) + +(deftest test-transform-enum-schema + (testing "transform-enum-schema function" + (let [schema [:enum :active :inactive] + result (pbuf/transform-enum-schema schema "Status")] + (is (= "enum" (:type result))) + (is (= "Status" (:name result))) + (is (= 2 (count (:values result)))) + (is (= {:name "ACTIVE" :index 0} (first (:values result)))) + (is (= {:name "INACTIVE" :index 1} (second (:values result))))))) + +(deftest test-transform-schema + (testing "transform-schema function" + (testing "with map schema" + (let [schema [:map [:name string?]] + result (pbuf/transform-schema schema "TestMessage")] + (is (= "message" (:type result))) + (is (= "Testmessage" (:name result))))) + + (testing "with vector schema" + (let [schema [:vector string?] + result (pbuf/transform-schema schema "TestVector")] + (is (= "repeated" (:type result))))) + + (testing "with enum schema" + (let [schema [:enum :status :active :inactive] + result (pbuf/transform-schema schema "Status")] + (is (= "enum" (:type result))))) + + (testing "with primitive type" + (let [schema string? + result (pbuf/transform-schema schema "TestString")] + (is (= {:name "string"} result)))))) + +(deftest test-transform-schema-primitive-types + (testing "transform-schema with primitive types" + (are [schema expected] (= expected (pbuf/transform-schema schema "TestField")) + string? {:name "string"} + int? {:name "int32"} + boolean? {:name "bool"} + double? {:name "double"} + :string {:name "string"} + :int {:name "int32"} + :boolean {:name "bool"} + :double {:name "double"}))) + +(deftest test-transform-schema-enum + (testing "transform-schema with enum" + (let [schema [:enum :red :green :blue] + result (pbuf/transform-schema schema "Color") + expected {:type "enum" + :name "Color" + :values [{:name "RED" :index 0} + {:name "GREEN" :index 1} + {:name "BLUE" :index 2}]}] + (is (= expected result))))) + +(deftest test-transform-schema-vector + (testing "transform-schema with vector" + (let [schema [:vector string?] + result (pbuf/transform-schema schema "StringList") + expected {:type "repeated" + :value-type {:name "string"}}] + (is (= expected result))))) + +(deftest test-transform-schema-nested-map + (testing "transform-schema with nested map" + (let [schema [:map + [:name string?] + [:age int?]] + result (pbuf/transform-schema schema "Person") + expected {:type "message" + :name "Person" + :fields [{:name "name" :type {:name "string"} :index 1} + {:name "age" :type {:name "int32"} :index 2}]}] + (is (= expected result))))) + +(deftest test-transform-schema-nested-vector + (testing "transform-schema with nested vector" + (let [schema [:vector [:map + [:x int?] + [:y int?]]] + result (pbuf/transform-schema schema "PointList") + expected {:type "repeated" + :value-type {:type "message" + :name "Pointlist" + :fields '({:name "x" :type {:name "int32"} :index 1} + {:name "y" :type {:name "int32"} :index 2})}}] + (is (= expected result))))) + +(deftest test-transform-schema-deep-nested-structure + (testing "transform-schema with deeply nested maps and vectors" + (let [schema [:map + [:id string?] + [:metadata [:map + [:created_at inst?] + [:tags [:vector string?]]]] + [:data [:vector [:map + [:name string?] + [:details [:map + [:type [:enum :type-a :type-b :type-c]] + [:properties [:vector [:map + [:key string?] + [:value [:or string? int? boolean?]] + [:nested [:vector [:map + [:sub_key string?] + [:sub_value any?]]]]]]]]]]]]] + result (pbuf/transform-schema schema "DeepNestedStructure") + expected {:type "message" + :name "Deepnestedstructure" + :fields [{:name "id" :type {:name "string"} :index 1} + {:name "metadata" + :type {:type "message" + :name "Metadata" + :fields [{:name "created_at" :type {:name "bytes"} :index 1} + {:name "tags" :type {:type "repeated" :value-type {:name "string"}} :index 2}]} + :index 2} + {:name "data" + :type {:type "repeated" + :value-type + {:type "message" + :name "Data" + :fields + [{:name "name" :type {:name "string"} :index 1} + {:name "details" + :type {:type "message" + :name "Details" + :fields + [{:name "type" + :type {:type "enum" + :name "Type" + :values [{:name "TYPE_A" :index 0} + {:name "TYPE_B" :index 1} + {:name "TYPE_C" :index 2}]} + :index 1} + {:name "properties" + :type {:type "repeated" + :value-type + {:type "message" + :name "Properties" + :fields + [{:name "key" :type {:name "string"} :index 1} + {:name "value" :type {:name "bytes"} :index 2} + {:name "nested" + :type {:type "repeated" + :value-type + {:type "message" + :name "Nested" + :fields + [{:name "sub_key" :type {:name "string"} :index 1} + {:name "sub_value" :type {:name "bytes"} :index 2}]}} + :index 3}]}} + :index 2}]} + :index 2}]}} + :index 3}]}] + (is (= expected result))))) + +(deftest test-collect-definitions + (testing "collect-definitions function" + (let [schema {:type "message" + :name "TestMessage" + :fields [{:name "sub_message" + :type {:type "message" + :name "SubMessage" + :fields [{:name "enum_field" + :type {:type "enum" + :name "TestEnum" + :values [{:name "VALUE1" :index 0}]}}]}}]} + result (pbuf/collect-definitions schema)] + (is (= 3 (count result))) + (is (some #(= "TestMessage" (:name %)) result)) + (is (some #(= "SubMessage" (:name %)) result)) + (is (some #(= "TestEnum" (:name %)) result))))) + +(defn normalize-whitespace [s] + (-> s + (str/replace #"\s+" " ") + (str/replace #"\n+" "\n") + str/trim)) + +(deftest test-generate-field + (testing "generate-field function" + (are [input expected] (= expected (normalize-whitespace (pbuf/generate-field input))) + {:type {:name "string"} :name "name" :index 1} + "string name = 1;" + + {:type {:name "int32"} :name "age" :index 2} + "int32 age = 2;" + + {:type {:type "repeated" :value-type {:name "string"}} :name "hobbies" :index 3} + "repeated string hobbies = 3;" + + {:type {:type "enum" :name "Status"} :name "status" :index 4} + "Status status = 4;"))) + +(deftest test-generate-message + (testing "generate-message function" + (let [input {:name "Person" + :fields [{:type {:name "string"} :name "name" :index 1} + {:type {:name "int32"} :name "age" :index 2}]} + expected (normalize-whitespace "message Person { string name = 1; int32 age = 2; }") + result (normalize-whitespace (pbuf/generate-message input))] + (is (= expected result))))) + +(deftest test-generate-enum + (testing "generate-enum function" + (let [input {:name "Status" + :values [{:name "ACTIVE" :index 0} + {:name "INACTIVE" :index 1}]} + expected "enum Status { ACTIVE = 0; INACTIVE = 1; }" + result (normalize-whitespace (pbuf/generate-enum input))] + (is (= expected result))))) + +(deftest test-generate-definition + (testing "generate-definition function for message" + (let [input {:type "message" + :name "Person" + :fields [{:type {:name "string"} :name "name" :index 1}]} + expected "message Person { string name = 1; }" + result (normalize-whitespace (pbuf/generate-definition input))] + (is (= expected result)))) + + (testing "generate-definition function for enum" + (let [input {:type "enum" + :name "Status" + :values [{:name "ACTIVE" :index 0}]} + expected "enum Status { ACTIVE = 0; }" + result (normalize-whitespace (pbuf/generate-definition input))] + (is (= expected result))))) + +(deftest test-sort-definitions + (testing "sort-definitions function" + (let [input [{:type "message" :name "Person" :fields []} + {:type "enum" :name "Status" :values []} + {:type "message" :name "Address" :fields []} + {:type "message" :name "Message" :fields []}] + expected [{:type "enum" :name "Status" :values []} + {:type "message" :name "Person" :fields []} + {:type "message" :name "Address" :fields []} + {:type "message" :name "Message" :fields []}] + result (pbuf/sort-definitions input)] + (is (= expected result))))) + +(deftest test-generate-protobuf3 + (testing "generate-protobuf3 function with various schema types" + (testing "Simple message with primitive types" + (let [schema {:type "message" + :name "Message" + :fields [{:name "name" :type {:name "string"} :index 1} + {:name "age" :type {:name "int32"} :index 2} + {:name "is_student" :type {:name "bool"} :index 3}]} + result (normalize-whitespace (pbuf/generate-protobuf3 schema)) + expected (normalize-whitespace " + syntax = \"proto3\"; + message Message { + string name = 1; + int32 age = 2; + bool is_student = 3; + }")] + (is (= expected result)))) + + (testing "Message with enum" + (let [schema {:type "message" + :name "Message" + :fields [{:name "name" :type {:name "string"} :index 1} + {:name "status" :type {:type "enum" + :name "Status" + :values [{:name "ACTIVE" :index 0} + {:name "INACTIVE" :index 1}]} :index 2}]} + result (normalize-whitespace (pbuf/generate-protobuf3 schema)) + expected (normalize-whitespace " + syntax = \"proto3\"; + enum Status { + ACTIVE = 0; + INACTIVE = 1; + } + message Message { + string name = 1; + Status status = 2; + }")] + (is (= expected result)))) + + (testing "Message with nested map" + (let [schema {:type "message" + :name "Message" + :fields [{:name "name" :type {:name "string"} :index 1} + {:name "address" :type {:type "message" + :name "Address" + :fields [{:name "street" :type {:name "string"} :index 1} + {:name "city" :type {:name "string"} :index 2}]} :index 2}]} + result (normalize-whitespace (pbuf/generate-protobuf3 schema)) + expected (normalize-whitespace " + syntax = \"proto3\"; + message Address { + string street = 1; + string city = 2; + } + message Message { + string name = 1; + Address address = 2; + }")] + (is (= expected result)))) + + (testing "Message with nested vector" + (let [schema {:type "message" + :name "Message" + :fields [{:name "name" :type {:name "string"} :index 1} + {:name "scores" :type {:type "repeated" + :value-type {:name "int32"}} :index 2}]} + result (normalize-whitespace (pbuf/generate-protobuf3 schema)) + expected (normalize-whitespace " + syntax = \"proto3\"; + message Message { + string name = 1; + repeated int32 scores = 2; + }")] + (is (= expected result)))) + + (testing "Complex nested structure" + (let [schema {:type "message" + :name "Message" + :fields [{:name "name" :type {:name "string"} :index 1} + {:name "departments" :type {:type "repeated" + :value-type {:type "message" + :name "Department" + :fields [{:name "name" :type {:name "string"} :index 1} + {:name "employees" :type {:type "repeated" + :value-type {:type "message" + :name "Employee" + :fields [{:name "name" :type {:name "string"} :index 1} + {:name "id" :type {:name "int32"} :index 2} + {:name "role" :type {:type "enum" + :name "Role" + :values [{:name "MANAGER" :index 0} + {:name "DEVELOPER" :index 1} + {:name "DESIGNER" :index 2}]} :index 3}]}} :index 2}]}} :index 2}]} + result (normalize-whitespace (pbuf/generate-protobuf3 schema)) + expected (normalize-whitespace " + syntax = \"proto3\"; + enum Role { + MANAGER = 0; + DEVELOPER = 1; + DESIGNER = 2; + } + message Department { + string name = 1; + repeated Employee employees = 2; + } + message Employee { + string name = 1; + int32 id = 2; + Role role = 3; + } + + message Message { + string name = 1; + repeated Department departments = 2; + }")] + (is (= expected result)))))) + +(deftest test-transform + (testing "transform function with various schema types" + (testing "Simple message with primitive types" + (let [schema [:map + [:name string?] + [:age int?] + [:is-student boolean?]] + result (normalize-whitespace (pbuf/transform schema)) + expected (normalize-whitespace " + syntax = \"proto3\"; + message Message { + string name = 1; + int32 age = 2; + bool is_student = 3; + }")] + (is (= expected result)))) + + (testing "Message with enum" + (let [schema [:map + [:name string?] + [:status [:enum :active :inactive]]] + result (normalize-whitespace (pbuf/transform schema)) + expected (normalize-whitespace " + syntax = \"proto3\"; + enum Status { + ACTIVE = 0; + INACTIVE = 1; + } + message Message { + string name = 1; + Status status = 2; + }")] + (is (= expected result)))) + + (testing "Message with nested map" + (let [schema [:map + [:name string?] + [:address [:map + [:street string?] + [:city string?]]]] + result (normalize-whitespace (pbuf/transform schema)) + expected (normalize-whitespace " + syntax = \"proto3\"; + message Address { + string street = 1; + string city = 2; + } + message Message { + string name = 1; + Address address = 2; + }")] + (is (= expected result)))) + + (testing "Message with nested vector" + (let [schema [:map + [:name string?] + [:scores [:vector int?]]] + result (normalize-whitespace (pbuf/transform schema)) + expected (normalize-whitespace " + syntax = \"proto3\"; + message Message { + string name = 1; + repeated int32 scores = 2; + }")] + (is (= expected result)))) + + (testing "Complex nested structure" + (let [schema [:map + [:name string?] + [:departments [:vector + [:map + [:name string?] + [:employees [:vector + [:map + [:name string?] + [:id int?] + [:role [:enum :manager :developer :designer]]]]]]]]] + result (normalize-whitespace (pbuf/transform schema)) + expected (normalize-whitespace " + syntax = \"proto3\"; + enum Role { + MANAGER = 0; + DEVELOPER = 1; + DESIGNER = 2; + } + + message Departments { + string name = 1; + repeated Employees employees = 2; + } + + message Employees { + string name = 1; + int32 id = 2; + Role role = 3; + } + + message Message { + string name = 1; + repeated Departments departments = 2; + }")] + (is (= expected result)))))) + +(deftest test-transform-deep-nested-structure + (testing "transform function with deeply nested maps and vectors" + (let [schema [:map + [:id string?] + [:metadata [:map + [:created_at inst?] + [:tags [:vector string?]]]] + [:data [:vector [:map + [:name string?] + [:details [:map + [:type [:enum :type-a :type-b :type-c]] + [:properties [:vector [:map + [:key string?] + [:value [:or string? int? boolean?]] + [:nested [:vector [:map + [:sub_key string?] + [:sub_value any?]]]]]]]]]]]]] + result (normalize-whitespace (pbuf/transform schema)) + expected (normalize-whitespace " + syntax = \"proto3\"; + + enum Type { + TYPE_A = 0; + TYPE_B = 1; + TYPE_C = 2; + } + + message Metadata { + bytes created_at = 1; + repeated string tags = 2; + } + + message Data { + string name = 1; + Details details = 2; + } + + message Details { + Type type = 1; + repeated Properties properties = 2; + } + + message Properties { + string key = 1; + bytes value = 2; + repeated Nested nested = 3; + } + + message Nested { + string sub_key = 1; + bytes sub_value = 2; + } + + message Message { + string id = 1; + Metadata metadata = 2; + repeated Data data = 3; + } + ")] + (is (= expected result)))))