├── .gitignore ├── .lein-classpath ├── .travis.yml ├── LICENSE ├── README.md ├── project.clj ├── resources └── proto │ └── flatland │ └── protobuf │ ├── extensions.proto │ └── test │ ├── codec.proto │ ├── core.proto │ ├── example.proto │ └── maps.proto ├── src └── flatland │ └── protobuf │ ├── PersistentProtocolBufferMap.java │ ├── codec.clj │ ├── core.clj │ └── schema.clj └── test ├── .gitignore └── flatland └── protobuf ├── codec_test.clj ├── core_test.clj └── example_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | .lein-deps-sum 2 | .lein-failures 3 | .cake 4 | *~ 5 | lib 6 | classes 7 | build 8 | pom.xml 9 | pom.xml.asc 10 | *.jar 11 | *.class 12 | resources/proto/google/protobuf/descriptor.proto 13 | docs 14 | protobuf-* 15 | protosrc 16 | .classpath 17 | .project 18 | .settings 19 | target/ 20 | -------------------------------------------------------------------------------- /.lein-classpath: -------------------------------------------------------------------------------- 1 | classes 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | lein: lein2 3 | script: lein2 testall -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 1.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF 5 | THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial code and 12 | documentation distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | 16 | i) changes to the Program, and 17 | 18 | ii) additions to the Program; 19 | 20 | where such changes and/or additions to the Program originate from and 21 | are distributed by that particular Contributor. A Contribution 22 | 'originates' from a Contributor if it was added to the Program by such 23 | Contributor itself or anyone acting on such Contributor's 24 | behalf. Contributions do not include additions to the Program which: 25 | (i) are separate modules of software distributed in conjunction with 26 | the Program under their own license agreement, and (ii) are not 27 | derivative works of the Program. 28 | 29 | "Contributor" means any person or entity that distributes the Program. 30 | 31 | "Licensed Patents" mean patent claims licensable by a Contributor 32 | which are necessarily infringed by the use or sale of its Contribution 33 | alone or when combined with the Program. 34 | 35 | "Program" means the Contributions distributed in accordance with this 36 | Agreement. 37 | 38 | "Recipient" means anyone who receives the Program under this 39 | Agreement, including all Contributors. 40 | 41 | 2. GRANT OF RIGHTS 42 | 43 | a) Subject to the terms of this Agreement, each Contributor hereby 44 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 45 | license to reproduce, prepare derivative works of, publicly display, 46 | publicly perform, distribute and sublicense the Contribution of such 47 | Contributor, if any, and such derivative works, in source code and 48 | object code form. 49 | 50 | b) Subject to the terms of this Agreement, each Contributor hereby 51 | grants Recipient a non-exclusive, worldwide, royalty-free patent 52 | license under Licensed Patents to make, use, sell, offer to sell, 53 | import and otherwise transfer the Contribution of such Contributor, if 54 | any, in source code and object code form. This patent license shall 55 | apply to the combination of the Contribution and the Program if, at 56 | the time the Contribution is added by the Contributor, such addition 57 | of the Contribution causes such combination to be covered by the 58 | Licensed Patents. The patent license shall not apply to any other 59 | combinations which include the Contribution. No hardware per se is 60 | licensed hereunder. 61 | 62 | c) Recipient understands that although each Contributor grants the 63 | licenses to its Contributions set forth herein, no assurances are 64 | provided by any Contributor that the Program does not infringe the 65 | patent or other intellectual property rights of any other entity. Each 66 | Contributor disclaims any liability to Recipient for claims brought by 67 | any other entity based on infringement of intellectual property rights 68 | or otherwise. As a condition to exercising the rights and licenses 69 | granted hereunder, each Recipient hereby assumes sole responsibility 70 | to secure any other intellectual property rights needed, if any. For 71 | example, if a third party patent license is required to allow 72 | Recipient to distribute the Program, it is Recipient's responsibility 73 | to acquire that license before distributing the Program. 74 | 75 | d) Each Contributor represents that to its knowledge it has sufficient 76 | copyright rights in its Contribution, if any, to grant the copyright 77 | license set forth in this Agreement. 78 | 79 | 3. REQUIREMENTS 80 | 81 | A Contributor may choose to distribute the Program in object code form 82 | under its own license agreement, provided that: 83 | 84 | a) it complies with the terms and conditions of this Agreement; and 85 | 86 | b) its license agreement: 87 | 88 | i) effectively disclaims on behalf of all Contributors all warranties 89 | and conditions, express and implied, including warranties or 90 | conditions of title and non-infringement, and implied warranties or 91 | conditions of merchantability and fitness for a particular purpose; 92 | 93 | ii) effectively excludes on behalf of all Contributors all liability 94 | for damages, including direct, indirect, special, incidental and 95 | consequential damages, such as lost profits; 96 | 97 | iii) states that any provisions which differ from this Agreement are 98 | offered by that Contributor alone and not by any other party; and 99 | 100 | iv) states that source code for the Program is available from such 101 | Contributor, and informs licensees how to obtain it in a reasonable 102 | manner on or through a medium customarily used for software exchange. 103 | 104 | When the Program is made available in source code form: 105 | 106 | a) it must be made available under this Agreement; and 107 | 108 | b) a copy of this Agreement must be included with each copy of the Program. 109 | 110 | Contributors may not remove or alter any copyright notices contained 111 | within the Program. 112 | 113 | Each Contributor must identify itself as the originator of its 114 | Contribution, if any, in a manner that reasonably allows subsequent 115 | Recipients to identify the originator of the Contribution. 116 | 117 | 4. COMMERCIAL DISTRIBUTION 118 | 119 | Commercial distributors of software may accept certain 120 | responsibilities with respect to end users, business partners and the 121 | like. While this license is intended to facilitate the commercial use 122 | of the Program, the Contributor who includes the Program in a 123 | commercial product offering should do so in a manner which does not 124 | create potential liability for other Contributors. Therefore, if a 125 | Contributor includes the Program in a commercial product offering, 126 | such Contributor ("Commercial Contributor") hereby agrees to defend 127 | and indemnify every other Contributor ("Indemnified Contributor") 128 | against any losses, damages and costs (collectively "Losses") arising 129 | from claims, lawsuits and other legal actions brought by a third party 130 | against the Indemnified Contributor to the extent caused by the acts 131 | or omissions of such Commercial Contributor in connection with its 132 | distribution of the Program in a commercial product offering. The 133 | obligations in this section do not apply to any claims or Losses 134 | relating to any actual or alleged intellectual property 135 | infringement. In order to qualify, an Indemnified Contributor must: a) 136 | promptly notify the Commercial Contributor in writing of such claim, 137 | and b) allow the Commercial Contributor tocontrol, and cooperate with 138 | the Commercial Contributor in, the defense and any related settlement 139 | negotiations. The Indemnified Contributor may participate in any such 140 | claim at its own expense. 141 | 142 | For example, a Contributor might include the Program in a commercial 143 | product offering, Product X. That Contributor is then a Commercial 144 | Contributor. If that Commercial Contributor then makes performance 145 | claims, or offers warranties related to Product X, those performance 146 | claims and warranties are such Commercial Contributor's responsibility 147 | alone. Under this section, the Commercial Contributor would have to 148 | defend claims against the other Contributors related to those 149 | performance claims and warranties, and if a court requires any other 150 | Contributor to pay any damages as a result, the Commercial Contributor 151 | must pay those damages. 152 | 153 | 5. NO WARRANTY 154 | 155 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS 156 | PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 157 | KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY 158 | WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY 159 | OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely 160 | responsible for determining the appropriateness of using and 161 | distributing the Program and assumes all risks associated with its 162 | exercise of rights under this Agreement , including but not limited to 163 | the risks and costs of program errors, compliance with applicable 164 | laws, damage to or loss of data, programs or equipment, and 165 | unavailability or interruption of operations. 166 | 167 | 6. DISCLAIMER OF LIABILITY 168 | 169 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR 170 | ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, 171 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING 172 | WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF 173 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 174 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR 175 | DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED 176 | HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 177 | 178 | 7. GENERAL 179 | 180 | If any provision of this Agreement is invalid or unenforceable under 181 | applicable law, it shall not affect the validity or enforceability of 182 | the remainder of the terms of this Agreement, and without further 183 | action by the parties hereto, such provision shall be reformed to the 184 | minimum extent necessary to make such provision valid and enforceable. 185 | 186 | If Recipient institutes patent litigation against any entity 187 | (including a cross-claim or counterclaim in a lawsuit) alleging that 188 | the Program itself (excluding combinations of the Program with other 189 | software or hardware) infringes such Recipient's patent(s), then such 190 | Recipient's rights granted under Section 2(b) shall terminate as of 191 | the date such litigation is filed. 192 | 193 | All Recipient's rights under this Agreement shall terminate if it 194 | fails to comply with any of the material terms or conditions of this 195 | Agreement and does not cure such failure in a reasonable period of 196 | time after becoming aware of such noncompliance. If all Recipient's 197 | rights under this Agreement terminate, Recipient agrees to cease use 198 | and distribution of the Program as soon as reasonably 199 | practicable. However, Recipient's obligations under this Agreement and 200 | any licenses granted by Recipient relating to the Program shall 201 | continue and survive. 202 | 203 | Everyone is permitted to copy and distribute copies of this Agreement, 204 | but in order to avoid inconsistency the Agreement is copyrighted and 205 | may only be modified in the following manner. The Agreement Steward 206 | reserves the right to publish new versions (including revisions) of 207 | this Agreement from time to time. No one other than the Agreement 208 | Steward has the right to modify this Agreement. The Eclipse Foundation 209 | is the initial Agreement Steward. The Eclipse Foundation may assign 210 | the responsibility to serve as the Agreement Steward to a suitable 211 | separate entity. Each new version of the Agreement will be given a 212 | distinguishing version number. The Program (including Contributions) 213 | may always be distributed subject to the version of the Agreement 214 | under which it was received. In addition, after a new version of the 215 | Agreement is published, Contributor may elect to distribute the 216 | Program (including its Contributions) under the new version. Except as 217 | expressly stated in Sections 2(a) and 2(b) above, Recipient receives 218 | no rights or licenses to the intellectual property of any Contributor 219 | under this Agreement, whether expressly, by implication, estoppel or 220 | otherwise. All rights in the Program not expressly granted under this 221 | Agreement are reserved. 222 | 223 | This Agreement is governed by the laws of the State of Washington and 224 | the intellectual property laws of the United States of America. No 225 | party to this Agreement will bring a legal action under this Agreement 226 | more than one year after the cause of action arose. Each party waives 227 | its rights to a jury trial in any resulting litigation. 228 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | clojure-protobuf provides a Clojure interface to Google's [protocol buffers](http://code.google.com/p/protobuf). 2 | Protocol buffers can be used to communicate with other languages over the network, and 3 | they are WAY faster to serialize and deserialize than standard Clojure objects. 4 | 5 | ## Getting started 6 | 7 | You'll probably want to use [Leiningen](https://github.com/technomancy/leiningen) with the 8 | [lein-protobuf](https://github.com/flatland/lein-protobuf) plugin for compiling `.proto` files. Add 9 | the following to your `project.clj` file: 10 | 11 | :dependencies [[org.flatland/protobuf "0.7.1"]] 12 | :plugins [[lein-protobuf "0.1.1"]] 13 | 14 | Be sure to replace `"0.6.0"` and `"0.1.1"` with the latest versions listed at 15 | http://clojars.org/protobuf and http://clojars.org/lein-protobuf. 16 | 17 | *Note: lein-protobuf requires at least version 2.0 of Leiningen.* 18 | 19 | ## Usage 20 | 21 | Assuming you have the following in `resources/proto/person.proto`: 22 | 23 | ```proto 24 | message Person { 25 | required int32 id = 1; 26 | required string name = 2; 27 | optional string email = 3; 28 | repeated string likes = 4; 29 | } 30 | ``` 31 | 32 | You can run the following to compile the `.proto` file: 33 | 34 | lein protobuf 35 | 36 | Now you can use the protocol buffer in Clojure: 37 | 38 | ```clojure 39 | (use 'flatland.protobuf.core) 40 | (import Example$Person) 41 | 42 | (def Person (protodef Example$Person)) 43 | 44 | (def p (protobuf Person :id 4 :name "Bob" :email "bob@example.com")) 45 | => {:id 4, :name "Bob", :email "bob@example.com"} 46 | 47 | (assoc p :name "Bill")) 48 | => {:id 4, :name "Bill", :email "bob@example.com"} 49 | 50 | (assoc p :likes ["climbing" "running" "jumping"]) 51 | => {:id 4, name "Bob", :email "bob@example.com", :likes ["climbing" "running" "jumping"]} 52 | 53 | (def b (protobuf-dump p)) 54 | => # 55 | 56 | (protobuf-load Person b) 57 | => {:id 4, :name "Bob", :email "bob@example.com"} 58 | ``` 59 | 60 | A protocol buffer map is immutable just like other clojure objects. It is similar to a 61 | struct-map, except you cannot insert fields that aren't specified in the `.proto` file. 62 | 63 | ## Extensions 64 | 65 | Clojure-protobuf supports extensions to protocol buffers which provide sets and maps using 66 | repeated fields. You can also provide metadata on protobuf fields using clojure syntax. To 67 | use these, you must import the extension file and include it when compiling. For example: 68 | 69 | ```proto 70 | import "flatland/protobuf/core/extensions.proto"; 71 | 72 | message Photo { 73 | required int32 id = 1; 74 | required string path = 2; 75 | repeated Label labels = 3 [(set) = true]; 76 | repeated Attr attrs = 4 [(map) = true]; 77 | repeated Tag tags = 5 [(map_by) = "person_id"]; 78 | 79 | message Label { 80 | required string item = 1; 81 | required bool exists = 2; 82 | } 83 | 84 | message Attr { 85 | required string key = 1; 86 | optional string val = 2; 87 | } 88 | 89 | message Tag { 90 | required int32 person_id = 1; 91 | optional int32 x_coord = 2 [(meta) = "{:max 100.0 :min -100.0}"]; 92 | optional int32 y_coord = 3; 93 | optional int32 width = 4; 94 | optional int32 height = 5; 95 | } 96 | } 97 | ``` 98 | Then you can access the extension fields in Clojure: 99 | 100 | ```clojure 101 | (use 'flatland.protobuf.core) 102 | (import Example$Photo) 103 | (import Example$Photo$Tag) 104 | 105 | (def Photo (protodef Example$Photo)) 106 | (def Tag (protodef Example$Photo$Tag)) 107 | 108 | (def p (protobuf Photo :id 7 :path "/photos/h2k3j4h9h23" :labels #{"hawaii" "family" "surfing"} 109 | :attrs {"dimensions" "1632x1224", "alpha" "no", "color space" "RGB"} 110 | :tags {4 {:person_id 4, :x_coord 607, :y_coord 813, :width 25, :height 27}})) 111 | => {:id 7 :path "/photos/h2k3j4h9h23" :labels #{"hawaii" "family" "surfing"}...} 112 | 113 | (def b (protobuf-dump p)) 114 | => # 115 | 116 | (protobuf-load Photo b) 117 | => {:id 7 :path "/photos/h2k3j4h9h23" :labels #{"hawaii" "family" "surfing"}...} 118 | 119 | (:x-coord (protobuf-schema Tag)) 120 | => {:max 100.0 :min -100.0} 121 | ``` 122 | 123 | ## Getting Help 124 | 125 | If you have any questions or need help, you can find us on IRC in [#flatland](irc://irc.freenode.net/#flatland). 126 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject org.flatland/protobuf "0.8.2-SNAPSHOT" 2 | :description "Clojure-protobuf provides a clojure interface to Google's protocol buffers." 3 | :license {:name "Eclipse Public License" 4 | :url "http://www.eclipse.org/legal/epl-v10.html"} 5 | :url "https://github.com/flatland/clojure-protobuf" 6 | :dependencies [[org.clojure/clojure "1.4.0"] 7 | [org.flatland/useful "0.9.0"] 8 | [org.flatland/schematic "0.1.0"] 9 | [org.flatland/io "0.3.0"] 10 | [ordered-collections "0.4.0"]] 11 | :plugins [[lein-protobuf "0.4.1"]] 12 | :aliases {"testall" ["with-profile" "dev,default:dev,1.3,default:dev,1.5,default" "test"]} 13 | :profiles {:1.3 {:dependencies [[org.clojure/clojure "1.3.0"]]} 14 | :1.5 {:dependencies [[org.clojure/clojure "1.5.0-master-SNAPSHOT"]]} 15 | :dev {:dependencies [[gloss "0.2.1"]]}} 16 | :repositories {"sonatype-snapshots" {:url "http://oss.sonatype.org/content/repositories/snapshots" 17 | :snapshots true 18 | :releases {:checksum :fail :update :always}}} 19 | :checksum-deps true 20 | :java-source-paths ["src"]) 21 | -------------------------------------------------------------------------------- /resources/proto/flatland/protobuf/extensions.proto: -------------------------------------------------------------------------------- 1 | import "google/protobuf/descriptor.proto"; 2 | 3 | option java_package = "flatland.protobuf"; 4 | option java_outer_classname = "Extensions"; 5 | 6 | extend google.protobuf.FieldOptions { 7 | optional bool set = 52001; 8 | optional bool map = 52002; 9 | optional string map_by = 52003; 10 | optional bool counter = 52004; 11 | optional bool succession = 52005; 12 | optional string map_deleted = 52006; 13 | optional string map_exists = 52007; 14 | optional string meta = 52010; 15 | optional bool nullable = 52020; 16 | optional string null_string = 52021; 17 | optional sint32 null_int = 52022; 18 | optional sint64 null_long = 52023; 19 | optional float null_float = 52024; 20 | optional double null_double = 52025; 21 | optional uint32 null_enum = 52026; 22 | } 23 | -------------------------------------------------------------------------------- /resources/proto/flatland/protobuf/test/codec.proto: -------------------------------------------------------------------------------- 1 | package flatland.protobuf.test.codec; 2 | 3 | import "flatland/protobuf/extensions.proto"; 4 | 5 | option java_package = "flatland.protobuf.test"; 6 | option java_outer_classname = "Codec"; 7 | 8 | message Foo { 9 | optional int32 foo = 1; 10 | optional int32 bar = 2; 11 | optional int32 baz = 3; 12 | 13 | repeated string tags = 4; 14 | repeated Item tag_set = 5 [(set) = true]; 15 | repeated Entry num_map = 6 [(map) = true]; 16 | 17 | optional Foo nested = 7; 18 | 19 | repeated int32 revisions = 14; 20 | optional fixed32 proto_length = 15; 21 | } 22 | 23 | message Item { 24 | required string item = 1; 25 | required bool exists = 2 [default = true]; 26 | } 27 | 28 | message Entry { 29 | required int32 key = 1; 30 | required string val = 2; 31 | } 32 | 33 | message Edge { 34 | required string to_id = 1; 35 | optional string a = 2; 36 | optional string b = 3; 37 | optional bool deleted = 4; 38 | } 39 | 40 | message Node { 41 | optional string id = 1; 42 | repeated Edge edges = 2 [(map_by) = "to_id"]; 43 | optional int32 rev = 3; 44 | optional int32 foo = 4; 45 | optional string bar = 5; 46 | repeated int32 baz = 6; 47 | } -------------------------------------------------------------------------------- /resources/proto/flatland/protobuf/test/core.proto: -------------------------------------------------------------------------------- 1 | package flatland.protobuf.test.core; 2 | 3 | import "flatland/protobuf/extensions.proto"; 4 | 5 | option java_package = "flatland.protobuf.test"; 6 | option java_outer_classname = "Core"; 7 | 8 | message Foo { 9 | optional uint32 id = 1 [default = 43]; 10 | optional string label = 2 [(meta) = "{:a 1 :b 2 :c 3}"]; 11 | repeated string tags = 3; 12 | optional Foo parent = 4; 13 | repeated Response responses = 5; 14 | 15 | repeated double doubles = 6; 16 | repeated float floats = 7; 17 | 18 | optional double lat = 8; 19 | optional float long = 9; 20 | 21 | repeated Count counts = 10 [(map_by) = "key"]; 22 | 23 | repeated Time time = 11 [(succession) = true]; 24 | 25 | enum Response { 26 | yes = 0; 27 | no = 1; 28 | maybe = 2; 29 | not_sure = 3; 30 | } 31 | 32 | repeated Item tag_set = 20 [(set) = true]; 33 | repeated Pair attr_map = 21 [(map) = true]; 34 | repeated Foo foo_by_id = 22 [(map_by) = "id"]; 35 | repeated Group groups = 23 [(map) = true]; 36 | repeated Item item_map = 24 [(map_by) = "item"]; 37 | repeated Pair pair_map = 25 [(map_by) = "key"]; 38 | optional bool deleted = 26 [default = false]; 39 | repeated Thing things = 27 [(map_by) = "id"]; 40 | } 41 | 42 | message Bar { 43 | optional int32 int = 1 [(nullable) = true, (null_int) = -1]; 44 | optional int64 long = 2 [(nullable) = true, (null_long) = -999999999999]; 45 | optional float flt = 3 [(nullable) = true, (null_float) = -0.0001]; 46 | optional double dbl = 4 [(nullable) = true, (null_double) = -0.00000001]; 47 | optional string str = 5 [(nullable) = true, (null_string) = "NULL"]; 48 | optional Enu enu = 6 [(nullable) = true, (null_enum) = 3]; 49 | 50 | enum Enu { 51 | a = 0; 52 | b = 1; 53 | c = 2; 54 | nil = 3; 55 | } 56 | 57 | repeated string label = 10 [(nullable) = true, (null_string) = "", (succession) = true]; 58 | repeated string labels = 11 [(nullable) = true, (null_string) = ""]; 59 | } 60 | 61 | message Time { 62 | optional sint32 year = 1; 63 | optional int32 month = 2; 64 | optional int32 day = 3; 65 | optional int32 hour = 4; 66 | optional int32 minute = 5; 67 | } 68 | 69 | message Pair { 70 | required string key = 1; 71 | required string val = 2; 72 | } 73 | 74 | message Group { 75 | required string key = 1; 76 | repeated Foo val = 2; 77 | } 78 | 79 | message Item { 80 | required string item = 1; 81 | required bool exists = 2 [default = true]; 82 | } 83 | 84 | message Count { 85 | required string key = 1; 86 | repeated int32 i = 2 [(counter) = true]; 87 | repeated double d = 3 [(counter) = true]; 88 | } 89 | 90 | message ErrorMsg { 91 | required sint32 code = 1; 92 | optional string data = 2; 93 | } 94 | 95 | message Response { 96 | required bool ok = 1; 97 | optional ErrorMsg error = 2; 98 | } 99 | 100 | message Thing { 101 | optional string id = 1; 102 | optional bool marked = 2; 103 | } 104 | -------------------------------------------------------------------------------- /resources/proto/flatland/protobuf/test/example.proto: -------------------------------------------------------------------------------- 1 | package flatland.protobuf.test.example; 2 | 3 | import "flatland/protobuf/extensions.proto"; 4 | 5 | option java_package = "flatland.protobuf.test"; 6 | option java_outer_classname = "Example"; 7 | 8 | message Photo { 9 | required int32 id = 1; 10 | required string path = 2; 11 | repeated Label labels = 3 [(set) = true]; 12 | repeated Attr attrs = 4 [(map) = true]; 13 | repeated Tag tags = 5 [(map_by) = "person_id"]; 14 | optional bytes image = 6; 15 | 16 | message Label { 17 | required string item = 1; 18 | required bool exists = 2; 19 | } 20 | 21 | message Attr { 22 | required string key = 1; 23 | optional string val = 2; 24 | } 25 | 26 | message Tag { 27 | required int32 person_id = 1; 28 | optional int32 x_coord = 2 [(meta) = "{:max 100.0 :min -100.0}"]; 29 | optional int32 y_coord = 3; 30 | optional int32 width = 4; 31 | optional int32 height = 5; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /resources/proto/flatland/protobuf/test/maps.proto: -------------------------------------------------------------------------------- 1 | package flatland.protobuf.test.maps; 2 | 3 | import "flatland/protobuf/extensions.proto"; 4 | 5 | option java_package = "flatland.protobuf.test"; 6 | option java_outer_classname = "Maps"; 7 | 8 | message Element { 9 | optional string id = 1; 10 | optional int32 foo = 2; 11 | optional int32 bar = 3; 12 | optional bool deleted = 4; 13 | optional bool exists = 5; 14 | } 15 | 16 | message Pair { 17 | required string key = 1; 18 | required Element val = 2; 19 | } 20 | 21 | message Struct { 22 | repeated Pair element_map = 1 [(map) = true]; 23 | repeated Pair element_map_e = 2 [(map) = true, (map_exists) = "exists"]; 24 | repeated Pair element_map_d = 3 [(map) = true, (map_deleted) = "deleted"]; 25 | 26 | repeated Element element_by_id = 4 [(map_by) = "id"]; 27 | repeated Element element_by_id_e = 5 [(map_by) = "id", (map_exists) = "exists"]; 28 | repeated Element element_by_id_d = 6 [(map_by) = "id", (map_deleted) = "deleted"]; 29 | } 30 | -------------------------------------------------------------------------------- /src/flatland/protobuf/PersistentProtocolBufferMap.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Justin Balthrop. All rights reserved. 3 | * The use and distribution terms for this software are covered by the 4 | * Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) 5 | * which can be found in the file epl-v10.html at the root of this distribution. 6 | * By using this software in any fashion, you are agreeing to be bound by 7 | * the terms of this license. 8 | * You must not remove this notice, or any other, from this software. 9 | **/ 10 | 11 | package flatland.protobuf; 12 | 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.io.OutputStream; 16 | import java.io.PrintWriter; 17 | import java.lang.reflect.InvocationTargetException; 18 | import java.util.ArrayList; 19 | import java.util.Iterator; 20 | import java.util.List; 21 | import java.util.Map; 22 | import java.util.concurrent.ConcurrentHashMap; 23 | 24 | import ordered_map.core.OrderedMap; 25 | import ordered_set.core.OrderedSet; 26 | import clojure.lang.APersistentMap; 27 | import clojure.lang.ASeq; 28 | import clojure.lang.IFn; 29 | import clojure.lang.IMapEntry; 30 | import clojure.lang.IObj; 31 | import clojure.lang.IPersistentCollection; 32 | import clojure.lang.IPersistentMap; 33 | import clojure.lang.IPersistentVector; 34 | import clojure.lang.ISeq; 35 | import clojure.lang.ITransientMap; 36 | import clojure.lang.ITransientSet; 37 | import clojure.lang.Keyword; 38 | import clojure.lang.MapEntry; 39 | import clojure.lang.Numbers; 40 | import clojure.lang.Obj; 41 | import clojure.lang.PersistentArrayMap; 42 | import clojure.lang.PersistentVector; 43 | import clojure.lang.RT; 44 | import clojure.lang.SeqIterator; 45 | import clojure.lang.Sequential; 46 | import clojure.lang.Symbol; 47 | import clojure.lang.Var; 48 | 49 | import com.google.protobuf.CodedInputStream; 50 | import com.google.protobuf.CodedOutputStream; 51 | import com.google.protobuf.DescriptorProtos; 52 | import com.google.protobuf.DescriptorProtos.FieldOptions; 53 | import com.google.protobuf.Descriptors; 54 | import com.google.protobuf.DynamicMessage; 55 | import com.google.protobuf.GeneratedMessage; 56 | import com.google.protobuf.InvalidProtocolBufferException; 57 | 58 | 59 | public class PersistentProtocolBufferMap extends APersistentMap implements IObj { 60 | public static class Def { 61 | public static interface NamingStrategy { 62 | /** 63 | * Given a Clojure map key, return the string to be used as the protobuf message field name. 64 | */ 65 | String protoName(Object clojureName); 66 | 67 | /** 68 | * Given a protobuf message field name, return a Clojure object suitable for use as a map key. 69 | */ 70 | Object clojureName(String protoName); 71 | } 72 | 73 | // we want this to work for anything Named, so use clojure.core/name 74 | public static final Var NAME_VAR = Var.intern(RT.CLOJURE_NS, Symbol.intern("name")); 75 | 76 | public static final String nameStr(Object named) { 77 | try { 78 | return (String)((IFn)NAME_VAR.deref()).invoke(named); 79 | } catch (Exception e) { 80 | return null; 81 | } 82 | } 83 | 84 | public static final NamingStrategy protobufNames = new NamingStrategy() { 85 | @Override 86 | public String protoName(Object name) { 87 | return nameStr(name); 88 | } 89 | 90 | @Override 91 | public Object clojureName(String name) { 92 | return Keyword.intern(name.toLowerCase()); 93 | } 94 | 95 | @Override 96 | public String toString() { 97 | return "[protobuf names]"; 98 | } 99 | }; 100 | public static final NamingStrategy convertUnderscores = new NamingStrategy() { 101 | @Override 102 | public String protoName(Object name) { 103 | return nameStr(name).replaceAll("-", "_"); 104 | } 105 | 106 | @Override 107 | public Object clojureName(String name) { 108 | return Keyword.intern(name.replaceAll("_", "-").toLowerCase()); 109 | } 110 | 111 | @Override 112 | public String toString() { 113 | return "[convert underscores]"; 114 | } 115 | }; 116 | 117 | public final Descriptors.Descriptor type; 118 | public final NamingStrategy namingStrategy; 119 | public final int sizeLimit; 120 | 121 | public static final Object NULL = new Object(); 122 | // keys should be FieldDescriptors, except that NULL is used as a replacement for real null 123 | ConcurrentHashMap key_to_field; 124 | 125 | private static final class DefOptions { 126 | public final Descriptors.Descriptor type; 127 | public final NamingStrategy strat; 128 | public final int sizeLimit; 129 | public DefOptions(Descriptors.Descriptor type, NamingStrategy strat, int sizeLimit) { 130 | this.type = type; 131 | this.strat = strat; 132 | this.sizeLimit = sizeLimit; 133 | } 134 | 135 | public boolean equals(Object other) { 136 | if (this.getClass() != other.getClass()) 137 | return false; 138 | DefOptions od = (DefOptions)other; 139 | return type.equals(od.type) && strat.equals(od.strat) && sizeLimit == od.sizeLimit; 140 | } 141 | 142 | public int hashCode() { 143 | return type.hashCode() + strat.hashCode() + sizeLimit; 144 | } 145 | } 146 | 147 | static ConcurrentHashMap defCache = new ConcurrentHashMap(); 148 | 149 | public static Def create(Descriptors.Descriptor type, NamingStrategy strat, int sizeLimit) { 150 | DefOptions opts = new DefOptions(type, strat, sizeLimit); 151 | 152 | Def def = defCache.get(type); 153 | if (def == null) { 154 | def = new Def(type, strat, sizeLimit); 155 | defCache.putIfAbsent(opts, def); 156 | } 157 | return def; 158 | } 159 | 160 | protected Def(Descriptors.Descriptor type, NamingStrategy strat, int sizeLimit) { 161 | this.type = type; 162 | this.key_to_field = new ConcurrentHashMap(); 163 | this.namingStrategy = strat; 164 | this.sizeLimit = sizeLimit; 165 | } 166 | 167 | public DynamicMessage parseFrom(byte[] bytes) throws InvalidProtocolBufferException { 168 | return DynamicMessage.parseFrom(type, bytes); 169 | } 170 | 171 | public DynamicMessage parseFrom(CodedInputStream input) throws IOException { 172 | input.setSizeLimit(sizeLimit); 173 | return DynamicMessage.parseFrom(type, input); 174 | } 175 | 176 | public DynamicMessage.Builder parseDelimitedFrom(InputStream input) throws IOException { 177 | DynamicMessage.Builder builder = newBuilder(); 178 | if (builder.mergeDelimitedFrom(input)) { 179 | return builder; 180 | } else { 181 | return null; 182 | } 183 | } 184 | 185 | public DynamicMessage.Builder newBuilder() { 186 | return DynamicMessage.newBuilder(type); 187 | } 188 | 189 | public Descriptors.FieldDescriptor fieldDescriptor(Object key) { 190 | if (key == null) { 191 | return null; 192 | } 193 | 194 | if (key instanceof Descriptors.FieldDescriptor) { 195 | return (Descriptors.FieldDescriptor)key; 196 | } else { 197 | Object field = key_to_field.get(key); 198 | if (field != null) { 199 | if (field == NULL) { 200 | return null; 201 | } 202 | return (Descriptors.FieldDescriptor)field; 203 | } else { 204 | field = type.findFieldByName(namingStrategy.protoName(key)); 205 | key_to_field.putIfAbsent(key, field == null ? NULL : field); 206 | } 207 | return (Descriptors.FieldDescriptor)field; 208 | } 209 | } 210 | 211 | public String getName() { 212 | return type.getName(); 213 | } 214 | 215 | public String getFullName() { 216 | return type.getFullName(); 217 | } 218 | 219 | public Descriptors.Descriptor getMessageType() { 220 | return type; 221 | } 222 | 223 | static final ConcurrentHashMap> caches = new ConcurrentHashMap>(); 224 | static final Object nullv = new Object(); 225 | 226 | public Object intern(String name) { 227 | ConcurrentHashMap nameCache = caches.get(namingStrategy); 228 | if (nameCache == null) { 229 | nameCache = new ConcurrentHashMap(); 230 | ConcurrentHashMap existing = caches.putIfAbsent(namingStrategy, nameCache); 231 | if (existing != null) { 232 | nameCache = existing; 233 | } 234 | } 235 | Object clojureName = nameCache.get(name); 236 | if (clojureName == null) { 237 | if (name == "") { 238 | clojureName = nullv; 239 | } else { 240 | clojureName = namingStrategy.clojureName(name); 241 | if (clojureName == null) { 242 | clojureName = nullv; 243 | } 244 | } 245 | Object existing = nameCache.putIfAbsent(name, clojureName); 246 | if (existing != null) { 247 | clojureName = existing; 248 | } 249 | } 250 | return clojureName == nullv ? null : clojureName; 251 | } 252 | 253 | public Object clojureEnumValue(Descriptors.EnumValueDescriptor enum_value) { 254 | return intern(enum_value.getName()); 255 | } 256 | 257 | protected Object mapFieldBy(Descriptors.FieldDescriptor field) { 258 | return intern(field.getOptions().getExtension(Extensions.mapBy)); 259 | } 260 | 261 | protected PersistentProtocolBufferMap mapValue(Descriptors.FieldDescriptor field, 262 | PersistentProtocolBufferMap left, 263 | PersistentProtocolBufferMap right) { 264 | if (left == null) { 265 | return right; 266 | } else { 267 | Object map_exists = intern(field.getOptions().getExtension(Extensions.mapExists)); 268 | if (map_exists != null) { 269 | if (left.valAt(map_exists) == Boolean.FALSE && 270 | right.valAt(map_exists) == Boolean.TRUE) { 271 | return right; 272 | } else { 273 | return left.append(right); 274 | } 275 | } 276 | 277 | Object map_deleted = intern(field.getOptions().getExtension(Extensions.mapDeleted)); 278 | if (map_deleted != null) { 279 | if (left.valAt(map_deleted) == Boolean.TRUE && 280 | right.valAt(map_deleted) == Boolean.FALSE) { 281 | return right; 282 | } else { 283 | return left.append(right); 284 | } 285 | } 286 | return left.append(right); 287 | } 288 | } 289 | } 290 | 291 | public final Def def; 292 | private final DynamicMessage message; 293 | private final IPersistentMap _meta; 294 | private final IPersistentMap ext; 295 | 296 | static public PersistentProtocolBufferMap create(Def def, byte[] bytes) 297 | throws InvalidProtocolBufferException { 298 | DynamicMessage message = def.parseFrom(bytes); 299 | return new PersistentProtocolBufferMap(null, def, message); 300 | } 301 | 302 | static public PersistentProtocolBufferMap parseFrom(Def def, CodedInputStream input) 303 | throws IOException { 304 | DynamicMessage message = def.parseFrom(input); 305 | return new PersistentProtocolBufferMap(null, def, message); 306 | } 307 | 308 | static public PersistentProtocolBufferMap parseDelimitedFrom(Def def, InputStream input) 309 | throws IOException { 310 | DynamicMessage.Builder builder = def.parseDelimitedFrom(input); 311 | if (builder != null) { 312 | return new PersistentProtocolBufferMap(null, def, builder); 313 | } else { 314 | return null; 315 | } 316 | } 317 | 318 | static public PersistentProtocolBufferMap construct(Def def, Object keyvals) { 319 | PersistentProtocolBufferMap protobuf = new PersistentProtocolBufferMap(null, def); 320 | return protobuf.cons(keyvals); 321 | } 322 | 323 | protected PersistentProtocolBufferMap(IPersistentMap meta, Def def) { 324 | this._meta = meta; 325 | this.ext = null; 326 | this.def = def; 327 | this.message = null; 328 | } 329 | 330 | protected PersistentProtocolBufferMap(IPersistentMap meta, Def def, DynamicMessage message) { 331 | this._meta = meta; 332 | this.ext = null; 333 | this.def = def; 334 | this.message = message; 335 | } 336 | 337 | protected PersistentProtocolBufferMap(IPersistentMap meta, IPersistentMap ext, Def def, 338 | DynamicMessage message) { 339 | this._meta = meta; 340 | this.ext = ext; 341 | this.def = def; 342 | this.message = message; 343 | } 344 | 345 | protected PersistentProtocolBufferMap(IPersistentMap meta, Def def, DynamicMessage.Builder builder) { 346 | this._meta = meta; 347 | this.ext = null; 348 | this.def = def; 349 | this.message = builder.build(); 350 | } 351 | 352 | protected PersistentProtocolBufferMap(IPersistentMap meta, IPersistentMap ext, Def def, 353 | DynamicMessage.Builder builder) { 354 | this._meta = meta; 355 | this.ext = ext; 356 | this.def = def; 357 | this.message = builder.build(); 358 | } 359 | 360 | public byte[] toByteArray() { 361 | return message().toByteArray(); 362 | } 363 | 364 | public void writeTo(CodedOutputStream output) throws IOException { 365 | message().writeTo(output); 366 | } 367 | 368 | public void writeDelimitedTo(OutputStream output) throws IOException { 369 | message().writeDelimitedTo(output); 370 | } 371 | 372 | public Descriptors.Descriptor getMessageType() { 373 | return def.getMessageType(); 374 | } 375 | 376 | public DynamicMessage message() { 377 | if (message == null) { 378 | return def.newBuilder().build(); // This will only work if an empty message is valid. 379 | } else { 380 | return message; 381 | } 382 | } 383 | 384 | public DynamicMessage.Builder builder() { 385 | if (message == null) { 386 | return def.newBuilder(); 387 | } else { 388 | return message.toBuilder(); 389 | } 390 | } 391 | 392 | protected Object fromProtoValue(Descriptors.FieldDescriptor field, Object value) { 393 | return fromProtoValue(field, value, true); 394 | } 395 | 396 | static Keyword k_key = Keyword.intern("key"); 397 | static Keyword k_val = Keyword.intern("val"); 398 | static Keyword k_item = Keyword.intern("item"); 399 | static Keyword k_exists = Keyword.intern("exists"); 400 | 401 | protected Object fromProtoValue(Descriptors.FieldDescriptor field, Object value, 402 | boolean use_extensions) { 403 | if (value instanceof List) { 404 | List values = (List)value; 405 | Iterator iterator = values.iterator(); 406 | 407 | if (use_extensions) { 408 | Object map_field_by = def.mapFieldBy(field); 409 | DescriptorProtos.FieldOptions options = field.getOptions(); 410 | if (map_field_by != null) { 411 | ITransientMap map = (ITransientMap)OrderedMap.EMPTY.asTransient(); 412 | while (iterator.hasNext()) { 413 | PersistentProtocolBufferMap v = 414 | (PersistentProtocolBufferMap)fromProtoValue(field, iterator.next()); 415 | Object k = v.valAt(map_field_by); 416 | PersistentProtocolBufferMap existing = (PersistentProtocolBufferMap)map.valAt(k); 417 | map = map.assoc(k, def.mapValue(field, existing, v)); 418 | } 419 | return map.persistent(); 420 | } else if (options.getExtension(Extensions.counter)) { 421 | Object count = iterator.next(); 422 | while (iterator.hasNext()) { 423 | count = Numbers.add(count, iterator.next()); 424 | } 425 | return count; 426 | } else if (options.getExtension(Extensions.succession)) { 427 | return fromProtoValue(field, values.get(values.size() - 1)); 428 | } else if (options.getExtension(Extensions.map)) { 429 | Descriptors.Descriptor type = field.getMessageType(); 430 | Descriptors.FieldDescriptor key_field = type.findFieldByName("key"); 431 | Descriptors.FieldDescriptor val_field = type.findFieldByName("val"); 432 | 433 | ITransientMap map = (ITransientMap)OrderedMap.EMPTY.asTransient(); 434 | while (iterator.hasNext()) { 435 | DynamicMessage message = (DynamicMessage)iterator.next(); 436 | Object k = fromProtoValue(key_field, message.getField(key_field)); 437 | Object v = fromProtoValue(val_field, message.getField(val_field)); 438 | Object existing = map.valAt(k); 439 | 440 | if (existing instanceof PersistentProtocolBufferMap) { 441 | map = map.assoc(k, def.mapValue(field, 442 | (PersistentProtocolBufferMap)existing, 443 | (PersistentProtocolBufferMap)v)); 444 | } else if (existing instanceof IPersistentCollection) { 445 | map = map.assoc(k, ((IPersistentCollection)existing).cons(v)); 446 | } else { 447 | map = map.assoc(k, v); 448 | } 449 | } 450 | return map.persistent(); 451 | } else if (options.getExtension(Extensions.set)) { 452 | Descriptors.Descriptor type = field.getMessageType(); 453 | Descriptors.FieldDescriptor item_field = type.findFieldByName("item"); 454 | Descriptors.FieldDescriptor exists_field = type.findFieldByName("exists"); 455 | 456 | ITransientSet set = (ITransientSet)OrderedSet.EMPTY.asTransient(); 457 | while (iterator.hasNext()) { 458 | DynamicMessage message = (DynamicMessage)iterator.next(); 459 | Object item = fromProtoValue(item_field, message.getField(item_field)); 460 | Boolean exists = (Boolean)message.getField(exists_field); 461 | 462 | if (exists) { 463 | set = (ITransientSet)set.conj(item); 464 | } else { 465 | try { 466 | set = set.disjoin(item); 467 | } catch (Exception e) { 468 | e.printStackTrace(); 469 | } 470 | } 471 | } 472 | return set.persistent(); 473 | } 474 | } 475 | List list = new ArrayList(values.size()); 476 | while (iterator.hasNext()) { 477 | list.add(fromProtoValue(field, iterator.next(), use_extensions)); 478 | } 479 | return PersistentVector.create(list); 480 | } else { 481 | switch (field.getJavaType()) { 482 | case ENUM: 483 | Descriptors.EnumValueDescriptor e = (Descriptors.EnumValueDescriptor)value; 484 | if (use_extensions && 485 | field.getOptions().getExtension(Extensions.nullable) && 486 | field.getOptions().getExtension(nullExtension(field)).equals(e.getNumber())) { 487 | return null; 488 | } else { 489 | return def.clojureEnumValue(e); 490 | } 491 | case MESSAGE: 492 | Def fieldDef = PersistentProtocolBufferMap.Def.create(field.getMessageType(), 493 | this.def.namingStrategy, 494 | this.def.sizeLimit); 495 | DynamicMessage message = (DynamicMessage)value; 496 | 497 | // Total hack because getField() doesn't return an empty array for repeated messages. 498 | if (field.isRepeated() && !message.isInitialized()) { 499 | return fromProtoValue(field, new ArrayList(), use_extensions); 500 | } 501 | 502 | return new PersistentProtocolBufferMap(null, fieldDef, message); 503 | default: 504 | if (use_extensions && 505 | field.getOptions().getExtension(Extensions.nullable) && 506 | field.getOptions().getExtension(nullExtension(field)).equals(value)) { 507 | return null; 508 | } else { 509 | return value; 510 | } 511 | } 512 | } 513 | } 514 | 515 | protected Object toProtoValue(Descriptors.FieldDescriptor field, Object value) { 516 | if (value == null && field.getOptions().getExtension(Extensions.nullable)) { 517 | value = field.getOptions().getExtension(nullExtension(field)); 518 | 519 | if (field.getJavaType() == Descriptors.FieldDescriptor.JavaType.ENUM) { 520 | Descriptors.EnumDescriptor enum_type = field.getEnumType(); 521 | Descriptors.EnumValueDescriptor enum_value = enum_type.findValueByNumber((Integer)value); 522 | if (enum_value == null) { 523 | PrintWriter err = (PrintWriter)RT.ERR.deref(); 524 | err.format("invalid enum number %s for enum type %s\n", value, enum_type.getFullName()); 525 | } 526 | return enum_value; 527 | } 528 | } 529 | 530 | switch (field.getJavaType()) { 531 | case LONG: 532 | return ((Number)value).longValue(); 533 | case INT: 534 | return ((Number)value).intValue(); 535 | case FLOAT: 536 | return ((Number)value).floatValue(); 537 | case DOUBLE: 538 | return ((Number)value).doubleValue(); 539 | case ENUM: 540 | String name = def.namingStrategy.protoName(value); 541 | Descriptors.EnumDescriptor enum_type = field.getEnumType(); 542 | Descriptors.EnumValueDescriptor enum_value = enum_type.findValueByName(name); 543 | if (enum_value == null) { 544 | PrintWriter err = (PrintWriter)RT.ERR.deref(); 545 | err.format("invalid enum value %s for enum type %s\n", name, enum_type.getFullName()); 546 | } 547 | return enum_value; 548 | case MESSAGE: 549 | PersistentProtocolBufferMap protobuf; 550 | if (value instanceof PersistentProtocolBufferMap) { 551 | protobuf = (PersistentProtocolBufferMap)value; 552 | } else { 553 | Def fieldDef = PersistentProtocolBufferMap.Def.create(field.getMessageType(), 554 | this.def.namingStrategy, 555 | this.def.sizeLimit); 556 | protobuf = PersistentProtocolBufferMap.construct(fieldDef, value); 557 | } 558 | return protobuf.message(); 559 | default: 560 | return value; 561 | } 562 | } 563 | 564 | static protected GeneratedMessage.GeneratedExtension nullExtension( 565 | Descriptors.FieldDescriptor field) { 566 | switch (field.getJavaType()) { 567 | case LONG: 568 | return Extensions.nullLong; 569 | case INT: 570 | return Extensions.nullInt; 571 | case FLOAT: 572 | return Extensions.nullFloat; 573 | case DOUBLE: 574 | return Extensions.nullDouble; 575 | case STRING: 576 | return Extensions.nullString; 577 | case ENUM: 578 | return Extensions.nullEnum; 579 | default: 580 | return null; 581 | } 582 | } 583 | 584 | protected void addRepeatedField(DynamicMessage.Builder builder, 585 | Descriptors.FieldDescriptor field, Object value) { 586 | try { 587 | builder.addRepeatedField(field, value); 588 | } catch (Exception e) { 589 | String msg = String.format("error adding %s to %s field %s", value, 590 | field.getJavaType().toString().toLowerCase(), field.getFullName()); 591 | throw new IllegalArgumentException(msg, e); 592 | } 593 | } 594 | 595 | protected void setField(DynamicMessage.Builder builder, Descriptors.FieldDescriptor field, 596 | Object value) { 597 | try { 598 | builder.setField(field, value); 599 | } catch (IllegalArgumentException e) { 600 | String msg = String.format("error setting %s field %s to %s", 601 | field.getJavaType().toString().toLowerCase(), field.getFullName(), value); 602 | throw new IllegalArgumentException(msg, e); 603 | } 604 | } 605 | 606 | // returns true if the protobuf can store this key 607 | protected boolean addField(DynamicMessage.Builder builder, Object key, Object value) { 608 | if (key == null) { 609 | return false; 610 | } 611 | Descriptors.FieldDescriptor field = def.fieldDescriptor(key); 612 | if (field == null) { 613 | return false; 614 | } 615 | if (value == null && !(field.getOptions().getExtension(Extensions.nullable))) { 616 | return true; 617 | } 618 | boolean set = field.getOptions().getExtension(Extensions.set); 619 | 620 | if (field.isRepeated()) { 621 | builder.clearField(field); 622 | if (value instanceof Sequential && !set) { 623 | for (ISeq s = RT.seq(value); s != null; s = s.next()) { 624 | Object v = toProtoValue(field, s.first()); 625 | addRepeatedField(builder, field, v); 626 | } 627 | } else { 628 | Object map_field_by = def.mapFieldBy(field); 629 | if (map_field_by != null) { 630 | String field_name = def.namingStrategy.protoName(map_field_by); 631 | for (ISeq s = RT.seq(value); s != null; s = s.next()) { 632 | Map.Entry e = (Map.Entry)s.first(); 633 | IPersistentMap map = (IPersistentMap)e.getValue(); 634 | Object k = e.getKey(); 635 | Object v = toProtoValue(field, map.assoc(map_field_by, k).assoc(field_name, k)); 636 | addRepeatedField(builder, field, v); 637 | } 638 | } else if (field.getOptions().getExtension(Extensions.map)) { 639 | for (ISeq s = RT.seq(value); s != null; s = s.next()) { 640 | Map.Entry e = (Map.Entry)s.first(); 641 | Object[] map = {k_key, e.getKey(), k_val, e.getValue()}; 642 | addRepeatedField(builder, field, toProtoValue(field, new PersistentArrayMap(map))); 643 | } 644 | } else if (set) { 645 | Object k, v; 646 | boolean isMap = (value instanceof IPersistentMap); 647 | for (ISeq s = RT.seq(value); s != null; s = s.next()) { 648 | if (isMap) { 649 | Map.Entry e = (Map.Entry)s.first(); 650 | k = e.getKey(); 651 | v = e.getValue(); 652 | } else { 653 | k = s.first(); 654 | v = true; 655 | } 656 | Object[] map = {k_item, k, k_exists, v}; 657 | addRepeatedField(builder, field, toProtoValue(field, new PersistentArrayMap(map))); 658 | } 659 | } else { 660 | addRepeatedField(builder, field, toProtoValue(field, value)); 661 | } 662 | } 663 | } else { 664 | Object v = toProtoValue(field, value); 665 | if (v instanceof DynamicMessage) { 666 | v = ((DynamicMessage)builder.getField(field)).toBuilder().mergeFrom((DynamicMessage)v).build(); 667 | } 668 | setField(builder, field, v); 669 | } 670 | 671 | return true; 672 | } 673 | 674 | @Override 675 | public PersistentProtocolBufferMap withMeta(IPersistentMap meta) { 676 | if (meta == meta()) { 677 | return this; 678 | } 679 | return new PersistentProtocolBufferMap(meta, ext, def, message); 680 | } 681 | 682 | @Override 683 | public IPersistentMap meta() { 684 | return _meta; 685 | } 686 | 687 | @Override 688 | public boolean containsKey(Object key) { 689 | return protoContainsKey(key) || RT.booleanCast(RT.contains(ext, key)); 690 | } 691 | 692 | private boolean protoContainsKey(Object key) { 693 | Descriptors.FieldDescriptor field = def.fieldDescriptor(key); 694 | if (field == null) { 695 | return false; 696 | } else if (field.isRepeated()) { 697 | return message().getRepeatedFieldCount(field) > 0; 698 | } else { 699 | return message().hasField(field) || field.hasDefaultValue(); 700 | } 701 | } 702 | 703 | private static final Object sentinel = new Object(); 704 | 705 | @Override 706 | public IMapEntry entryAt(Object key) { 707 | Object value = valAt(key, sentinel); 708 | return (value == sentinel) ? null : new MapEntry(key, value); 709 | } 710 | 711 | @Override 712 | public Object valAt(Object key) { 713 | return getValAt(key, true); 714 | } 715 | 716 | @Override 717 | public Object valAt(Object key, Object notFound) { 718 | return getValAt(key, notFound, true); 719 | } 720 | 721 | public Object getValAt(Object key, boolean use_extensions) { 722 | Object val = getValAt(key, sentinel, use_extensions); 723 | return (val == sentinel) ? null : val; 724 | } 725 | 726 | public Object getValAt(Object key, Object notFound, boolean use_extensions) { 727 | Descriptors.FieldDescriptor field = def.fieldDescriptor(key); 728 | if (protoContainsKey(key)) { 729 | return fromProtoValue(field, message().getField(field), use_extensions); 730 | } else { 731 | return RT.get(ext, key, notFound); 732 | } 733 | } 734 | 735 | @Override 736 | public PersistentProtocolBufferMap assoc(Object key, Object value) { 737 | DynamicMessage.Builder builder = builder(); 738 | 739 | if (addField(builder, key, value)) { 740 | return new PersistentProtocolBufferMap(meta(), ext, def, builder); 741 | } else { 742 | return new PersistentProtocolBufferMap(meta(), (IPersistentMap)RT.assoc(ext, key, value), def, builder); 743 | } 744 | } 745 | 746 | @Override 747 | public PersistentProtocolBufferMap assocEx(Object key, Object value) { 748 | if (containsKey(key)) { 749 | throw new RuntimeException("Key already present"); 750 | } 751 | return assoc(key, value); 752 | } 753 | 754 | @Override 755 | public PersistentProtocolBufferMap cons(Object o) { 756 | if (o instanceof Map.Entry) { 757 | Map.Entry e = (Map.Entry)o; 758 | return assoc(e.getKey(), e.getValue()); 759 | } else if (o instanceof IPersistentVector) { 760 | IPersistentVector v = (IPersistentVector)o; 761 | if (v.count() != 2) { 762 | throw new IllegalArgumentException("Vector arg to map conj must be a pair"); 763 | } 764 | return assoc(v.nth(0), v.nth(1)); 765 | } else { 766 | DynamicMessage.Builder builder = builder(); 767 | IPersistentMap ext = this.ext; 768 | for (ISeq s = RT.seq(o); s != null; s = s.next()) { 769 | Map.Entry e = (Map.Entry)s.first(); 770 | 771 | Object k = e.getKey(), v = e.getValue(); 772 | if (!addField(builder, k, v)) { 773 | ext = (IPersistentMap)RT.assoc(ext, k, v); 774 | } 775 | } 776 | return new PersistentProtocolBufferMap(meta(), ext, def, builder); 777 | } 778 | } 779 | 780 | public PersistentProtocolBufferMap append(IPersistentMap map) { 781 | PersistentProtocolBufferMap proto; 782 | if (map instanceof PersistentProtocolBufferMap) { 783 | proto = (PersistentProtocolBufferMap)map; 784 | } else { 785 | proto = construct(def, map); 786 | } 787 | return new PersistentProtocolBufferMap(meta(), ext, def, builder().mergeFrom(proto.message())); 788 | } 789 | 790 | @Override 791 | public IPersistentMap without(Object key) { 792 | Descriptors.FieldDescriptor field = def.fieldDescriptor(key); 793 | if (field == null) { 794 | IPersistentMap newExt = (IPersistentMap)RT.dissoc(ext, key); 795 | if (newExt == ext) { 796 | return this; 797 | } 798 | return new PersistentProtocolBufferMap(meta(), newExt, def, builder()); 799 | } 800 | if (field.isRequired()) { 801 | throw new RuntimeException("Can't remove required field"); 802 | } 803 | 804 | return new PersistentProtocolBufferMap(meta(), ext, def, builder().clearField(field)); 805 | } 806 | 807 | @Override 808 | public Iterator iterator() { 809 | return new SeqIterator(seq()); 810 | } 811 | 812 | @Override 813 | public int count() { 814 | int count = RT.count(ext); 815 | for (Descriptors.FieldDescriptor field : def.type.getFields()) { 816 | if (protoContainsKey(field)) { 817 | count++; 818 | } 819 | } 820 | return count; 821 | } 822 | 823 | @Override 824 | public ISeq seq() { 825 | return Seq.create(null, this, RT.seq(def.type.getFields())); 826 | } 827 | 828 | @Override 829 | public IPersistentCollection empty() { 830 | return new PersistentProtocolBufferMap(meta(), null, def, builder().clear()); 831 | } 832 | 833 | private static class Seq extends ASeq { 834 | private final PersistentProtocolBufferMap proto; 835 | private final MapEntry first; 836 | private final ISeq fields; 837 | 838 | public static ISeq create(IPersistentMap meta, PersistentProtocolBufferMap proto, ISeq fields) { 839 | for (ISeq s = fields; s != null; s = s.next()) { 840 | Descriptors.FieldDescriptor field = (Descriptors.FieldDescriptor)s.first(); 841 | Object k = proto.def.intern(field.getName()); 842 | Object v = proto.valAt(k, sentinel); 843 | if (v != sentinel) { 844 | return new Seq(meta, proto, new MapEntry(k, v), s); 845 | } 846 | } 847 | return RT.seq(proto.ext); 848 | } 849 | 850 | protected Seq(IPersistentMap meta, PersistentProtocolBufferMap proto, MapEntry first, 851 | ISeq fields) { 852 | super(meta); 853 | this.proto = proto; 854 | this.first = first; 855 | this.fields = fields; 856 | } 857 | 858 | @Override 859 | public Obj withMeta(IPersistentMap meta) { 860 | if (meta != meta()) { 861 | return new Seq(meta, proto, first, fields); 862 | } 863 | return this; 864 | } 865 | 866 | @Override 867 | public Object first() { 868 | return first; 869 | } 870 | 871 | @Override 872 | public ISeq next() { 873 | return create(meta(), proto, fields.next()); 874 | } 875 | } 876 | } 877 | -------------------------------------------------------------------------------- /src/flatland/protobuf/codec.clj: -------------------------------------------------------------------------------- 1 | (ns flatland.protobuf.codec 2 | (:use flatland.protobuf.core 3 | [gloss.core.protocols :only [Reader Writer]] 4 | [gloss.core.formats :only [to-buf-seq]] 5 | [flatland.useful.fn :only [fix]] 6 | [flatland.useful.experimental :only [lift-meta]] 7 | [clojure.java.io :only [input-stream]]) 8 | 9 | ;; flatland.io extends Seqable so we can concat InputStream from 10 | ;; ByteBuffer sequences. 11 | (:require flatland.io.core 12 | [flatland.schematic.core :as schema] 13 | [gloss.core :as gloss])) 14 | 15 | (declare protobuf-codec) 16 | 17 | (def ^{:private true} len-key :proto_length) 18 | (def ^{:private true} reset-key :codec_reset) 19 | 20 | (defn length-prefix [proto] 21 | (let [proto (protodef proto) 22 | min (alength (protobuf-dump proto {len-key 0})) 23 | max (alength (protobuf-dump proto {len-key Integer/MAX_VALUE}))] 24 | (letfn [(check [test msg] 25 | (when-not test 26 | (throw (Exception. (format "In %s: %s %s" 27 | (.getFullName proto) (name len-key) msg)))))] 28 | (check (pos? min) 29 | "field is required for repeated protobufs") 30 | (check (= min max) 31 | "must be of type fixed32 or fixed64")) 32 | (gloss/compile-frame (gloss/finite-frame max (protobuf-codec proto)) 33 | #(hash-map len-key %) 34 | len-key))) 35 | 36 | (defn protobuf-codec [proto & {:keys [validator repeated]}] 37 | (let [proto (protodef proto)] 38 | (-> (reify 39 | Reader 40 | (read-bytes [this buf-seq] 41 | [true (protobuf-load-stream proto (input-stream buf-seq)) nil]) 42 | Writer 43 | (sizeof [this] nil) 44 | (write-bytes [this _ val] 45 | (when (and validator (not (validator val))) 46 | (throw (IllegalStateException. "Invalid value in protobuf-codec"))) 47 | (to-buf-seq 48 | (protobuf-dump 49 | (if (protobuf? val) 50 | val 51 | (protobuf proto val)))))) 52 | (fix repeated 53 | #(gloss/repeated (gloss/finite-frame (length-prefix proto) %) 54 | :prefix :none))))) 55 | 56 | (defn codec-schema [proto] 57 | (schema/dissoc-fields (protobuf-schema proto) 58 | len-key reset-key)) 59 | -------------------------------------------------------------------------------- /src/flatland/protobuf/core.clj: -------------------------------------------------------------------------------- 1 | (ns flatland.protobuf.core 2 | (:use [flatland.protobuf.schema :only [field-schema]] 3 | [flatland.useful.fn :only [fix]] 4 | [clojure.java.io :only [input-stream output-stream]]) 5 | (:require flatland.useful.utils) 6 | (:import (flatland.protobuf PersistentProtocolBufferMap PersistentProtocolBufferMap$Def PersistentProtocolBufferMap$Def$NamingStrategy Extensions) 7 | (com.google.protobuf GeneratedMessage CodedInputStream Descriptors$Descriptor) 8 | (java.io InputStream OutputStream) 9 | (clojure.lang Reflector))) 10 | 11 | (defn protobuf? 12 | "Is the given object a PersistentProtocolBufferMap?" 13 | [obj] 14 | (instance? PersistentProtocolBufferMap obj)) 15 | 16 | (defn protodef? 17 | "Is the given object a PersistentProtocolBufferMap$Def?" 18 | [obj] 19 | (instance? PersistentProtocolBufferMap$Def obj)) 20 | 21 | (defn ^PersistentProtocolBufferMap$Def protodef 22 | "Create a protodef from a string or protobuf class." 23 | ([def] 24 | (if (or (protodef? def) (nil? def)) 25 | def 26 | (protodef def {}))) 27 | ([def opts] 28 | (when def 29 | (let [{:keys [^PersistentProtocolBufferMap$Def$NamingStrategy naming-strategy 30 | size-limit] 31 | :or {naming-strategy PersistentProtocolBufferMap$Def/convertUnderscores 32 | size-limit 67108864}} opts ;; 64MiB 33 | ^Descriptors$Descriptor descriptor 34 | (if (instance? Descriptors$Descriptor def) 35 | def 36 | (Reflector/invokeStaticMethod ^Class def "getDescriptor" (to-array nil)))] 37 | (PersistentProtocolBufferMap$Def/create descriptor naming-strategy size-limit))))) 38 | 39 | (defn protobuf 40 | "Construct a protobuf of the given type." 41 | ([^PersistentProtocolBufferMap$Def type] 42 | (PersistentProtocolBufferMap/construct type {})) 43 | ([^PersistentProtocolBufferMap$Def type m] 44 | (PersistentProtocolBufferMap/construct type m)) 45 | ([^PersistentProtocolBufferMap$Def type k v & kvs] 46 | (PersistentProtocolBufferMap/construct type (apply array-map k v kvs)))) 47 | 48 | (defn protobuf-schema 49 | "Return the schema for the given protodef." 50 | [& args] 51 | (let [^PersistentProtocolBufferMap$Def def (apply protodef args)] 52 | (field-schema (.getMessageType def) def))) 53 | 54 | (defn protobuf-load 55 | "Load a protobuf of the given type from an array of bytes." 56 | ([^PersistentProtocolBufferMap$Def type ^bytes data] 57 | (when data 58 | (PersistentProtocolBufferMap/create type data))) 59 | ([^PersistentProtocolBufferMap$Def type ^bytes data ^Integer offset ^Integer length] 60 | (when data 61 | (let [^CodedInputStream in (CodedInputStream/newInstance data offset length)] 62 | (PersistentProtocolBufferMap/parseFrom type in))))) 63 | 64 | (defn protobuf-load-stream 65 | "Load a protobuf of the given type from an InputStream." 66 | [^PersistentProtocolBufferMap$Def type ^InputStream stream] 67 | (when stream 68 | (let [^CodedInputStream in (CodedInputStream/newInstance stream)] 69 | (PersistentProtocolBufferMap/parseFrom type in)))) 70 | 71 | (defn ^"[B" protobuf-dump 72 | "Return the byte representation of the given flatland.protobuf." 73 | ([^PersistentProtocolBufferMap p] 74 | (.toByteArray p)) 75 | ([^PersistentProtocolBufferMap$Def type m] 76 | (protobuf-dump (PersistentProtocolBufferMap/construct type m)))) 77 | 78 | (defn protobuf-seq 79 | "Lazily read a sequence of length-delimited protobufs of the specified type from the given input stream." 80 | [^PersistentProtocolBufferMap$Def type in] 81 | (lazy-seq 82 | (io! 83 | (let [^InputStream in (input-stream in)] 84 | (if-let [p (PersistentProtocolBufferMap/parseDelimitedFrom type in)] 85 | (cons p (protobuf-seq type in)) 86 | (.close in)))))) 87 | 88 | (defn protobuf-write 89 | "Write the given protobufs to the given output stream, prefixing each with its length to delimit them." 90 | [out & ps] 91 | (io! 92 | (let [^OutputStream out (output-stream out)] 93 | (doseq [^PersistentProtocolBufferMap p ps] 94 | (.writeDelimitedTo p out)) 95 | (.flush out)))) 96 | 97 | (extend-protocol flatland.useful.utils/Adjoin 98 | PersistentProtocolBufferMap 99 | (adjoin-onto [^PersistentProtocolBufferMap this other] 100 | (.append this other))) 101 | 102 | ;; TODO make this nil-safe? Or just delete it? 103 | (defn get-raw 104 | "Get value at key ignoring extension fields." 105 | [^PersistentProtocolBufferMap p key] 106 | (.getValAt p key false)) 107 | -------------------------------------------------------------------------------- /src/flatland/protobuf/schema.clj: -------------------------------------------------------------------------------- 1 | (ns flatland.protobuf.schema 2 | (:use [flatland.useful.fn :only [fix]] 3 | [clojure.string :only [lower-case]]) 4 | (:import (flatland.protobuf PersistentProtocolBufferMap 5 | PersistentProtocolBufferMap$Def Extensions) 6 | (com.google.protobuf Descriptors$Descriptor 7 | Descriptors$FieldDescriptor 8 | Descriptors$FieldDescriptor$Type))) 9 | 10 | (defn extension [ext ^Descriptors$FieldDescriptor field] 11 | (-> (.getOptions field) 12 | (.getExtension ext) 13 | (fix string? not-empty))) 14 | 15 | (defn field-type [field] 16 | (condp instance? field 17 | Descriptors$FieldDescriptor 18 | (if (.isRepeated ^Descriptors$FieldDescriptor field) 19 | (condp extension field 20 | (Extensions/counter) :counter 21 | (Extensions/succession) :succession 22 | (Extensions/map) :map 23 | (Extensions/mapBy) :map-by 24 | (Extensions/set) :set 25 | :list) 26 | :basic) 27 | Descriptors$Descriptor 28 | :struct)) 29 | 30 | (defmulti field-schema (fn [field def & _] (field-type field))) 31 | 32 | (defn struct-schema [^Descriptors$Descriptor struct 33 | ^PersistentProtocolBufferMap$Def def 34 | & [parents]] 35 | (let [struct-name (.getFullName struct)] 36 | (into {:type :struct 37 | :name struct-name} 38 | (when (not-any? (partial = struct-name) parents) 39 | {:fields (into {} 40 | (for [^Descriptors$FieldDescriptor field (.getFields struct)] 41 | [(.intern def (.getName field)) 42 | (field-schema field def (conj parents struct-name))]))})))) 43 | 44 | (defn basic-schema [^Descriptors$FieldDescriptor field 45 | ^PersistentProtocolBufferMap$Def def 46 | & [parents]] 47 | (let [java-type (keyword (lower-case (.name (.getJavaType field)))) 48 | meta-string (extension (Extensions/meta) field)] 49 | (merge (case java-type 50 | :message (struct-schema (.getMessageType field) def parents) 51 | :enum {:type :enum 52 | :values (set (map #(.clojureEnumValue def %) 53 | (.. field getEnumType getValues)))} 54 | {:type java-type}) 55 | (when (.hasDefaultValue field) 56 | {:default (.getDefaultValue field)}) 57 | (when meta-string 58 | (read-string meta-string))))) 59 | 60 | (defn subfield [^Descriptors$FieldDescriptor field field-name] 61 | (.findFieldByName (.getMessageType field) (name field-name))) 62 | 63 | (defmethod field-schema :basic [field def & [parents]] 64 | (basic-schema field def parents)) 65 | 66 | (defmethod field-schema :list [field def & [parents]] 67 | {:type :list 68 | :values (basic-schema field def parents)}) 69 | 70 | (defmethod field-schema :succession [field def & [parents]] 71 | (assoc (basic-schema field def parents) 72 | :succession true)) 73 | 74 | (defmethod field-schema :counter [field def & [parents]] 75 | (assoc (basic-schema field def parents) 76 | :counter true)) 77 | 78 | (defmethod field-schema :set [field def & [parents]] 79 | {:type :set 80 | :values (field-schema (subfield field :item) def parents)}) 81 | 82 | (defmethod field-schema :map [field def & [parents]] 83 | {:type :map 84 | :keys (field-schema (subfield field :key) def parents) 85 | :values (field-schema (subfield field :val) def parents)}) 86 | 87 | (defmethod field-schema :map-by [field def & [parents]] 88 | (let [map-by (extension (Extensions/mapBy) field)] 89 | {:type :map 90 | :keys (field-schema (subfield field map-by) def parents) 91 | :values (basic-schema field def parents)})) 92 | 93 | (defmethod field-schema :struct [field def & [parents]] 94 | (struct-schema field def parents)) 95 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | Proto.java -------------------------------------------------------------------------------- /test/flatland/protobuf/codec_test.clj: -------------------------------------------------------------------------------- 1 | (ns flatland.protobuf.codec-test 2 | (:use clojure.test gloss.io flatland.protobuf.codec) 3 | (:import (java.nio ByteBuffer))) 4 | 5 | (deftest protobuf-codec-test 6 | (let [codec (protobuf-codec flatland.protobuf.test.Codec$Foo)] 7 | (testing "decode an encoded data structure" 8 | (let [val {:foo 1 :bar 2}] 9 | (is (= val (decode codec (encode codec val)))))) 10 | 11 | (testing "append two simple encoded data structures" 12 | (let [data1 (encode codec {:foo 1 :bar 2}) 13 | data2 (encode codec {:foo 4 :baz 8})] 14 | (is (= {:foo 4 :bar 2 :baz 8} 15 | (decode codec (concat data1 data2)))))) 16 | 17 | (testing "concat lists when appending" 18 | (let [data1 (encode codec {:tags ["foo" "bar"] :foo 1}) 19 | data2 (encode codec {:tags ["baz" "foo"] :foo 2})] 20 | (is (= {:foo 2 :tags ["foo" "bar" "baz" "foo"]} 21 | (decode codec (concat data1 data2)))))) 22 | 23 | (testing "merge maps when appending" 24 | (let [data1 (encode codec {:num-map {1 "one" 3 "three"}}) 25 | data2 (encode codec {:num-map {2 "dos" 3 "tres"}}) 26 | data3 (encode codec {:num-map {3 "san" 6 "roku"}})] 27 | (is (= {:num-map {1 "one" 2 "dos" 3 "san" 6 "roku"}} 28 | (decode codec (concat data1 data2 data3)))))) 29 | 30 | (testing "merge sets when appending" 31 | (let [data1 (encode codec {:tag-set #{"foo" "bar"}}) 32 | data2 (encode codec {:tag-set #{"baz" "foo"}})] 33 | (is (= {:tag-set #{"foo" "bar" "baz"}} 34 | (decode codec (concat data1 data2)))))) 35 | 36 | (testing "support set deletion using existence map" 37 | (let [data1 (encode codec {:tag-set #{"foo" "bar" "baz"}}) 38 | data2 (encode codec {:tag-set {"baz" false "foo" true "zap" true "bam" false}})] 39 | (is (= {:tag-set #{"foo" "bar" "zap"}} 40 | (decode codec (concat data1 data2)))))) 41 | 42 | (testing "merge and append nested data structures when appending" 43 | (let [data1 (encode codec {:nested {:foo 1 :tags ["bar"] :nested {:tag-set #{"a" "c"}}}}) 44 | data2 (encode codec {:nested {:foo 4 :tags ["baz"] :bar 3}}) 45 | data3 (encode codec {:nested {:baz 5 :tags ["foo"] :nested {:tag-set {"b" true "c" false}}}})] 46 | (is (= {:nested {:foo 4 :bar 3 :baz 5 :tags ["bar" "baz" "foo"] :nested {:tag-set #{"a" "b"}}}} 47 | (decode codec (concat data1 data2 data3)))))))) 48 | 49 | (deftest repeated-protobufs 50 | (let [len (length-prefix flatland.protobuf.test.Codec$Foo) 51 | codec (protobuf-codec flatland.protobuf.test.Codec$Foo :repeated true)] 52 | (testing "length-prefix" 53 | (doseq [i [0 10 100 1000 10000 100000 Integer/MAX_VALUE]] 54 | (is (= i (decode len (encode len i)))))) 55 | (testing "repeated" 56 | (let [data1 (encode codec [{:foo 1 :bar 2}]) 57 | data2 (encode codec [{:foo 4 :baz 8}])] 58 | (is (= [{:foo 1 :bar 2} {:foo 4 :baz 8}] 59 | (decode codec (concat data1 data2)))))))) -------------------------------------------------------------------------------- /test/flatland/protobuf/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns flatland.protobuf.core-test 2 | (:use flatland.protobuf.core clojure.test 3 | [flatland.io.core :only [catbytes]] 4 | [flatland.useful.utils :only [adjoin]] 5 | ordered-map.core) 6 | (:import (java.io PipedInputStream PipedOutputStream))) 7 | 8 | (def Foo (protodef flatland.protobuf.test.Core$Foo)) 9 | (def FooUnder (protodef flatland.protobuf.test.Core$Foo 10 | {:naming-strategy flatland.protobuf.PersistentProtocolBufferMap$Def/protobufNames})) 11 | (def Bar (protodef flatland.protobuf.test.Core$Bar)) 12 | (def Response (protodef flatland.protobuf.test.Core$Response)) 13 | (def ErrorMsg (protodef flatland.protobuf.test.Core$ErrorMsg)) 14 | (def Maps (protodef flatland.protobuf.test.Maps$Struct)) 15 | 16 | (deftest test-conj 17 | (let [p (protobuf Foo :id 5 :tags ["little" "yellow"] :doubles [1.2 3.4 5.6] :floats [0.01 0.02 0.03])] 18 | (let [p (conj p {:label "bar"})] 19 | (is (= 5 (:id p))) 20 | (is (= "bar" (:label p))) 21 | (is (= ["little" "yellow"] (:tags p))) 22 | (is (= [1.2 3.4 5.6] (:doubles p))) 23 | (is (= [(float 0.01) (float 0.02) (float 0.03)] (:floats p)))) 24 | (let [p (conj p {:tags ["different"]})] 25 | (is (= ["different"] (:tags p)))) 26 | (let [p (conj p {:tags ["little" "yellow" "different"] :label "very"})] 27 | (is (= ["little" "yellow" "different"] (:tags p))) 28 | (is (= "very" (:label p)))))) 29 | 30 | (deftest test-adjoin 31 | (let [p (protobuf Foo :id 5 :tags ["little" "yellow"] :doubles [1.2] :floats [0.01])] 32 | (let [p (adjoin p {:label "bar"})] 33 | (is (= 5 (:id p))) 34 | (is (= "bar" (:label p))) 35 | (is (= false (:deleted p))) 36 | (is (= ["little" "yellow"] (:tags p)))) 37 | (let [p (adjoin (assoc p :deleted true) p)] 38 | (is (= true (:deleted p)))) 39 | (let [p (adjoin p {:tags ["different"]})] 40 | (is (= ["little" "yellow" "different"] (:tags p)))) 41 | (let [p (adjoin p {:tags ["different"] :label "very"})] 42 | (is (= ["little" "yellow" "different"] (:tags p))) 43 | (is (= "very" (:label p)))) 44 | (let [p (adjoin p {:doubles [3.4] :floats [0.02]})] 45 | (is (= [1.2 3.4] (:doubles p))) 46 | (is (= [(float 0.01) (float 0.02)] (:floats p))))) 47 | (testing "adjoining works with set extension" 48 | (let [p (protobuf Foo :tag-set #{"foo" "bar" "baz"}) 49 | q (protobuf Foo :tag-set {"bar" false "foo" false "bap" true}) 50 | r (adjoin p q)] 51 | (is (= #{"foo" "bar" "baz"} (p :tag-set))) 52 | (is (= #{"bap"} (q :tag-set))) 53 | (is (= #{"bap" "baz"} (r :tag-set))))) 54 | (testing "adjoining works with counters" 55 | (let [p (protobuf Foo :counts {"foo" {:i 5 :d 5.0}}) 56 | q (protobuf Foo :counts {"foo" {:i 8 :d -3.0}}) 57 | r (adjoin p q)] 58 | (is (= 5 (get-in p [:counts "foo" :i]))) 59 | (is (= 5.0 (get-in p [:counts "foo" :d]))) 60 | (is (= 8 (get-in q [:counts "foo" :i]))) 61 | (is (= -3.0 (get-in q [:counts "foo" :d]))) 62 | (is (= 13 (get-in r [:counts "foo" :i]))) 63 | (is (= 2.0 (get-in r [:counts "foo" :d])))))) 64 | 65 | (deftest test-ordered-adjoin 66 | (let [inputs (apply ordered-map (for [x (range 26) 67 | entry [(str x) (str (char (+ (int \a) x)))]] 68 | entry))] 69 | (= (seq inputs) 70 | (seq (reduce (fn [m [k v]] 71 | (adjoin m {:attr_map {k v}})) 72 | (protobuf Foo) 73 | inputs))))) 74 | 75 | (deftest test-assoc 76 | (let [p (protobuf Foo :id 5 :tags ["little" "yellow"] :foo-by-id {1 {:label "one"} 2 {:label "two"}})] 77 | (let [p (assoc p :label "baz" :tags ["nuprin"])] 78 | (is (= ["nuprin"] (:tags p))) 79 | (is (= "baz" (:label p)))) 80 | (let [p (assoc p :responses [:yes :no :maybe :no "yes"])] 81 | (is (= [:yes :no :maybe :no :yes] (:responses p)))) 82 | (let [p (assoc p :tags "aspirin")] 83 | (is (= ["aspirin"] (:tags p)))) 84 | (let [p (assoc p :foo-by-id {3 {:label "three"} 2 {:label "two"}})] 85 | (is (= {3 {:id 3, :label "three", :deleted false} 86 | 2 {:id 2, :label "two", :deleted false}} 87 | (:foo-by-id p)))))) 88 | 89 | (deftest test-dissoc 90 | (let [p (protobuf Foo :id 5 :tags ["fast" "shiny"] :label "nice")] 91 | (let [p (dissoc p :label :tags)] 92 | (is (= nil (:tags p))) 93 | (is (= nil (:label p)))))) 94 | 95 | (deftest test-equality 96 | (let [m {:id 5 :tags ["fast" "shiny"] :label "nice" :deleted false} 97 | p (protobuf Foo :id 5 :tags ["fast" "shiny"] :label "nice") 98 | q (protobuf Foo :id 5 :tags ["fast" "shiny"] :label "nice")] 99 | (is (= m p)) 100 | (is (= p m)) 101 | (is (= q p)))) 102 | 103 | (deftest test-meta 104 | (let [p (protobuf Foo :id 5 :tags ["fast" "shiny"] :label "nice") 105 | m {:foo :bar} 106 | q (with-meta p m)] 107 | (is (empty? (meta p))) 108 | (is (= p q)) 109 | (is (= m (meta q))))) 110 | 111 | (deftest test-extmap 112 | (let [p (protobuf Foo :id 5 :tags ["fast" "shiny"] :label "nice") 113 | m {:id 5 :tags ["fast" "shiny"] :label "nice" :deleted false} 114 | p2 (assoc p :some-key 10) 115 | m2 (assoc m :some-key 10)] 116 | (is (= p m)) 117 | (is (= p2 m2)) 118 | (is (= m2 p2)) 119 | (is (= (into {} m2) (into {} p2))) 120 | (is (= (set (keys m2)) (set (keys p2))))) 121 | 122 | (let [m {:id 5 :wat 10} 123 | p (protobuf Foo m)] 124 | (testing "protobuf function uses extmap" 125 | (is (= (:wat m) (:wat p))) 126 | (let [p (conj p {:stuff 15})] 127 | (is (= 15 (:stuff p))))) 128 | 129 | ;; TODO add test back once we re-enable this check 130 | (comment 131 | (is (thrown? Exception (protobuf-dump p)) 132 | "Should refuse to serialize with stuff in extmap")))) 133 | 134 | (deftest test-string-keys 135 | (let [p (protobuf Foo "id" 5 "label" "rad")] 136 | (is (= 5 (p :id))) 137 | (is (= 5 (p "id"))) 138 | (is (= "rad" (p :label))) 139 | (is (= "rad" (p "label"))) 140 | (let [p (conj p {"tags" ["check" "it" "out"]})] 141 | (is (= ["check" "it" "out"] (p :tags))) 142 | (is (= ["check" "it" "out"] (p "tags")))))) 143 | 144 | (deftest test-append-bytes 145 | (let [p (protobuf Foo :id 5 :label "rad" :deleted true 146 | :tags ["sweet"] :tag-set #{"foo" "bar" "baz"} 147 | :things {"first" {:marked false} "second" {:marked false}}) 148 | q (protobuf Foo :id 43 :deleted false 149 | :tags ["savory"] :tag-set {"bar" false "foo" false "bap" true} 150 | :things {"first" {:marked true}}) 151 | r (protobuf Foo :label "bad") 152 | s (protobuf-load Foo (catbytes (protobuf-dump p) (protobuf-dump q))) 153 | t (protobuf-load Foo (catbytes (protobuf-dump p) (protobuf-dump r)))] 154 | (is (= 43 (s :id))) ; make sure an explicit default overwrites on append 155 | (is (= 5 (t :id))) ; make sure a missing default doesn't overwrite on append 156 | (is (= "rad" (s :label))) 157 | (is (= "bad" (t :label))) 158 | (is (= ["sweet"] (t :tags))) 159 | (is (= ["sweet" "savory"] (s :tags))) 160 | (is (= #{"foo" "bar" "baz"} (p :tag-set))) 161 | (is (= #{"bap" "baz"} (s :tag-set))) 162 | (is (= (s :things) {"first" {:id "first" :marked true} 163 | "second" {:id "second" :marked false}})) 164 | (is (= false (r :deleted))))) 165 | 166 | (deftest test-manual-append 167 | (let [p (protobuf Foo :id 5 :label "rad" :deleted true 168 | :tags ["sweet"] :tag-set #{"foo" "bar" "baz"}) 169 | q (protobuf Foo :id 43 :deleted false 170 | :tags ["savory"] :tag-set {"bar" false "foo" false "bap" true}) 171 | r (protobuf Foo :label "bad") 172 | s (.append p q) 173 | t (.append p r)] 174 | (is (= 43 (s :id))) ; make sure an explicit default overwrites on append 175 | (is (= 5 (t :id))) ; make sure a missing default doesn't overwrite on append 176 | (is (= "rad" (s :label))) 177 | (is (= "bad" (t :label))) 178 | (is (= ["sweet"] (t :tags))) 179 | (is (= ["sweet" "savory"] (s :tags))) 180 | (is (= #{"foo" "bar" "baz"} (p :tag-set))) 181 | (is (= #{"bap" "baz"} (s :tag-set))) 182 | (is (= false (r :deleted))))) 183 | 184 | (deftest test-map-exists 185 | (doseq [map-key [:element-map-e :element-by-id-e]] 186 | (let [p (protobuf Maps map-key {"A" {:foo 1} 187 | "B" {:foo 2} 188 | "C" {:foo 3} 189 | "D" {:foo 4 :exists true} 190 | "E" {:foo 5 :exists true} 191 | "F" {:foo 6 :exists true} 192 | "G" {:foo 7 :exists false} 193 | "H" {:foo 8 :exists false} 194 | "I" {:foo 9 :exists false}}) 195 | q (protobuf Maps map-key {"A" {:bar 1} 196 | "B" {:bar 2 :exists true} 197 | "C" {:bar 3 :exists false} 198 | "D" {:bar 4} 199 | "E" {:bar 5 :exists true} 200 | "F" {:bar 6 :exists false} 201 | "G" {:bar 7} 202 | "H" {:bar 8 :exists true} 203 | "I" {:bar 9 :exists false}}) 204 | r (protobuf-load Maps (catbytes (protobuf-dump p) (protobuf-dump q)))] 205 | (are [key vals] (= vals (map (get-in r [map-key key]) 206 | [:foo :bar :exists])) 207 | "A" [1 1 nil ] 208 | "B" [2 2 true ] 209 | "C" [3 3 false] 210 | "D" [4 4 true ] 211 | "E" [5 5 true ] 212 | "F" [6 6 false] 213 | "G" [7 7 false] 214 | "H" [nil 8 true ] 215 | "I" [9 9 false])))) 216 | 217 | (deftest test-map-deleted 218 | (doseq [map-key [:element-map-d :element-by-id-d]] 219 | (let [p (protobuf Maps map-key {"A" {:foo 1} 220 | "B" {:foo 2} 221 | "C" {:foo 3} 222 | "D" {:foo 4 :deleted true} 223 | "E" {:foo 5 :deleted true} 224 | "F" {:foo 6 :deleted true} 225 | "G" {:foo 7 :deleted false} 226 | "H" {:foo 8 :deleted false} 227 | "I" {:foo 9 :deleted false}}) 228 | q (protobuf Maps map-key {"A" {:bar 1} 229 | "B" {:bar 2 :deleted true} 230 | "C" {:bar 3 :deleted false} 231 | "D" {:bar 4} 232 | "E" {:bar 5 :deleted true} 233 | "F" {:bar 6 :deleted false} 234 | "G" {:bar 7} 235 | "H" {:bar 8 :deleted true} 236 | "I" {:bar 9 :deleted false}}) 237 | r (protobuf-load Maps (catbytes (protobuf-dump p) (protobuf-dump q)))] 238 | (are [key vals] (= vals (map (get-in r [map-key key]) 239 | [:foo :bar :deleted])) 240 | "A" [1 1 nil ] 241 | "B" [2 2 true ] 242 | "C" [3 3 false] 243 | "D" [4 4 true ] 244 | "E" [5 5 true ] 245 | "F" [nil 6 false] 246 | "G" [7 7 false] 247 | "H" [8 8 true ] 248 | "I" [9 9 false])))) 249 | 250 | (deftest test-coercing 251 | (let [p (protobuf Foo :lat 5 :long 6)] 252 | (is (= 5.0 (p :lat))) 253 | (is (= 6.0 (p :long)))) 254 | (let [p (protobuf Foo :lat (float 5.0) :long (double 6.0))] 255 | (is (= 5.0 (p :lat))) 256 | (is (= 6.0 (p :long))))) 257 | 258 | (deftest test-create 259 | (let [p (protobuf Foo :id 5 :tag-set #{"little" "yellow"} :attr-map {"size" "little", "color" "yellow", "style" "different"})] 260 | (is (= #{"little" "yellow"} (:tag-set p))) 261 | (is (associative? (:attr-map p))) 262 | (is (= "different" (get-in p [:attr-map "style"]))) 263 | (is (= "little" (get-in p [:attr-map "size" ]))) 264 | (is (= "yellow" (get-in p [:attr-map "color"])))) 265 | (let [p (protobuf Foo :id 1 :foo-by-id {5 {:label "five"}, 6 {:label "six"}})] 266 | (let [five ((p :foo-by-id) 5) 267 | six ((p :foo-by-id) 6)] 268 | (is (= 5 (five :id))) 269 | (is (= "five" (five :label))) 270 | (is (= 6 (six :id))) 271 | (is (= "six" (six :label)))))) 272 | 273 | (deftest test-map-by-with-required-field 274 | (let [p (protobuf Foo :id 1 :item-map {"foo" {:exists true} "bar" {:exists false}})] 275 | (is (= "foo" (get-in p [:item-map "foo" :item]))) 276 | (is (= "bar" (get-in p [:item-map "bar" :item]))))) 277 | 278 | (deftest test-map-by-with-inconsistent-keys 279 | (let [p (protobuf Foo :pair-map {"foo" {"key" "bar" "val" "hmm"}})] 280 | (is (= "hmm" (get-in p [:pair-map "foo" :val]))) 281 | (is (= nil (get-in p [:pair-map "bar" :val])))) 282 | (let [p (protobuf Foo :pair-map {"foo" {:key "bar" :val "hmm"}})] 283 | (is (= "hmm" (get-in p [:pair-map "foo" :val]))) 284 | (is (= nil (get-in p [:pair-map "bar" :val]))))) 285 | 286 | (deftest test-conj 287 | (let [p (protobuf Foo :id 1 :foo-by-id {5 {:label "five", :tag-set ["odd"]}, 6 {:label "six" :tags ["even"]}})] 288 | (let [p (conj p {:foo-by-id {5 {:tag-set ["prime" "odd"]} 6 {:tags ["odd"]}}})] 289 | (is (= #{"prime" "odd"} (get-in p [:foo-by-id 5 :tag-set]))) 290 | (is (= ["odd"] (get-in p [:foo-by-id 6 :tags]))) 291 | (is (= nil (get-in p [:foo-by-id 6 :label])))))) 292 | 293 | (deftest test-nested-counters 294 | (let [p (protobuf Foo :counts {"foo" {:i 5 :d 5.0}})] 295 | (is (= 5 (get-in p [:counts "foo" :i]))) 296 | (is (= 5.0 (get-in p [:counts "foo" :d]))) 297 | (let [p (adjoin p {:counts {"foo" {:i 2 :d -2.4} "bar" {:i 99}}})] 298 | (is (= 7 (get-in p [:counts "foo" :i]))) 299 | (is (= 2.6 (get-in p [:counts "foo" :d]))) 300 | (is (= 99 (get-in p [:counts "bar" :i]))) 301 | (let [p (adjoin p {:counts {"foo" {:i -8 :d 4.06} "bar" {:i -66}}})] 302 | (is (= -1 (get-in p [:counts "foo" :i]))) 303 | (is (= 6.66 (get-in p [:counts "foo" :d]))) 304 | (is (= 33 (get-in p [:counts "bar" :i]))) 305 | (is (= [{:key "foo", :i 5, :d 5.0} 306 | {:key "foo", :i 2, :d -2.4} 307 | {:key "bar", :i 99} 308 | {:key "foo", :i -8, :d 4.06} 309 | {:key "bar", :i -66}] 310 | (get-raw p :counts))))))) 311 | 312 | (deftest test-succession 313 | (let [p (protobuf Foo :time {:year 1978 :month 11 :day 24})] 314 | (is (= 1978 (get-in p [:time :year]))) 315 | (is (= 11 (get-in p [:time :month]))) 316 | (is (= 24 (get-in p [:time :day]))) 317 | (let [p (adjoin p {:time {:year 1974 :month 1}})] 318 | (is (= 1974 (get-in p [:time :year]))) 319 | (is (= 1 (get-in p [:time :month]))) 320 | (is (= nil (get-in p [:time :day]))) 321 | (is (= [{:year 1978, :month 11, :day 24} {:year 1974, :month 1}] 322 | (get-raw p :time)))))) 323 | 324 | (deftest test-nullable 325 | (let [p (protobuf Bar :int 1 :long 330000000000 :flt 1.23 :dbl 9.87654321 :str "foo" :enu :a) 326 | keyset #{:int :long :flt :dbl :str :enu}] 327 | (is (= 1 (get p :int))) 328 | (is (= 330000000000 (get p :long))) 329 | (is (= (float 1.23) (get p :flt))) 330 | (is (= 9.87654321 (get p :dbl))) 331 | (is (= "foo" (get p :str))) 332 | (is (= :a (get p :enu))) 333 | (is (= keyset (set (keys p)))) 334 | (let [p (adjoin p {:int nil :long nil :flt nil :dbl nil :str nil :enu nil})] 335 | (is (= nil (get p :int))) 336 | (is (= nil (get p :long))) 337 | (is (= nil (get p :flt))) 338 | (is (= nil (get p :dbl))) 339 | (is (= nil (get p :str))) 340 | (is (= nil (get p :enu))) 341 | (is (= keyset (set (keys p))))) 342 | (testing "nullable successions" 343 | (let [p (protobuf Bar :label "foo")] 344 | (is (= "foo" (get p :label))) 345 | (let [p (adjoin p {:label nil})] 346 | (is (= nil (get p :label))) 347 | (is (= ["foo" ""] (get-raw p :label)))))) 348 | (testing "repeated nullable" 349 | (let [p (protobuf Bar :labels ["foo" "bar"])] 350 | (is (= ["foo" "bar"] (get p :labels))) 351 | (let [p (adjoin p {:labels [nil]})] 352 | (is (= ["foo" "bar" nil] (get p :labels))) 353 | (is (= ["foo" "bar" ""] (get-raw p :labels)))))))) 354 | 355 | (deftest test-protobuf-schema 356 | (let [fields 357 | {:type :struct 358 | :name "flatland.protobuf.test.core.Foo" 359 | :fields {:id {:default 43, :type :int} 360 | :deleted {:default false, :type :boolean} 361 | :lat {:type :double} 362 | :long {:type :float} 363 | :parent {:type :struct, :name "flatland.protobuf.test.core.Foo"} 364 | :floats {:type :list, :values {:type :float}} 365 | :doubles {:type :list, :values {:type :double}} 366 | :label {:type :string, :c 3, :b 2, :a 1} 367 | :tags {:type :list, :values {:type :string}} 368 | :tag-set {:type :set, :values {:type :string}} 369 | :counts {:type :map 370 | :keys {:type :string} 371 | :values {:type :struct, :name "flatland.protobuf.test.core.Count" 372 | :fields {:key {:type :string} 373 | :i {:counter true, :type :int} 374 | :d {:counter true, :type :double}}}} 375 | :foo-by-id {:type :map 376 | :keys {:default 43, :type :int} 377 | :values {:type :struct, :name "flatland.protobuf.test.core.Foo"}} 378 | :attr-map {:type :map 379 | :keys {:type :string} 380 | :values {:type :string}} 381 | :pair-map {:type :map 382 | :keys {:type :string} 383 | :values {:type :struct, :name "flatland.protobuf.test.core.Pair" 384 | :fields {:key {:type :string} 385 | :val {:type :string}}}} 386 | :groups {:type :map 387 | :keys {:type :string} 388 | :values {:type :list 389 | :values {:type :struct, :name "flatland.protobuf.test.core.Foo"}}} 390 | :responses {:type :list 391 | :values {:type :enum, :values #{:no :yes :maybe :not-sure}}} 392 | :time {:type :struct, :name "flatland.protobuf.test.core.Time", :succession true 393 | :fields {:year {:type :int} 394 | :month {:type :int} 395 | :day {:type :int} 396 | :hour {:type :int} 397 | :minute {:type :int}}} 398 | :item-map {:type :map 399 | :keys {:type :string} 400 | :values {:type :struct, :name "flatland.protobuf.test.core.Item" 401 | :fields {:item {:type :string}, 402 | :exists {:default true, :type :boolean}}}} 403 | :things {:type :map 404 | :keys {:type :string} 405 | :values {:type :struct, :name "flatland.protobuf.test.core.Thing" 406 | :fields {:id {:type :string} 407 | :marked {:type :boolean}}}}}}] 408 | (is (= fields (protobuf-schema Foo))) 409 | (is (= fields (protobuf-schema flatland.protobuf.test.Core$Foo))))) 410 | 411 | (comment deftest test-default-protobuf 412 | (is (= 43 (default-protobuf Foo :id))) 413 | (is (= 0.0 (default-protobuf Foo :lat))) 414 | (is (= 0.0 (default-protobuf Foo :long))) 415 | (is (= "" (default-protobuf Foo :label))) 416 | (is (= [] (default-protobuf Foo :tags))) 417 | (is (= nil (default-protobuf Foo :parent))) 418 | (is (= [] (default-protobuf Foo :responses))) 419 | (is (= #{} (default-protobuf Foo :tag-set))) 420 | (is (= {} (default-protobuf Foo :foo-by-id))) 421 | (is (= {} (default-protobuf Foo :groups))) 422 | (is (= {} (default-protobuf Foo :item-map))) 423 | (is (= false (default-protobuf Foo :deleted))) 424 | (is (= {} (default-protobuf flatland.protobuf.test.Core$Foo :groups)))) 425 | 426 | (deftest test-use-underscores 427 | (let [dashes (protobuf Foo {:tag_set ["odd"] 428 | :responses [:yes :not-sure :maybe :not-sure :no]}) 429 | underscores (protobuf FooUnder {:tag_set ["odd"] 430 | :responses [:yes :not_sure :maybe :not_sure :no]})] 431 | (is (= '(:id :responses :tag-set :deleted) (keys dashes))) 432 | (is (= [:yes :not-sure :maybe :not-sure :no] (:responses dashes))) 433 | 434 | (is (= '(:id :responses :tag_set :deleted) (keys underscores))) 435 | (is (= [:yes :not_sure :maybe :not_sure :no] (:responses underscores))) 436 | 437 | (is (= #{:id :label :tags :parent :responses :tag_set :deleted :attr_map :foo_by_id 438 | :pair_map :groups :doubles :floats :item_map :counts :time :lat :long :things} 439 | (-> (protobuf-schema FooUnder) :fields keys set))))) 440 | 441 | (deftest test-protobuf-nested-message 442 | (let [p (protobuf Response :ok false :error (protobuf ErrorMsg :code -10 :data "abc"))] 443 | (is (= "abc" (get-in p [:error :data]))))) 444 | 445 | (deftest test-protobuf-nested-null-field 446 | (let [p (protobuf Response :ok true :error (protobuf ErrorMsg :code -10 :data nil))] 447 | (is (:ok p)))) 448 | 449 | (deftest test-protobuf-seq-and-write-protobuf 450 | (let [in (PipedInputStream.) 451 | out (PipedOutputStream. in) 452 | foo (protobuf Foo :id 1 :label "foo") 453 | bar (protobuf Foo :id 2 :label "bar") 454 | baz (protobuf Foo :id 3 :label "baz")] 455 | (protobuf-write out foo bar baz) 456 | (.close out) 457 | (is (= [{:id 1, :label "foo", :deleted false} 458 | {:id 2, :label "bar", :deleted false} 459 | {:id 3, :label "baz", :deleted false}] 460 | (protobuf-seq Foo in))))) 461 | 462 | (deftest test-encoding-errors 463 | (is (thrown-with-msg? IllegalArgumentException #"error setting string field flatland.protobuf.test.core.Foo.label to 8" 464 | (protobuf Foo :label 8))) 465 | (is (thrown-with-msg? IllegalArgumentException #"error adding 1 to string field flatland.protobuf.test.core.Foo.tags" 466 | (protobuf Foo :tags [1 2 3])))) -------------------------------------------------------------------------------- /test/flatland/protobuf/example_test.clj: -------------------------------------------------------------------------------- 1 | (ns flatland.protobuf.example-test 2 | (:use flatland.protobuf.core clojure.test) 3 | (:import com.google.protobuf.ByteString)) 4 | 5 | (def Photo (protodef flatland.protobuf.test.Example$Photo)) 6 | 7 | (def data {:id 7, :path "/photos/h2k3j4h9h23", :labels #{"hawaii" "family" "surfing"}, 8 | :attrs {"color space" "RGB", "dimensions" "1632x1224", "alpha" "no"}, 9 | :tags {4 {:person-id 4, :x-coord 607, :y-coord 813, :width 25, :height 27}} 10 | :image (ByteString/copyFrom (byte-array (map unchecked-byte [1 2 3 4 -1])))}) 11 | 12 | (deftest example-test 13 | (is (= data (apply protobuf Photo (apply concat data))))) 14 | --------------------------------------------------------------------------------