├── .gitignore ├── .rspec ├── .yard_redcarpet_ext ├── .yardopts ├── BUILD.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── DEV.md ├── Gemfile ├── Jarfile ├── LICENSE ├── README.md ├── Rakefile ├── TODO.org ├── benchmarks ├── decoding_time.rb └── encoding_time.rb ├── bin ├── benchmark ├── read-transit ├── read-write ├── roundtrip └── rspec-across-supported-versions ├── build ├── jruby_version ├── release └── revision ├── dev └── irb_tools.rb ├── ext └── com │ └── cognitect │ └── transit │ └── ruby │ ├── TransitService.java │ ├── TransitTypeConverter.java │ ├── marshaler │ ├── Base.java │ ├── Json.java │ ├── MessagePack.java │ └── VerboseJson.java │ └── unmarshaler │ ├── Base.java │ ├── Json.java │ ├── MessagePack.java │ ├── RubyArrayReader.java │ ├── RubyMapReader.java │ └── RubyReaders.java ├── lib ├── transit.rb └── transit │ ├── date_time_util.rb │ ├── decoder.rb │ ├── marshaler │ ├── base.rb │ ├── cruby │ │ ├── json.rb │ │ └── messagepack.rb │ └── jruby │ │ ├── json.rb │ │ └── messagepack.rb │ ├── read_handlers.rb │ ├── reader.rb │ ├── rolling_cache.rb │ ├── transit_types.rb │ ├── unmarshaler │ └── cruby │ │ ├── json.rb │ │ └── messagepack.rb │ ├── write_handlers.rb │ └── writer.rb ├── spec ├── spec_helper.rb └── transit │ ├── date_time_util_spec.rb │ ├── decoder_spec.rb │ ├── exemplar_spec.rb │ ├── marshaler_spec.rb │ ├── reader_spec.rb │ ├── rolling_cache_spec.rb │ ├── round_trip_spec.rb │ ├── transit_types_spec.rb │ └── writer_spec.rb └── transit-ruby.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | *.log 4 | .bundle 5 | .config 6 | coverage 7 | InstalledFiles 8 | lib/bundler/man 9 | pkg 10 | rdoc 11 | spec/reports 12 | test/tmp 13 | test/version_tmp 14 | tmp 15 | 16 | # YARD artifacts 17 | .yardoc 18 | _yardoc 19 | doc/ 20 | .rbenv-version 21 | .rvm 22 | Gemfile.lock 23 | .ruby-version 24 | .ruby-gemset 25 | spec_helper-local.rb 26 | Gemfile-custom 27 | 28 | # lock_jar 29 | Jarfile.lock 30 | 31 | # IDE 32 | .classpath 33 | .project 34 | transit.jar 35 | /target/ 36 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.yard_redcarpet_ext: -------------------------------------------------------------------------------- 1 | :tables -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --markup markdown 2 | - 3 | CHANGELOG.md 4 | LICENSE 5 | -------------------------------------------------------------------------------- /BUILD.md: -------------------------------------------------------------------------------- 1 | ## Build (internal) 2 | 3 | ### Version 4 | 5 | The build version is automatically incremented. To determine the 6 | current build version: 7 | 8 | build/version 9 | 10 | ### Package 11 | 12 | gem install bundler 13 | bundle install 14 | bundle exec rake build 15 | 16 | ### Install locally 17 | 18 | bundle exec rake install 19 | 20 | ### Docs 21 | 22 | To build api documentation: 23 | 24 | gem install yard 25 | yard 26 | 27 | ### Release 28 | 29 | #### Pre-requisites: 30 | 31 | * permission to push gems to https://rubygems.org/gems/transit-ruby 32 | * public and private key files for MRI 33 | 34 | To sign the gem for MRI (currently disabled for JRuby), you'll need 35 | to generate public and private keys. Follow the directions from `gem 36 | cert -h` to generate the following files: 37 | 38 | ~/.gem/transit-ruby/gem-private_key.pem 39 | ~/.gem/transit-ruby/gem-public_cert.pem 40 | 41 | Once those are in place, you can run: 42 | 43 | ./build/release 44 | 45 | You'll be prompted to confirm MRI and JRuby releases and for cert 46 | passwords for the MRI version. 47 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 0.8.591 / 2015-05-03 2 | 3 | * Bump lock_jar dependency to ~> 0.12.0 #17 4 | 5 | ### 0.8.588 / 2015-04-10 6 | 7 | * Update to transit-java-0.8.287 for json int boundary fix in JRuby 8 | 9 | ### 0.8.586 / 2015-03-13 10 | 11 | * Add handler caching for MRI 12 | * Bump to transit-java-0.8.285 for handler caching in JRuby 13 | 14 | ### 0.8.572 / 2015-01-15 15 | 16 | * Marshal int map keys as ints in msgpack 17 | 18 | ### 0.8.569 / 2014-12-03 19 | 20 | * ByteArray#to_s forces default encoding for platform 21 | * fixes rare bug in which trying to print binary data nested within 22 | decoded binary data raises an encoding incompatibility error. 23 | 24 | ### 0.8.567 / 2014-09-21 25 | 26 | * restore newline suppression when writing in json mode 27 | * helpful error message when nested object has no handler 28 | 29 | ### 0.8.560 (java platform only) / 2014-09-12 30 | 31 | * Bump dependency on transit-java to 0.8.269 32 | * fixes bug which turned an empty set into an array 33 | 34 | ### 0.8.552 (java platform only) / 2014-09-12 35 | 36 | * Support JRuby! 37 | 38 | ### 0.8.539 / 2014-09-05 39 | 40 | * Support special numbers (NaN, INF, -INF) 41 | 42 | ### 0.8.467 / 2014-07-22 43 | 44 | * Initial release 45 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | This library is open source, developed internally by Cognitect. We welcome discussions of potential problems and enhancement suggestions on the [transit-format mailing list](https://groups.google.com/forum/#!forum/transit-format). Issues can be filed using GitHub [issues](https://github.com/cognitect/transit-ruby/issues) for this project. Because transit is incorporated into products and client projects, we prefer to do development internally and are not accepting pull requests or patches. 4 | -------------------------------------------------------------------------------- /DEV.md: -------------------------------------------------------------------------------- 1 | ### Development setup 2 | 3 | gem install bundler 4 | bundle install 5 | 6 | Transit Ruby uses transit as a submodule to get at the transit 7 | exemplar files. The tests will not run without the exemplar files. 8 | You need to run a couple of git commands to set up the transit 9 | git submodule: 10 | 11 | git submodule init 12 | git submodule update 13 | 14 | ### Run rspec examples 15 | 16 | rspec 17 | 18 | ## Benchmarks 19 | 20 | ./bin/benchmark # reads transit data in json and json-verbose formats 21 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | 7 | # Gemfile-custom is .gitignored, but eval'd here so you can add 8 | # whatever dev tools you like to use to your local environment. 9 | eval File.read('Gemfile-custom') if File.exist?('Gemfile-custom') 10 | -------------------------------------------------------------------------------- /Jarfile: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | 3 | jruby_version=File.read("build/jruby_version").chomp 4 | 5 | # dependency to transit-java 6 | # this setup uses maven central for the repository 7 | 8 | jar "com.cognitect:transit-java:0.8.287" 9 | 10 | group 'development' do 11 | jar "org.jruby:jruby-complete:#{jruby_version}" 12 | end 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This library is no longer maintained. If you are interested in using or maintaining, please fork it and update according to the license. 2 | 3 | 4 | transit-ruby 5 | =================== 6 | 7 | Transit is a data format and a set of libraries for conveying 8 | values between applications written in different languages. This 9 | library provides support for marshalling Transit data to/from Ruby. 10 | 11 | [Rationale](http://blog.cognitect.com/blog/2014/7/22/transit)
12 | [API docs](http://rubydoc.info/gems/transit-ruby)
13 | [Specification](https://github.com/cognitect/transit-format) 14 | 15 | This implementation's major.minor version number corresponds to the 16 | version of the Transit specification it supports. 17 | 18 | _NOTE: Transit is intended primarily as a wire protocol for transferring data between applications. If storing Transit data durably, readers and writers are expected to use the same version of Transit and you are responsible for migrating/transforming/re-storing that data when and if the transit format changes._ 19 | 20 | ## Contributing 21 | 22 | This library is open source, developed internally by Cognitect. We welcome discussions of potential problems and enhancement suggestions on the [transit-format mailing list](https://groups.google.com/forum/#!forum/transit-format). Issues can be filed using GitHub [issues](https://github.com/cognitect/transit-ruby/issues) for this project. Because transit is incorporated into products and client projects, we prefer to do development internally and are not accepting pull requests or patches. 23 | 24 | ## Releases and Dependency Information 25 | 26 | See https://rubygems.org/gems/transit-ruby 27 | 28 | ## Install 29 | 30 | ```sh 31 | gem install transit-ruby 32 | ``` 33 | 34 | ## Basic Usage 35 | 36 | ```ruby 37 | # io can be any Ruby IO 38 | 39 | writer = Transit::Writer.new(:json, io) # or :json_verbose, :msgpack 40 | writer.write(value) 41 | 42 | reader = Transit::Reader.new(:json, io) # or :msgpack 43 | reader.read 44 | 45 | # or 46 | 47 | reader.read {|val| do_something_with(val)} 48 | ``` 49 | 50 | For example: 51 | 52 | ``` 53 | irb(2.1.1): io = StringIO.new('', 'w+') 54 | ==========> # 55 | irb(2.1.1): writer = Transit::Writer.new(:json, io) 56 | ==========> # nil 59 | irb(2.1.1): writer.write(123456789012345678901234567890) 60 | ==========> nil 61 | irb(2.1.1): io.string 62 | ==========> "[\"~#'\",\"abc\"]\n[\"~#'\",\"~n123456789012345678901234567890\"]\n" 63 | irb(2.1.1): reader = Transit::Reader.new(:json, StringIO.new(io.string)) 64 | ==========> # {Point => PointWriteHandler.new}) 106 | writer.write(Point.new(37,42)) 107 | 108 | p io.string.chomp 109 | #=> "[\"~#point\",[37,42]]" 110 | 111 | reader = Transit::Reader.new(:json, StringIO.new(io.string), 112 | :handlers => {"point" => PointReadHandler.new}) 113 | p reader.read 114 | #=> # 115 | ``` 116 | 117 | See 118 | [Transit::WriteHandlers](http://rubydoc.info/gems/transit-ruby/Transit/WriteHandlers) 119 | for more info. 120 | 121 | ## Default Type Mapping 122 | 123 | |Transit type|Write accepts|Read returns|Example(write)|Example(read)| 124 | |------------|-------------|------------|--------------|-------------| 125 | |null|nil|nil|nil|nil| 126 | |string|String|String|"abc"|"abc"| 127 | |boolean|true, false|true, false|false|false| 128 | |integer|Integer|Integer|123|123| 129 | |decimal|Float|Float|123.456|123.456| 130 | |keyword|Symbol|Symbol|:abc|:abc| 131 | |symbol|Transit::Symbol|Transit::Symbol|Transit::Symbol.new("foo")|`#`| 132 | |big decimal|BigDecimal|BigDecimal|BigDecimal("2**64")|`#`| 133 | |big integer|Integer|Integer|2**128|340282366920938463463374607431768211456| 134 | |time|DateTime, Date, Time|DateTime|DateTime.now|`#`| 135 | |uri|Addressable::URI, URI|Addressable::URI|Addressable::URI.parse("http://example.com")|`#`| 136 | |uuid|Transit::UUID|Transit::UUID|Transit::UUID.new|`#`| 137 | |char|Transit::TaggedValue|String|Transit::TaggedValue.new("c", "a")|"a"| 138 | |array|Array|Array|[1, 2, 3]|[1, 2, 3]| 139 | |list|Transit::TaggedValue|Array|Transit::TaggedValue.new("list", [1, 2, 3])|[1, 2, 3]| 140 | |set|Set|Set|Set.new([1, 2, 3])|`#`| 141 | |map|Hash|Hash|`{a: 1, b: 2, c: 3}`|`{:a=>1, :b=>2, :c=>3}`| 142 | |bytes|Transit::ByteArray|Transit::ByteArray|Transit::ByteArray.new("base64")|base64| 143 | |link|Transit::Link|Transit::Link|Transit::Link.new(Addressable::URI.parse("http://example.org/search"), "search")|`##, "rel"=>"search", "name"=>nil, "render"=>nil, "prompt"=>nil}>`| 144 | 145 | ### Additional types (not required by the [transit-format](https://github.com/cognitect/transit-format) spec) 146 | 147 | |Semantic type|Write accepts|Read returns|Example(write)|Example(read)| 148 | |------------|-------------|------------|--------------|-------------| 149 | |ratio|Rational|Rational|Rational(1, 3)|Rational(1, 3)| 150 | 151 | ## Tested Ruby Versions 152 | 153 | * MRI 2.1.10, 2.2.7, 2.3.4, 2.4.0, 2.4.1, 2.6.0..3 154 | * JRuby 1.7.13..16 155 | 156 | ## Copyright and License 157 | 158 | Copyright © 2014 Cognitect 159 | 160 | Licensed under the Apache License, Version 2.0 (the "License"); 161 | you may not use this file except in compliance with the License. 162 | You may obtain a copy of the License at 163 | 164 | http://www.apache.org/licenses/LICENSE-2.0 165 | 166 | Unless required by applicable law or agreed to in writing, software 167 | distributed under the License is distributed on an "AS IS" BASIS, 168 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 169 | implied. 170 | See the License for the specific language governing permissions and 171 | limitations under the License. 172 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | #!/usr/bin/env rake 3 | 4 | require 'bundler' 5 | Bundler.setup 6 | 7 | require 'rspec/core/rake_task' 8 | RSpec::Core::RakeTask.new(:spec) 9 | Rake::Task[:spec].prerequisites << :compile 10 | task :default => :spec 11 | 12 | task :irb do 13 | sh 'irb -I lib -r transit -I dev -r irb_tools' 14 | end 15 | 16 | def project_name 17 | "transit-ruby" 18 | end 19 | 20 | def gemspec_filename 21 | @gemspec_filename ||= "#{project_name}.gemspec" 22 | end 23 | 24 | def spec_version 25 | @spec_version ||= /"(\d+\.\d+).dev"/.match(File.read(gemspec_filename))[1] 26 | end 27 | 28 | def jruby? 29 | defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby" 30 | end 31 | 32 | def jruby_version 33 | @jruby_version ||= `cat build/jruby_version`.chomp 34 | end 35 | 36 | def revision 37 | @revision ||= `build/revision`.chomp.to_i 38 | end 39 | 40 | def build_version 41 | @build_version ||= "#{spec_version}.#{revision}" 42 | end 43 | 44 | def published? 45 | gem_search = `gem q -rn "^transit-ruby$"` 46 | if jruby? 47 | gem_search =~ /\(#{revision}.*java.*\)/ 48 | else 49 | gem_search =~ /\(#{revision}.*ruby.*\)/ 50 | end 51 | end 52 | 53 | def tagged? 54 | `git tag` =~ /#{revision}/ 55 | end 56 | 57 | def gem_filename 58 | if jruby? 59 | @gem_filename ||= "#{project_name}-#{build_version}-java.gem" 60 | else 61 | @gem_filename ||= "#{project_name}-#{build_version}.gem" 62 | end 63 | end 64 | 65 | def gem_path 66 | @gem_path ||= "pkg/#{gem_filename}" 67 | end 68 | 69 | desc "Use JRuby" 70 | task :use_jruby do 71 | sh "rbenv local jruby-#{jruby_version}" 72 | end 73 | 74 | desc "Build #{gem_filename}.gem into the pkg directory" 75 | task :build => [:compile] do 76 | begin 77 | gemspec_content = File.read(gemspec_filename) 78 | File.open(gemspec_filename, 'w+') do |f| 79 | f.write gemspec_content.sub("#{spec_version}.dev", build_version) 80 | end 81 | sh "gem build #{gemspec_filename}" 82 | sh "mkdir -p pkg" 83 | sh "mv #{gem_filename} #{gem_path}" 84 | ensure 85 | File.open(gemspec_filename, 'w+') do |f| 86 | f.write gemspec_content 87 | end 88 | end 89 | end 90 | 91 | desc "Build and install #{gem_filename}" 92 | task :install => [:build] do 93 | sh "gem install #{gem_path}" 94 | end 95 | 96 | task :ensure_committed do 97 | raise "Cannot release with uncommitted changes." unless `git status` =~ /clean/ 98 | end 99 | 100 | desc "Prepare to sign the gem" 101 | task :prepare_to_sign do 102 | if jruby? 103 | puts "Gem signing is disabled for transit-ruby for JRuby" 104 | else 105 | private_key_path = File.expand_path(File.join(ENV['HOME'], '.gem', 'transit-ruby', 'gem-private_key.pem')) 106 | public_key_path = File.expand_path(File.join(ENV['HOME'], '.gem', 'transit-ruby', 'gem-public_cert.pem')) 107 | if File.exist?(public_key_path) and File.exist?(private_key_path) 108 | ENV['SIGN_GEM'] = 'true' 109 | else 110 | raise "Missing one or both key files: #{public_key_path}, #{private_key_path}" 111 | end 112 | end 113 | end 114 | 115 | task :publish => [:build] do 116 | unless tagged? 117 | sh "git tag v#{build_version}" 118 | end 119 | if published? 120 | puts "Already published #{gem_filename}" 121 | exit 122 | end 123 | puts "Ready to publish #{gem_filename} to rubygems. Enter 'Y' to publish, anything else to stop:" 124 | input = STDIN.gets.chomp 125 | if input.downcase == 'y' 126 | sh "gem push #{gem_path}" 127 | else 128 | puts "Canceling publish (you entered #{input.inspect} instead of 'Y')" 129 | sh "git tag -d v#{build_version}" 130 | end 131 | end 132 | 133 | desc "Create tag v#{build_version} and build and push #{gem_filename} to Rubygems" 134 | task :release => [:ensure_committed, :prepare_to_sign, :publish] 135 | 136 | desc "Uninstall #{project_name}" 137 | task :uninstall do 138 | sh "gem uninstall #{project_name}" 139 | end 140 | 141 | desc "Clean up generated files" 142 | task :clobber do 143 | sh "rm -rf ./tmp ./pkg ./.yardoc doc lib/transit.jar" 144 | end 145 | 146 | # rake compiler 147 | if jruby? 148 | require 'rake/javaextensiontask' 149 | Rake::JavaExtensionTask.new('transit') do |ext| 150 | require 'lock_jar' 151 | LockJar.lock 152 | locked_jars = LockJar.load(['default', 'development']) 153 | 154 | ext.name = 'transit' 155 | ext.ext_dir = 'ext' 156 | ext.lib_dir = 'lib' 157 | ext.source_version = '1.6' 158 | ext.target_version = '1.6' 159 | ext.classpath = locked_jars.map {|x| File.expand_path x}.join ':' 160 | end 161 | else 162 | task :compile do 163 | # no-op for C Ruby 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /TODO.org: -------------------------------------------------------------------------------- 1 | * ship it! 2 | * continue spike of streaming parse 3 | There is a conflict between OJ's parsing model and our caching 4 | model: hash values get processed before keys, so we're caching keys 5 | after any caching is performed within a nested structure. 6 | -------------------------------------------------------------------------------- /benchmarks/decoding_time.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | $LOAD_PATH << File.expand_path("../../lib", __FILE__) 16 | require 'transit' 17 | require 'benchmark' 18 | 19 | decoder = Transit::Decoder.new 20 | custom_decoder = Transit::Decoder.new 21 | custom_decoder.register("t") {|t| Transit::Util.date_time_from_millis(t.to_i)} 22 | 23 | n = 10000 24 | 25 | t = Time.now 26 | m = Transit::Util.date_time_to_millis(t) 27 | m_to_s = m.to_s 28 | s = t.utc.iso8601(3) 29 | 30 | results = [Time.parse(s).utc, 31 | Transit::Util.date_time_from_millis(m), 32 | Transit::Util.date_time_from_millis(m_to_s.to_i), 33 | decoder.decode("~t#{s}"), 34 | decoder.decode({"~#t" => m}), 35 | custom_decoder.decode("~t#{m}")] 36 | 37 | as_millis = results.map {|r| Transit::Util.date_time_to_millis(r)} 38 | 39 | if Set.new(as_millis).length > 1 40 | warn "Not all methods returned the same values:" 41 | warn as_millis.to_s 42 | end 43 | 44 | Benchmark.benchmark do |bm| 45 | puts "Time.parse(#{s.inspect}).utc" 46 | 3.times do 47 | bm.report do 48 | n.times do 49 | Time.parse(s).utc 50 | end 51 | end 52 | end 53 | 54 | puts 55 | puts "Transit::Util.date_time_from_millis(#{m.inspect})" 56 | 3.times do 57 | bm.report do 58 | n.times do 59 | Transit::Util.date_time_from_millis(m) 60 | end 61 | end 62 | end 63 | 64 | puts 65 | puts "Transit::Util.date_time_from_millis(#{m_to_s.inspect}.to_i)" 66 | 3.times do 67 | bm.report do 68 | n.times do 69 | Transit::Util.date_time_from_millis(m_to_s.to_i) 70 | end 71 | end 72 | end 73 | 74 | puts 75 | puts "decoder.decode(\"~t#{s}\")" 76 | 3.times do 77 | bm.report do 78 | n.times do 79 | decoder.decode("~t#{s}") 80 | end 81 | end 82 | end 83 | 84 | puts 85 | puts "decoder.decode({\"~#t\" => #{m}})" 86 | 3.times do 87 | bm.report do 88 | n.times do 89 | decoder.decode({"~#t" => m}) 90 | end 91 | end 92 | end 93 | 94 | puts 95 | puts "custom_decoder.decode(\"~t#{m}\")" 96 | 3.times do 97 | bm.report do 98 | n.times do 99 | custom_decoder.decode("~t#{m}") 100 | end 101 | end 102 | end 103 | end 104 | 105 | __END__ 106 | 107 | $ ruby benchmarks/decoding_time.rb 108 | Not all methods returned the same values: 109 | [1397450386660, 1397450386661, 1397450386661, 1397450386660, 1397450386661, 1397450386661] 110 | 111 | # This ^^ shows that Time.new.iso8601(3) is truncating millis instead of rounding them. 112 | 113 | Time.parse("2014-04-14T04:39:46.660Z").utc 114 | 0.270000 0.000000 0.270000 ( 0.265990) 115 | 0.260000 0.000000 0.260000 ( 0.261357) 116 | 0.260000 0.000000 0.260000 ( 0.263597) 117 | 118 | Transit::Util.date_time_from_millis(1397450386661) 119 | 0.040000 0.000000 0.040000 ( 0.041289) 120 | 0.040000 0.000000 0.040000 ( 0.043084) 121 | 0.050000 0.000000 0.050000 ( 0.043169) 122 | 123 | Transit::Util.date_time_from_millis("1397450386661".to_i) 124 | 0.040000 0.000000 0.040000 ( 0.048342) 125 | 0.050000 0.000000 0.050000 ( 0.047006) 126 | 0.050000 0.010000 0.060000 ( 0.046771) 127 | 128 | decoder.decode("~t2014-04-14T04:39:46.660Z") 129 | 0.310000 0.000000 0.310000 ( 0.311126) 130 | 0.310000 0.000000 0.310000 ( 0.312943) 131 | 0.320000 0.000000 0.320000 ( 0.317080) 132 | 133 | decoder.decode({"~#t" => 1397450386661}) 134 | 0.080000 0.000000 0.080000 ( 0.081899) 135 | 0.070000 0.000000 0.070000 ( 0.077323) 136 | 0.080000 0.000000 0.080000 ( 0.079400) 137 | 138 | custom_decoder.decode("~t1397450386661") 139 | 0.080000 0.000000 0.080000 ( 0.074236) 140 | 0.080000 0.000000 0.080000 ( 0.080308) 141 | 0.070000 0.000000 0.070000 ( 0.075567) 142 | -------------------------------------------------------------------------------- /benchmarks/encoding_time.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | $LOAD_PATH << File.expand_path("../../lib", __FILE__) 16 | require 'transit' 17 | require 'benchmark' 18 | 19 | n = 10000 20 | 21 | $date_time = DateTime.now 22 | $time = Time.now 23 | $date = Date.today 24 | 25 | def header(s) 26 | puts s.sub(/^\$/,'') 27 | puts eval(s) 28 | end 29 | 30 | Benchmark.benchmark do |bm| 31 | header '$date_time.new_offset(0).strftime(Transit::TIME_FORMAT)' 32 | 3.times do 33 | bm.report do 34 | n.times do 35 | $date_time.new_offset(0).strftime("%FT%H:%M:%S.%LZ") 36 | end 37 | end 38 | end 39 | 40 | puts 41 | 42 | header '$date_time.to_time.utc.strftime(Transit::TIME_FORMAT)' 43 | 3.times do 44 | bm.report do 45 | n.times do; 46 | $date_time.to_time.utc.strftime(Transit::TIME_FORMAT) 47 | end 48 | end 49 | end 50 | 51 | puts 52 | 53 | header "$date_time.to_time.utc.iso8601(3)" 54 | 3.times do 55 | bm.report do 56 | n.times do 57 | $date_time.to_time.utc.iso8601(3) 58 | end 59 | end 60 | end 61 | 62 | puts 63 | 64 | header '$time.getutc.strftime(Transit::TIME_FORMAT)' 65 | 3.times do 66 | bm.report do 67 | n.times do 68 | $time.getutc.strftime(Transit::TIME_FORMAT) 69 | end 70 | end 71 | end 72 | 73 | puts 74 | 75 | header '$time.to_datetime.new_offset(0).strftime(Transit::TIME_FORMAT)' 76 | 3.times do 77 | bm.report do 78 | n.times do 79 | $time.to_datetime.new_offset(0).strftime(Transit::TIME_FORMAT) 80 | end 81 | end 82 | end 83 | 84 | puts 85 | 86 | header '$date.to_datetime.strftime(Transit::TIME_FORMAT)' 87 | 3.times do 88 | bm.report do 89 | n.times do 90 | $date.to_datetime.strftime(Transit::TIME_FORMAT) 91 | end 92 | end 93 | end 94 | 95 | puts 96 | 97 | header '$date.to_time.strftime(Transit::TIME_FORMAT)' 98 | 3.times do 99 | bm.report do 100 | n.times do 101 | $date.to_time.strftime(Transit::TIME_FORMAT) 102 | end 103 | end 104 | end 105 | 106 | puts 107 | 108 | header 'Time.gm($date.year, $date.month, $date.day).iso8601(3)' 109 | 3.times do 110 | bm.report do 111 | n.times do 112 | Time.gm($date.year, $date.month, $date.day).iso8601(3) 113 | end 114 | end 115 | end 116 | 117 | puts 118 | 119 | header 'Time.gm($date.year, $date.month, $date.day).strftime(Transit::TIME_FORMAT)' 120 | 3.times do 121 | bm.report do 122 | n.times do 123 | Time.gm($date.year, $date.month, $date.day).strftime(Transit::TIME_FORMAT) 124 | end 125 | end 126 | end 127 | end 128 | 129 | __END__ 130 | 131 | $ ruby benchmarks/encoding_time.rb 132 | date_time.new_offset(0).strftime(Transit::TIME_FORMAT) 133 | 2014-04-18T19:35:20.150Z 134 | 0.020000 0.000000 0.020000 ( 0.022102) 135 | 0.020000 0.000000 0.020000 ( 0.020739) 136 | 0.030000 0.010000 0.040000 ( 0.025088) 137 | 138 | date_time.to_time.utc.strftime(Transit::TIME_FORMAT) 139 | 2014-04-18T19:35:20.150Z 140 | 0.080000 0.000000 0.080000 ( 0.081011) 141 | 0.070000 0.000000 0.070000 ( 0.079435) 142 | 0.080000 0.000000 0.080000 ( 0.079693) 143 | 144 | date_time.to_time.utc.iso8601(3) 145 | 2014-04-18T19:35:20.150Z 146 | 0.100000 0.000000 0.100000 ( 0.095387) 147 | 0.100000 0.000000 0.100000 ( 0.099325) 148 | 0.090000 0.000000 0.090000 ( 0.097779) 149 | 150 | time.getutc.strftime(Transit::TIME_FORMAT) 151 | 2014-04-18T19:35:20.150Z 152 | 0.030000 0.000000 0.030000 ( 0.022180) 153 | 0.020000 0.000000 0.020000 ( 0.023639) 154 | 0.030000 0.000000 0.030000 ( 0.027751) 155 | 156 | time.to_datetime.new_offset(0).strftime(Transit::TIME_FORMAT) 157 | 2014-04-18T19:35:20.150Z 158 | 0.040000 0.000000 0.040000 ( 0.043754) 159 | 0.040000 0.000000 0.040000 ( 0.039013) 160 | 0.040000 0.000000 0.040000 ( 0.044270) 161 | 162 | date.to_datetime.strftime(Transit::TIME_FORMAT) 163 | 2014-04-18T00:00:00.000Z 164 | 0.020000 0.000000 0.020000 ( 0.020463) 165 | 0.020000 0.000000 0.020000 ( 0.020716) 166 | 0.030000 0.000000 0.030000 ( 0.022477) 167 | 168 | date.to_time.strftime(Transit::TIME_FORMAT) 169 | 2014-04-18T00:00:00.000Z 170 | 0.090000 0.000000 0.090000 ( 0.098463) 171 | 0.090000 0.000000 0.090000 ( 0.082547) 172 | 0.080000 0.000000 0.080000 ( 0.088301) 173 | 174 | Time.gm($date.year, $date.month, $date.day).iso8601(3) 175 | 2014-04-18T00:00:00.000Z 176 | 0.050000 0.000000 0.050000 ( 0.050571) 177 | 0.070000 0.000000 0.070000 ( 0.063049) 178 | 0.050000 0.000000 0.050000 ( 0.049378) 179 | 180 | Time.gm($date.year, $date.month, $date.day).strftime(Transit::TIME_FORMAT) 181 | 2014-04-18T00:00:00.000Z 182 | 0.030000 0.000000 0.030000 ( 0.037934) 183 | 0.040000 0.000000 0.040000 ( 0.038376) 184 | 0.040000 0.000000 0.040000 ( 0.037938) 185 | -------------------------------------------------------------------------------- /bin/benchmark: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ## Copyright (c) Cognitect, Inc. 4 | ## All rights reserved. 5 | 6 | $LOAD_PATH << File.expand_path("../../lib", __FILE__) 7 | require 'benchmark' 8 | require 'stringio' 9 | require 'json' 10 | require 'transit' 11 | require 'oj' 12 | 13 | def read_transit(data) 14 | io = StringIO.new(data, 'r+') 15 | Transit::Reader.new(:json, io).read 16 | end 17 | 18 | def write_transit(data) 19 | io = StringIO.new('', 'w+') 20 | Transit::Writer.new(:json, io).write(data) 21 | end 22 | 23 | def read_json(data) 24 | io = StringIO.new(data, 'r+') 25 | Oj::load(data) 26 | end 27 | 28 | data = nil 29 | 30 | n = 100 31 | 32 | include Benchmark 33 | puts "**************************" 34 | puts "transit-json" 35 | open("../transit-format/examples/0.8/example.json", 'r') {|f| data = f.read} 36 | Benchmark.benchmark(CAPTION, 20, FORMAT, "avg read transit:", "avg read oj:", "avg write transit:") do |bm| 37 | t = bm.report("read transit (#{n} x):") { n.times { read_transit(data) } } 38 | y = bm.report("read oj (#{n} x):") { n.times { read_json(data) } } 39 | w = bm.report("write transit (#{n} x):") { n.times { write_transit(data) } } 40 | [t/n, y/n, w/n] 41 | end 42 | 43 | puts "**************************" 44 | puts "transit-json-verbose" 45 | open("../transit-format/examples/0.8/example.verbose.json", 'r') {|f| data = f.read} 46 | Benchmark.benchmark(CAPTION, 20, FORMAT, "avg read transit:", "avg read oj:", "avg write transit:") do |bm| 47 | t = bm.report("read transit (#{n} x):") { n.times { read_transit(data) } } 48 | y = bm.report("read oj (#{n} x):") { n.times { read_json(data) } } 49 | w = bm.report("write transit (#{n} x):") { n.times { write_transit(data) } } 50 | [t/n, y/n, w/n] 51 | end 52 | puts "**************************" 53 | -------------------------------------------------------------------------------- /bin/read-transit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Copyright (c) Cognitect, Inc. 3 | # All rights reserved. 4 | 5 | $LOAD_PATH << 'lib' 6 | require 'transit' 7 | 8 | transport = ARGV[0] || "json" 9 | 10 | r = Transit::Reader.new(transport.gsub("-","_").to_sym, STDIN) 11 | r.read {|o| p o} 12 | -------------------------------------------------------------------------------- /bin/read-write: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Copyright (c) Cognitect, Inc. 3 | # All rights reserved. 4 | 5 | $LOAD_PATH << 'lib' 6 | require 'transit' 7 | 8 | transport = (ARGV[0] || "json").gsub("-","_").to_sym 9 | 10 | r = Transit::Reader.new(transport, STDIN) 11 | w = Transit::Writer.new(transport, STDOUT) 12 | 13 | r.read {|o| w.write o} 14 | -------------------------------------------------------------------------------- /bin/roundtrip: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (c) Cognitect, Inc. 3 | # All rights reserved. 4 | 5 | cd `dirname $0`/.. 6 | 7 | exec bin/read-write "$@" 8 | -------------------------------------------------------------------------------- /bin/rspec-across-supported-versions: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # Copyright (c) Cognitect, Inc. 3 | # All rights reserved. 4 | 5 | for r in 1.9.3-p547 2.0.0-p353 2.1.0 2.1.1 2.1.2 jruby-1.7.13 jruby-1.7.14 jruby-1.7.15 jruby-1.7.16 6 | do 7 | eval "rbenv local $r" 8 | echo `ruby -v` 9 | rake compile 10 | rspec 11 | done 12 | 13 | rm .ruby-version 14 | -------------------------------------------------------------------------------- /build/jruby_version: -------------------------------------------------------------------------------- 1 | 1.7.16 2 | -------------------------------------------------------------------------------- /build/release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Returns the revision number used for deployment. 4 | 5 | rbenv local --unset 6 | rake release 7 | rake use_jruby 8 | rake release 9 | -------------------------------------------------------------------------------- /build/revision: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Returns the revision number used for deployment. 4 | 5 | set -e 6 | 7 | REVISION=`git --no-replace-objects describe --tags --match v0.0` 8 | 9 | # Extract the version number from the string. Do this in two steps so 10 | # it is a little easier to understand. 11 | REVISION=${REVISION:5} # drop the first 5 characters 12 | REVISION=${REVISION:0:${#REVISION}-9} # drop the last 9 characters 13 | 14 | echo $REVISION 15 | -------------------------------------------------------------------------------- /dev/irb_tools.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require 'stringio' 16 | 17 | def time 18 | start = Time.now 19 | yield 20 | puts "Elapsed: #{Time.now - start}" 21 | end 22 | 23 | class Object 24 | def to_transit(format=:json) 25 | sio = StringIO.new 26 | Transit::Writer.new(format, sio).write(self) 27 | sio.string 28 | end 29 | end 30 | 31 | class String 32 | def from_transit(format=:json) 33 | sio = StringIO.new(self) 34 | Transit::Reader.new(format, sio).read 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /ext/com/cognitect/transit/ruby/TransitService.java: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Cognitect. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS-IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package com.cognitect.transit.ruby; 17 | 18 | import java.io.IOException; 19 | 20 | import org.jruby.Ruby; 21 | import org.jruby.RubyClass; 22 | import org.jruby.RubyModule; 23 | import org.jruby.runtime.ObjectAllocator; 24 | import org.jruby.runtime.builtin.IRubyObject; 25 | import org.jruby.runtime.load.BasicLibraryService; 26 | 27 | public class TransitService implements BasicLibraryService { 28 | 29 | @Override 30 | public boolean basicLoad(Ruby runtime) throws IOException { 31 | RubyModule transit = runtime.defineModule("Transit"); 32 | RubyModule unmarshaler = transit.defineModuleUnder("Unmarshaler"); 33 | RubyClass json_unmarshaler = unmarshaler.defineClassUnder("Json", runtime.getObject(), new ObjectAllocator() { 34 | private com.cognitect.transit.ruby.unmarshaler.Json json = null; 35 | public IRubyObject allocate(Ruby runtime, RubyClass rubyClass) { 36 | if (json == null) { 37 | json = new com.cognitect.transit.ruby.unmarshaler.Json(runtime, rubyClass); 38 | } 39 | try { 40 | com.cognitect.transit.ruby.unmarshaler.Json clone = 41 | (com.cognitect.transit.ruby.unmarshaler.Json)json.clone(); 42 | clone.setMetaClass(rubyClass); 43 | return clone; 44 | } catch (CloneNotSupportedException e) { 45 | return new com.cognitect.transit.ruby.unmarshaler.Json(runtime, rubyClass); 46 | } 47 | } 48 | }); 49 | json_unmarshaler.defineAnnotatedMethods(com.cognitect.transit.ruby.unmarshaler.Json.class); 50 | 51 | RubyClass messagepack_unmarshaler = unmarshaler.defineClassUnder("MessagePack", runtime.getObject(), new ObjectAllocator() { 52 | private com.cognitect.transit.ruby.unmarshaler.MessagePack msgpack = null; 53 | public IRubyObject allocate(Ruby runtime, RubyClass rubyClass) { 54 | if (msgpack == null) { 55 | msgpack = new com.cognitect.transit.ruby.unmarshaler.MessagePack(runtime, rubyClass); 56 | } 57 | try { 58 | com.cognitect.transit.ruby.unmarshaler.MessagePack clone = 59 | (com.cognitect.transit.ruby.unmarshaler.MessagePack)msgpack.clone(); 60 | clone.setMetaClass(rubyClass); 61 | return clone; 62 | } catch (CloneNotSupportedException e) { 63 | return new com.cognitect.transit.ruby.unmarshaler.MessagePack(runtime, rubyClass); 64 | } 65 | } 66 | }); 67 | messagepack_unmarshaler.defineAnnotatedMethods(com.cognitect.transit.ruby.unmarshaler.MessagePack.class); 68 | 69 | RubyModule marshaler = transit.defineModuleUnder("Marshaler"); 70 | RubyClass json_marshaler = marshaler.defineClassUnder("Json", runtime.getObject(), new ObjectAllocator() { 71 | private com.cognitect.transit.ruby.marshaler.Json json = null; 72 | public IRubyObject allocate(Ruby runtime, RubyClass rubyClass) { 73 | if (json == null) { 74 | json = new com.cognitect.transit.ruby.marshaler.Json(runtime, rubyClass); 75 | } 76 | try { 77 | com.cognitect.transit.ruby.marshaler.Json clone = 78 | (com.cognitect.transit.ruby.marshaler.Json)json.clone(); 79 | clone.setMetaClass(rubyClass); 80 | return clone; 81 | } catch (CloneNotSupportedException e) { 82 | return new com.cognitect.transit.ruby.marshaler.Json(runtime, rubyClass); 83 | } 84 | } 85 | }); 86 | json_marshaler.defineAnnotatedMethods(com.cognitect.transit.ruby.marshaler.Json.class); 87 | 88 | RubyClass verbosejson_marshaler = marshaler.defineClassUnder("VerboseJson", runtime.getObject(), new ObjectAllocator() { 89 | private com.cognitect.transit.ruby.marshaler.VerboseJson verboseJson = null; 90 | public IRubyObject allocate(Ruby runtime, RubyClass rubyClass) { 91 | if (verboseJson == null) { 92 | verboseJson = new com.cognitect.transit.ruby.marshaler.VerboseJson(runtime, rubyClass); 93 | } 94 | try { 95 | com.cognitect.transit.ruby.marshaler.VerboseJson clone = 96 | (com.cognitect.transit.ruby.marshaler.VerboseJson)verboseJson.clone(); 97 | clone.setMetaClass(rubyClass); 98 | return clone; 99 | } catch (CloneNotSupportedException e) { 100 | return new com.cognitect.transit.ruby.marshaler.VerboseJson(runtime, rubyClass); 101 | } 102 | } 103 | }); 104 | verbosejson_marshaler.defineAnnotatedMethods(com.cognitect.transit.ruby.marshaler.VerboseJson.class); 105 | 106 | RubyClass messagepack_marshaler = marshaler.defineClassUnder("MessagePack", runtime.getObject(), new ObjectAllocator() { 107 | private com.cognitect.transit.ruby.marshaler.MessagePack msgpack = null; 108 | public IRubyObject allocate(Ruby runtime, RubyClass rubyClass) { 109 | if (msgpack == null) { 110 | msgpack = new com.cognitect.transit.ruby.marshaler.MessagePack(runtime, rubyClass); 111 | } 112 | try { 113 | com.cognitect.transit.ruby.marshaler.MessagePack clone = 114 | (com.cognitect.transit.ruby.marshaler.MessagePack)msgpack.clone(); 115 | clone.setMetaClass(rubyClass); 116 | return clone; 117 | } catch (CloneNotSupportedException e) { 118 | return new com.cognitect.transit.ruby.marshaler.MessagePack(runtime, rubyClass); 119 | } 120 | } 121 | }); 122 | messagepack_marshaler.defineAnnotatedMethods(com.cognitect.transit.ruby.marshaler.MessagePack.class); 123 | 124 | return true; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /ext/com/cognitect/transit/ruby/TransitTypeConverter.java: -------------------------------------------------------------------------------- 1 | package com.cognitect.transit.ruby; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | 6 | import org.jruby.Ruby; 7 | import org.jruby.runtime.builtin.IRubyObject; 8 | 9 | public class TransitTypeConverter { 10 | private static final List specialString = Arrays.asList("NaN", "Infinity", "-Infinity"); 11 | 12 | public static boolean needsCostomConverter(Object o) { 13 | if ((o instanceof String) && specialString.contains((String)o)) { 14 | return true; 15 | } else { 16 | return false; 17 | } 18 | } 19 | 20 | public static IRubyObject convertStringToFloat(Ruby runtime, Object o) { 21 | String str = (String)o; 22 | if ("NaN".equals(str)) { 23 | return runtime.newFloat(Double.NaN); 24 | } else if ("Infinity".equals(str)) { 25 | return runtime.newFloat(Double.POSITIVE_INFINITY); 26 | } else if ("-Infinity".equals(str)) { 27 | return runtime.newFloat(Double.NEGATIVE_INFINITY); 28 | } else { 29 | return runtime.getNil(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ext/com/cognitect/transit/ruby/marshaler/Base.java: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Cognitect. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS-IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package com.cognitect.transit.ruby.marshaler; 17 | 18 | import java.io.OutputStream; 19 | import java.util.HashMap; 20 | import java.util.Map; 21 | import java.util.Set; 22 | 23 | import org.jruby.Ruby; 24 | import org.jruby.RubyArray; 25 | import org.jruby.RubyClass; 26 | import org.jruby.RubyHash; 27 | import org.jruby.RubyModule; 28 | import org.jruby.RubyObject; 29 | import org.jruby.RubyString; 30 | import org.jruby.javasupport.JavaUtil; 31 | import org.jruby.runtime.Block; 32 | import org.jruby.runtime.ThreadContext; 33 | import org.jruby.runtime.builtin.IRubyObject; 34 | 35 | import com.cognitect.transit.WriteHandler; 36 | import com.cognitect.transit.Writer; 37 | 38 | public class Base extends RubyObject { 39 | private static final long serialVersionUID = -3179062656279837886L; 40 | protected Writer writer; 41 | 42 | @Override 43 | public Object clone() throws CloneNotSupportedException { 44 | return super.clone(); 45 | } 46 | 47 | public Base(Ruby runtime, RubyClass metaClass) { 48 | super(runtime, metaClass); 49 | } 50 | 51 | protected OutputStream convertRubyIOToOutputStream(ThreadContext context, IRubyObject rubyObject) { 52 | if (rubyObject.respondsTo("to_outputstream")) { 53 | return (OutputStream) rubyObject.callMethod(context, "to_outputstream").toJava(OutputStream.class); 54 | } else { 55 | throw rubyObject.getRuntime().newArgumentError("The first argument is not IO"); 56 | } 57 | } 58 | 59 | /** 60 | * Converts the handlers defined in Ruby to java and wraps them in a single java handler 61 | * that delegates to the correct handler. Assumes that @handlers includes custom handlers 62 | * and any verbose handlers. 63 | */ 64 | protected Map> convertRubyHandlersToJavaHandler( 65 | final ThreadContext context, 66 | IRubyObject arg) { 67 | Map> result = new HashMap>(1); 68 | RubyHash rubyHandlers = (RubyHash)this.getInstanceVariable("@handlers"); 69 | final Map> javaHandlers = new HashMap>(); 70 | 71 | for (Map.Entry entry : (Set)rubyHandlers.entrySet()) { 72 | javaHandlers.put(((RubyModule)entry.getKey()).getName(), 73 | convertRubyToJava(context, (RubyObject)entry.getValue())); 74 | } 75 | result.put(RubyObject.class, new WriteHandler() { 76 | @Override 77 | public WriteHandler getVerboseHandler() { 78 | return null; 79 | } 80 | 81 | private WriteHandler findHandler(Object o) { 82 | if (o instanceof RubyObject) { 83 | RubyArray ancestors = (RubyArray)((RubyObject)o).getMetaClass().callMethod(context, "ancestors"); 84 | for (Object ancestor : ancestors) { 85 | WriteHandler handler = javaHandlers.get(((RubyModule)ancestor).getName()); 86 | if (handler != null) return handler; 87 | } 88 | } 89 | return null; 90 | } 91 | 92 | @Override 93 | public Object rep(Object o) { 94 | WriteHandler handler = findHandler(o); 95 | if (handler != null) return handler.rep(o); 96 | return null; 97 | } 98 | 99 | @Override 100 | public String stringRep(Object o) { 101 | WriteHandler handler = findHandler(o); 102 | if (handler != null) return handler.stringRep(o); 103 | return null; 104 | } 105 | 106 | @Override 107 | public String tag(Object o) { 108 | WriteHandler handler = findHandler(o); 109 | if (handler != null) return handler.tag(o); 110 | return null; 111 | } 112 | }); 113 | return result; 114 | } 115 | 116 | private WriteHandler convertRubyToJava(final ThreadContext context, final RubyObject handler) { 117 | return new WriteHandler() { 118 | @Override 119 | public WriteHandler getVerboseHandler() { 120 | return null; 121 | } 122 | 123 | @Override 124 | public Object rep(Object o) { 125 | IRubyObject ret = 126 | handler.callMethod(context, "rep", JavaUtil.convertJavaToUsableRubyObject(context.getRuntime(), o)); 127 | return ret.toJava(Object.class); 128 | } 129 | 130 | @Override 131 | public String stringRep(Object o) { 132 | RubyString ret = 133 | (RubyString) handler.callMethod(context, "string_rep", JavaUtil.convertJavaToUsableRubyObject(context.getRuntime(), o)); 134 | return ret.asJavaString(); 135 | } 136 | 137 | @Override 138 | public String tag(Object o) { 139 | IRubyObject ret = 140 | handler.callMethod(context, "tag", JavaUtil.convertJavaToUsableRubyObject(context.getRuntime(), o)); 141 | if (ret.isNil()) { 142 | return null; 143 | } else { 144 | return ((RubyString)ret).asJavaString(); 145 | } 146 | } 147 | }; 148 | } 149 | 150 | protected IRubyObject write(ThreadContext context, IRubyObject arg) { 151 | try { 152 | writer.write(arg); 153 | } catch (Throwable t) { 154 | // TODO: use log api to spit out java exception 155 | //e.printStackTrace(); 156 | if (t.getCause() != null) { 157 | throw context.getRuntime().newRuntimeError(t.getCause().getMessage()); 158 | } else { 159 | throw context.getRuntime().newRuntimeError(t.getMessage()); 160 | } 161 | } 162 | return context.getRuntime().getNil(); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /ext/com/cognitect/transit/ruby/marshaler/Json.java: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Cognitect. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS-IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package com.cognitect.transit.ruby.marshaler; 17 | 18 | import java.io.OutputStream; 19 | import java.util.Map; 20 | 21 | import org.jruby.Ruby; 22 | import org.jruby.RubyClass; 23 | import org.jruby.anno.JRubyClass; 24 | import org.jruby.anno.JRubyMethod; 25 | import org.jruby.runtime.ThreadContext; 26 | import org.jruby.runtime.builtin.IRubyObject; 27 | 28 | import com.cognitect.transit.TransitFactory; 29 | import com.cognitect.transit.WriteHandler; 30 | 31 | @JRubyClass(name="Transit::Marshaler::Json") 32 | public class Json extends Base { 33 | private static final long serialVersionUID = 3453641906194326319L; 34 | 35 | public Json(final Ruby runtime, RubyClass rubyClass) { 36 | super(runtime, rubyClass); 37 | } 38 | 39 | /** 40 | args[0] - io : any Ruby IO 41 | args[1] - opts : Ruby Hash 42 | **/ 43 | @JRubyMethod(name="new", meta=true, required=1, rest=true) 44 | public static IRubyObject rbNew(ThreadContext context, IRubyObject klazz, IRubyObject[] args) { 45 | try { 46 | RubyClass rubyClass = (RubyClass)context.getRuntime().getClassFromPath("Transit::Marshaler::Json"); 47 | Json json = (Json)rubyClass.allocate(); 48 | json.callMethod(context, "initialize", args); 49 | json.init(context, args); 50 | return json; 51 | } catch (Throwable t) { 52 | t.printStackTrace(); 53 | throw context.getRuntime().newRuntimeError(t.getMessage()); 54 | } 55 | } 56 | 57 | private void init(ThreadContext context, IRubyObject[] args) { 58 | OutputStream output = convertRubyIOToOutputStream(context, args[0]); 59 | Map> handlers = convertRubyHandlersToJavaHandler(context, args[1]); 60 | writer = TransitFactory.writer(TransitFactory.Format.JSON, output, handlers); 61 | } 62 | 63 | @JRubyMethod 64 | public IRubyObject write(ThreadContext context, IRubyObject arg) { 65 | return super.write(context, arg); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ext/com/cognitect/transit/ruby/marshaler/MessagePack.java: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Cognitect. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS-IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package com.cognitect.transit.ruby.marshaler; 17 | 18 | import java.io.OutputStream; 19 | import java.util.Map; 20 | 21 | import org.jruby.Ruby; 22 | import org.jruby.RubyClass; 23 | import org.jruby.anno.JRubyClass; 24 | import org.jruby.anno.JRubyMethod; 25 | import org.jruby.runtime.ThreadContext; 26 | import org.jruby.runtime.builtin.IRubyObject; 27 | 28 | import com.cognitect.transit.TransitFactory; 29 | import com.cognitect.transit.WriteHandler; 30 | 31 | @JRubyClass(name="Transit::Marshaler::MessagePack") 32 | public class MessagePack extends Base { 33 | private static final long serialVersionUID = -7367649057673037952L; 34 | 35 | public MessagePack(final Ruby runtime, RubyClass rubyClass) { 36 | super(runtime, rubyClass); 37 | } 38 | 39 | /** 40 | args[0] - io : any Ruby IO 41 | args[1] - opts : Ruby Hash 42 | **/ 43 | @JRubyMethod(name="new", meta=true, required=1, rest=true) 44 | public static IRubyObject rbNew(ThreadContext context, IRubyObject klazz, IRubyObject[] args) { 45 | RubyClass rubyClass = (RubyClass)context.getRuntime().getClassFromPath("Transit::Marshaler::MessagePack"); 46 | MessagePack messagepack = (MessagePack)rubyClass.allocate(); 47 | messagepack.callMethod(context, "initialize", args); 48 | messagepack.init(context, args); 49 | return messagepack; 50 | } 51 | 52 | private void init(ThreadContext context, IRubyObject[] args) { 53 | OutputStream output = convertRubyIOToOutputStream(context, args[0]); 54 | Map> handlers = convertRubyHandlersToJavaHandler(context, args[1]); 55 | writer = TransitFactory.writer(TransitFactory.Format.MSGPACK, output, handlers); 56 | } 57 | 58 | @JRubyMethod 59 | public IRubyObject write(ThreadContext context, IRubyObject arg) { 60 | return super.write(context, arg); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ext/com/cognitect/transit/ruby/marshaler/VerboseJson.java: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Cognitect. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS-IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package com.cognitect.transit.ruby.marshaler; 17 | 18 | import java.io.OutputStream; 19 | import java.util.Map; 20 | 21 | import org.jruby.Ruby; 22 | import org.jruby.RubyClass; 23 | import org.jruby.anno.JRubyClass; 24 | import org.jruby.anno.JRubyMethod; 25 | import org.jruby.runtime.ThreadContext; 26 | import org.jruby.runtime.builtin.IRubyObject; 27 | 28 | import com.cognitect.transit.TransitFactory; 29 | import com.cognitect.transit.WriteHandler; 30 | 31 | @JRubyClass(name="Transit::Marshaler::VerboseJson") 32 | public class VerboseJson extends Base { 33 | private static final long serialVersionUID = 7872087524091784518L; 34 | 35 | public VerboseJson(final Ruby runtime, RubyClass rubyClass) { 36 | super(runtime, rubyClass); 37 | } 38 | 39 | /** 40 | args[0] - io : any Ruby IO 41 | args[1] - opts : Ruby Hash 42 | **/ 43 | @JRubyMethod(name="new", meta=true, required=1, rest=true) 44 | public static IRubyObject rbNew(ThreadContext context, IRubyObject klazz, IRubyObject[] args) { 45 | RubyClass rubyClass = (RubyClass)context.getRuntime().getClassFromPath("Transit::Marshaler::VerboseJson"); 46 | VerboseJson verbosejson = (VerboseJson)rubyClass.allocate(); 47 | verbosejson.callMethod(context, "initialize", args); 48 | verbosejson.init(context, args); 49 | return verbosejson; 50 | } 51 | 52 | private void init(ThreadContext context, IRubyObject[] args) { 53 | OutputStream output = convertRubyIOToOutputStream(context, args[0]); 54 | Map> handlers = convertRubyHandlersToJavaHandler(context, args[1]); 55 | writer = TransitFactory.writer(TransitFactory.Format.JSON_VERBOSE, output, handlers); 56 | } 57 | 58 | @JRubyMethod 59 | public IRubyObject write(ThreadContext context, IRubyObject arg) { 60 | return super.write(context, arg); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ext/com/cognitect/transit/ruby/unmarshaler/Base.java: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Cognitect. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS-IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package com.cognitect.transit.ruby.unmarshaler; 17 | 18 | import java.io.InputStream; 19 | import java.util.HashMap; 20 | import java.util.Map; 21 | 22 | import org.jruby.Ruby; 23 | import org.jruby.RubyClass; 24 | import org.jruby.RubyHash; 25 | import org.jruby.RubyObject; 26 | import org.jruby.RubyString; 27 | import org.jruby.javasupport.JavaUtil; 28 | import org.jruby.runtime.Block; 29 | import org.jruby.runtime.ThreadContext; 30 | import org.jruby.runtime.builtin.IRubyObject; 31 | 32 | import com.cognitect.transit.DefaultReadHandler; 33 | import com.cognitect.transit.ReadHandler; 34 | import com.cognitect.transit.Reader; 35 | import com.cognitect.transit.impl.ReaderFactory; 36 | 37 | public abstract class Base extends RubyObject { 38 | private static final long serialVersionUID = -2693178195157618851L; 39 | protected Reader reader; 40 | 41 | @Override 42 | public Object clone() throws CloneNotSupportedException { 43 | return super.clone(); 44 | } 45 | 46 | public Base(final Ruby runtime, RubyClass rubyClass) { 47 | super(runtime, rubyClass); 48 | } 49 | 50 | protected static IRubyObject newDecoder(ThreadContext context, IRubyObject opts) { 51 | RubyClass decoderClass = (RubyClass) context.getRuntime().getClassFromPath("Transit::Decoder"); 52 | return decoderClass.callMethod(context, "new", opts); 53 | } 54 | 55 | protected InputStream convertRubyIOToInputStream(ThreadContext context, IRubyObject rubyObject) { 56 | if (rubyObject.respondsTo("to_inputstream")) { 57 | return (InputStream) rubyObject.callMethod(context, "to_inputstream").toJava(InputStream.class); 58 | } else { 59 | throw rubyObject.getRuntime().newArgumentError("The first argument is not IO"); 60 | } 61 | } 62 | 63 | protected Map> convertRubyHandlersToJavaHandlers( 64 | final ThreadContext context) { 65 | IRubyObject decoder = this.getInstanceVariable("@decoder"); 66 | IRubyObject ivar = decoder.callMethod(context.getRuntime().getCurrentContext(), "instance_variable_get", context.getRuntime().newString("@handlers")); 67 | final RubyHash handlers = (RubyHash)ivar; 68 | Map> javaHandlers = new HashMap>(); 69 | for (Object key : handlers.keySet()) { 70 | final IRubyObject handler = (IRubyObject)handlers.get(key); 71 | javaHandlers.put((String)key, new ReadHandler() { 72 | public IRubyObject fromRep(Object o) { 73 | return handler.callMethod(context, "from_rep", 74 | JavaUtil.convertJavaToUsableRubyObject(context.getRuntime(), o)); 75 | } 76 | }); 77 | } 78 | // replaces TimeStringHandler to cover JRuby's bug in DateTime.iso8601() method 79 | if (((RubyObject)handlers.get("t")).getMetaClass().getName(). 80 | equals("Transit::ReadHandlers::TimeStringHandler")) { 81 | javaHandlers.put("t", new ReadHandler() { 82 | public IRubyObject fromRep(Object o) { 83 | RubyClass klazz = (RubyClass) context.getRuntime().getClassFromPath("DateTime"); 84 | RubyString string = context.getRuntime().newString((String)o); 85 | RubyString format = context.getRuntime().newString("%Y-%m-%dT%H:%M:%S.%N%z"); 86 | return klazz.callMethod(context, "strptime", new IRubyObject[]{string, format}); 87 | } 88 | }); 89 | } 90 | return javaHandlers; 91 | } 92 | 93 | protected DefaultReadHandler convertRubyDefaultHandlerToJavaDefaultHandler( 94 | final ThreadContext context) { 95 | IRubyObject decoder = this.getInstanceVariable("@decoder"); 96 | IRubyObject ivar = decoder.callMethod(context.getRuntime().getCurrentContext(), 97 | "instance_variable_get", 98 | context.getRuntime().newString("@default_handler")); 99 | final RubyObject handler = (RubyObject)ivar; 100 | DefaultReadHandler javaHandler = new DefaultReadHandler() { 101 | public IRubyObject fromRep(String tag, Object rep) { 102 | return handler.callMethod("from_rep", 103 | context.getRuntime().newString(tag), 104 | JavaUtil.convertJavaToUsableRubyObject(context.getRuntime(), rep)); 105 | } 106 | }; 107 | return javaHandler; 108 | } 109 | 110 | /** 111 | read method accepts a block 112 | **/ 113 | protected IRubyObject read(ThreadContext context, Block block) { 114 | try { 115 | Object o; 116 | while ((o = reader.read()) != null) { 117 | IRubyObject value; 118 | if (o instanceof IRubyObject) { 119 | value = (IRubyObject)o; 120 | } else { 121 | value = JavaUtil.convertJavaToUsableRubyObject(context.getRuntime(), o); 122 | } 123 | if ((value != null) && block.isGiven()) { 124 | block.yield(context, value); 125 | } else { 126 | return value; 127 | } 128 | } 129 | return context.getRuntime().getNil(); 130 | } catch (Throwable t) { 131 | throw context.getRuntime().newRuntimeError(t.getMessage()); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /ext/com/cognitect/transit/ruby/unmarshaler/Json.java: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Cognitect. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS-IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package com.cognitect.transit.ruby.unmarshaler; 17 | 18 | import java.io.InputStream; 19 | import java.util.Map; 20 | 21 | import org.jruby.Ruby; 22 | import org.jruby.RubyClass; 23 | import org.jruby.anno.JRubyClass; 24 | import org.jruby.anno.JRubyMethod; 25 | import org.jruby.runtime.Block; 26 | import org.jruby.runtime.ThreadContext; 27 | import org.jruby.runtime.builtin.IRubyObject; 28 | 29 | import com.cognitect.transit.ArrayReader; 30 | import com.cognitect.transit.DefaultReadHandler; 31 | import com.cognitect.transit.MapReader; 32 | import com.cognitect.transit.ReadHandler; 33 | import com.cognitect.transit.SPI.ReaderSPI; 34 | 35 | @JRubyClass(name="Transit::Unmarshaler::Json") 36 | public class Json extends Base { 37 | private static final long serialVersionUID = -6605166968548176488L; 38 | 39 | public Json(final Ruby runtime, RubyClass rubyClass) { 40 | super(runtime, rubyClass); 41 | } 42 | 43 | /** 44 | args[0] - io : any Ruby IO 45 | args[1] - opts : Ruby Hash 46 | **/ 47 | @JRubyMethod(name="new", meta=true, required=1, rest=true) 48 | public static IRubyObject rbNew(ThreadContext context, IRubyObject klazz, IRubyObject[] args) { 49 | RubyClass rubyClass = (RubyClass)context.getRuntime().getClassFromPath("Transit::Unmarshaler::Json"); 50 | Json json = (Json)rubyClass.allocate(); 51 | json.instance_variable_set(context.getRuntime().newString("@decoder"), newDecoder(context, args[1])); 52 | json.init(context, args); 53 | return json; 54 | } 55 | 56 | private void init(final ThreadContext context, IRubyObject[] args) { 57 | InputStream input = convertRubyIOToInputStream(context, args[0]); 58 | Map> handlers = convertRubyHandlersToJavaHandlers(context); 59 | DefaultReadHandler defaultHandler = convertRubyDefaultHandlerToJavaDefaultHandler(context); 60 | reader = new RubyReaders.JsonReaderImpl(input, handlers, defaultHandler); 61 | ((ReaderSPI)reader).setBuilders((MapReader)(new RubyMapReader(context.getRuntime())), 62 | (ArrayReader)(new RubyArrayReader(context.getRuntime()))); 63 | } 64 | 65 | /** 66 | read method accepts a block 67 | **/ 68 | @JRubyMethod 69 | public IRubyObject read(ThreadContext context, Block block) { 70 | return super.read(context, block); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ext/com/cognitect/transit/ruby/unmarshaler/MessagePack.java: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Cognitect. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS-IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package com.cognitect.transit.ruby.unmarshaler; 17 | 18 | import java.io.InputStream; 19 | import java.util.Map; 20 | 21 | import org.jruby.Ruby; 22 | import org.jruby.RubyClass; 23 | import org.jruby.anno.JRubyClass; 24 | import org.jruby.anno.JRubyMethod; 25 | import org.jruby.runtime.Block; 26 | import org.jruby.runtime.ThreadContext; 27 | import org.jruby.runtime.builtin.IRubyObject; 28 | 29 | import com.cognitect.transit.ArrayReader; 30 | import com.cognitect.transit.DefaultReadHandler; 31 | import com.cognitect.transit.MapReader; 32 | import com.cognitect.transit.ReadHandler; 33 | import com.cognitect.transit.SPI.ReaderSPI; 34 | 35 | @JRubyClass(name="Transit::Unmarshaler::MessagePack") 36 | public class MessagePack extends Base { 37 | private static final long serialVersionUID = 8837562079042631858L; 38 | 39 | public MessagePack(final Ruby runtime, RubyClass rubyClass) { 40 | super(runtime, rubyClass); 41 | } 42 | 43 | /** 44 | args[0] - io : any Ruby IO 45 | args[1] - opts : Ruby Hash 46 | **/ 47 | @JRubyMethod(name="new", meta=true, required=1, rest=true) 48 | public static IRubyObject rbNew(ThreadContext context, IRubyObject klazz, IRubyObject[] args) { 49 | RubyClass rubyClass = (RubyClass)context.getRuntime().getClassFromPath("Transit::Unmarshaler::MessagePack"); 50 | MessagePack messagepack = (MessagePack)rubyClass.allocate(); 51 | messagepack.instance_variable_set(context.getRuntime().newString("@decoder"), newDecoder(context, args[1])); 52 | messagepack.init(context, args); 53 | return messagepack; 54 | } 55 | 56 | private void init(ThreadContext context, IRubyObject[] args) { 57 | InputStream input = convertRubyIOToInputStream(context, args[0]); 58 | Map> handlers = convertRubyHandlersToJavaHandlers(context); 59 | DefaultReadHandler defaultHandler = convertRubyDefaultHandlerToJavaDefaultHandler(context); 60 | reader = new RubyReaders.MsgPackReaderImpl(input, handlers, defaultHandler); 61 | ((ReaderSPI)reader).setBuilders((MapReader)(new RubyMapReader(context.getRuntime())), 62 | (ArrayReader)(new RubyArrayReader(context.getRuntime()))); 63 | } 64 | 65 | @JRubyMethod 66 | public IRubyObject read(ThreadContext context, Block block) { 67 | return super.read(context, block); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /ext/com/cognitect/transit/ruby/unmarshaler/RubyArrayReader.java: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Cognitect. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS-IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package com.cognitect.transit.ruby.unmarshaler; 17 | 18 | import org.jruby.Ruby; 19 | import org.jruby.RubyArray; 20 | import org.jruby.javasupport.JavaUtil; 21 | import org.jruby.runtime.builtin.IRubyObject; 22 | 23 | import com.cognitect.transit.ArrayReader; 24 | import com.cognitect.transit.ruby.TransitTypeConverter; 25 | 26 | public class RubyArrayReader implements ArrayReader { 27 | private Ruby runtime; 28 | 29 | public RubyArrayReader(Ruby runtime) { 30 | this.runtime = runtime; 31 | } 32 | 33 | @Override 34 | public RubyArray add(RubyArray array, Object item) { 35 | IRubyObject value; 36 | if (TransitTypeConverter.needsCostomConverter(item)) { 37 | value = TransitTypeConverter.convertStringToFloat(runtime, item); 38 | } else { 39 | value = JavaUtil.convertJavaToUsableRubyObject(runtime, item); 40 | } 41 | array.callMethod(array.getRuntime().getCurrentContext(), "<<", value); 42 | return array; 43 | } 44 | 45 | @Override 46 | public RubyArray complete(RubyArray array) { 47 | return array; 48 | } 49 | 50 | @Override 51 | public RubyArray init() { 52 | return runtime.newArray(); 53 | } 54 | 55 | @Override 56 | public RubyArray init(int size) { 57 | return runtime.newArray(size); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ext/com/cognitect/transit/ruby/unmarshaler/RubyMapReader.java: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Cognitect. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS-IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package com.cognitect.transit.ruby.unmarshaler; 17 | 18 | import org.jruby.Ruby; 19 | import org.jruby.RubyHash; 20 | import org.jruby.javasupport.JavaUtil; 21 | import org.jruby.runtime.builtin.IRubyObject; 22 | 23 | import com.cognitect.transit.MapReader; 24 | import com.cognitect.transit.ruby.TransitTypeConverter; 25 | 26 | public class RubyMapReader implements MapReader { 27 | private Ruby runtime; 28 | 29 | public RubyMapReader(Ruby runtime) { 30 | this.runtime = runtime; 31 | } 32 | 33 | @Override 34 | public RubyHash add(RubyHash hash, Object key, Object value) { 35 | IRubyObject ruby_key = convertJavaToRuby(key); 36 | IRubyObject ruby_value = convertJavaToRuby(value); 37 | IRubyObject[] args = new IRubyObject[]{ruby_key, ruby_value}; 38 | hash.callMethod(runtime.getCurrentContext(), "[]=", args); 39 | return hash; 40 | } 41 | 42 | private IRubyObject convertJavaToRuby(Object o) { 43 | if (TransitTypeConverter.needsCostomConverter(o)) { 44 | return TransitTypeConverter.convertStringToFloat(runtime, o); 45 | } else { 46 | return JavaUtil.convertJavaToUsableRubyObject(runtime, o); 47 | } 48 | } 49 | 50 | @Override 51 | public RubyHash complete(RubyHash hash) { 52 | return hash; 53 | } 54 | 55 | @Override 56 | public RubyHash init() { 57 | return RubyHash.newHash(runtime); 58 | } 59 | 60 | @Override 61 | public RubyHash init(int size) { 62 | return RubyHash.newHash(runtime); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /ext/com/cognitect/transit/ruby/unmarshaler/RubyReaders.java: -------------------------------------------------------------------------------- 1 | package com.cognitect.transit.ruby.unmarshaler; 2 | 3 | import java.io.InputStream; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | import org.msgpack.MessagePack; 8 | 9 | import com.cognitect.transit.ArrayReader; 10 | import com.cognitect.transit.DefaultReadHandler; 11 | import com.cognitect.transit.MapReader; 12 | import com.cognitect.transit.ReadHandler; 13 | import com.cognitect.transit.Reader; 14 | import com.cognitect.transit.SPI.ReaderSPI; 15 | import com.cognitect.transit.impl.AbstractParser; 16 | import com.cognitect.transit.impl.JsonParser; 17 | import com.cognitect.transit.impl.ListBuilderImpl; 18 | import com.cognitect.transit.impl.MapBuilderImpl; 19 | import com.cognitect.transit.impl.MsgpackParser; 20 | import com.cognitect.transit.impl.ReadCache; 21 | import com.fasterxml.jackson.core.JsonFactory; 22 | 23 | public class RubyReaders { 24 | private abstract static class ReaderImpl implements Reader, ReaderSPI { 25 | InputStream in; 26 | Map> handlers; 27 | DefaultReadHandler defaultHandler; 28 | MapReader, Object, Object> mapBuilder; 29 | ArrayReader, Object> listBuilder; 30 | ReadCache cache; 31 | AbstractParser p; 32 | boolean initialized; 33 | 34 | public ReaderImpl(InputStream in, Map> handlers, DefaultReadHandler defaultHandler) { 35 | this.initialized = false; 36 | this.in = in; 37 | this.handlers = handlers; 38 | this.defaultHandler = defaultHandler; 39 | this.cache = new ReadCache(); 40 | } 41 | 42 | @Override 43 | @SuppressWarnings("unchecked") 44 | public T read() { 45 | if (!initialized) initialize(); 46 | try { 47 | return (T) p.parse(cache.init()); 48 | } catch (Throwable e) { 49 | throw new RuntimeException(e); 50 | } 51 | } 52 | 53 | @Override 54 | public Reader setBuilders(MapReader, Object, Object> mapBuilder, 55 | ArrayReader, Object> listBuilder) { 56 | if (initialized) throw new IllegalStateException("Cannot set builders after read has been called"); 57 | this.mapBuilder = mapBuilder; 58 | this.listBuilder = listBuilder; 59 | return this; 60 | } 61 | 62 | private void ensureBuilders() { 63 | if (mapBuilder == null) mapBuilder = new MapBuilderImpl(); 64 | if (listBuilder == null) listBuilder = new ListBuilderImpl(); 65 | } 66 | 67 | protected void initialize() { 68 | ensureBuilders(); 69 | p = createParser(); 70 | initialized = true; 71 | } 72 | 73 | protected abstract AbstractParser createParser(); 74 | } 75 | 76 | static class JsonReaderImpl extends ReaderImpl { 77 | 78 | public JsonReaderImpl(InputStream in, Map> handlers, DefaultReadHandler defaultHandler) { 79 | super(in, handlers, defaultHandler); 80 | } 81 | 82 | @Override 83 | protected AbstractParser createParser() { 84 | try { 85 | JsonFactory jf = new JsonFactory(); 86 | com.fasterxml.jackson.core.JsonParser json_parser = jf.createParser(in); 87 | json_parser.enable(com.fasterxml.jackson.core.JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS); 88 | return new JsonParser(json_parser, handlers, defaultHandler, 89 | mapBuilder, listBuilder); 90 | } catch (Throwable e) { 91 | throw new RuntimeException(e); 92 | } 93 | } 94 | } 95 | 96 | static class MsgPackReaderImpl extends ReaderImpl { 97 | 98 | public MsgPackReaderImpl(InputStream in, Map> handlers, DefaultReadHandler defaultHandler) { 99 | super(in, handlers, defaultHandler); 100 | } 101 | 102 | @Override 103 | protected AbstractParser createParser() { 104 | MessagePack mp = new MessagePack(); 105 | return new MsgpackParser(mp.createUnpacker(in), handlers, defaultHandler, 106 | mapBuilder, listBuilder); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/transit.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | # WriteHandlers convert instances of Ruby types to their corresponding Transit 17 | # semantic types, and ReadHandlers read convert transit values back into instances 18 | # of Ruby types. transit-ruby ships with default sets of WriteHandlers for each 19 | # of the Ruby types that map naturally to transit types, and ReadHandlers for each 20 | # transit type. For the common case, the 21 | # built-in handlers will suffice, but you can add your own extension types and/or 22 | # override the built-in handlers. 23 | # 24 | # For example, Ruby has Date, Time, and DateTime, each with their 25 | # own semantics. Transit has an instance type, which does not 26 | # differentiate between Date and Time, so transit-ruby writes Dates, 27 | # Times, and DateTimes as transit instances, and reads transit 28 | # instances as DateTimes. If your application cares that Dates are 29 | # different from DateTimes, you could register custom write and read 30 | # handlers, overriding the built-in DateHandler and adding a new DateReadHandler. 31 | # 32 | # ```ruby 33 | # class DateWriteHandler 34 | # def tag(_) "D" end 35 | # def rep(o) o.to_s end 36 | # def string_rep(o) o.to_s end 37 | # end 38 | # 39 | # class DateReadHandler 40 | # def from_rep(rep) 41 | # Date.parse(rep) 42 | # end 43 | # end 44 | # 45 | # io = StringIO.new('','w+') 46 | # writer = Transit::Writer.new(:json, io, :handlers => {Date => DateWriteHandler.new}) 47 | # writer.write(Date.new(2014,7,22)) 48 | # io.string 49 | # # => "[\"~#'\",\"~D2014-07-22\"]\n" 50 | # 51 | # reader = Transit::Reader.new(:json, StringIO.new(io.string), :handlers => {"D" => DateReadHandler.new}) 52 | # reader.read 53 | # # => # 54 | # ``` 55 | module Transit 56 | ESC = "~" 57 | SUB = "^" 58 | RES = "`" 59 | TAG = "~#" 60 | MAP_AS_ARRAY = "^ " 61 | TIME_FORMAT = "%FT%H:%M:%S.%LZ" 62 | QUOTE = "'" 63 | 64 | MAX_INT = 2**63 - 1 65 | MIN_INT = -2**63 66 | 67 | JSON_MAX_INT = 2**53 - 1 68 | JSON_MIN_INT = -JSON_MAX_INT 69 | 70 | def jruby? 71 | defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby" 72 | end 73 | module_function :jruby? 74 | end 75 | 76 | require 'set' 77 | require 'time' 78 | require 'uri' 79 | require 'base64' 80 | require 'bigdecimal' 81 | require 'securerandom' 82 | require 'forwardable' 83 | require 'addressable/uri' 84 | require 'transit/date_time_util' 85 | require 'transit/transit_types' 86 | require 'transit/rolling_cache' 87 | require 'transit/write_handlers' 88 | require 'transit/read_handlers' 89 | require 'transit/marshaler/base' 90 | require 'transit/writer' 91 | require 'transit/decoder' 92 | require 'transit/reader' 93 | 94 | if Transit::jruby? 95 | require 'lock_jar' 96 | LockJar.lock(File.join(File.dirname(__FILE__), "..", "Jarfile")) 97 | LockJar.load 98 | require 'transit.jar' 99 | require 'jruby' 100 | com.cognitect.transit.ruby.TransitService.new.basicLoad(JRuby.runtime) 101 | require 'transit/marshaler/jruby/json' 102 | require 'transit/marshaler/jruby/messagepack' 103 | else 104 | require 'transit/marshaler/cruby/json' 105 | require 'transit/marshaler/cruby/messagepack' 106 | require 'transit/unmarshaler/cruby/json' 107 | require 'transit/unmarshaler/cruby/messagepack' 108 | end 109 | -------------------------------------------------------------------------------- /lib/transit/date_time_util.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | module Transit 16 | # @api private 17 | module DateTimeUtil 18 | def to_millis(v) 19 | case v 20 | when DateTime 21 | t = v.new_offset(0).to_time 22 | when Date 23 | t = Time.gm(v.year, v.month, v.day) 24 | when Time 25 | t = v 26 | else 27 | raise "Don't know how to get millis from #{t.inspect}" 28 | end 29 | (t.to_i * 1000) + (t.usec / 1000.0).round 30 | end 31 | 32 | def from_millis(millis) 33 | t = Time.at(millis / 1000).utc 34 | DateTime.new(t.year, t.month, t.day, t.hour, t.min, t.sec + (millis % 1000 * 0.001)) 35 | end 36 | 37 | module_function :to_millis, :from_millis 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/transit/decoder.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | module Transit 16 | # Converts a transit value to an instance of a type 17 | # @api private 18 | class Decoder 19 | MUTEX = Mutex.new 20 | HANDLER_CACHE = {} 21 | 22 | ESC_ESC = "#{ESC}#{ESC}" 23 | ESC_SUB = "#{ESC}#{SUB}" 24 | ESC_RES = "#{ESC}#{RES}" 25 | 26 | IDENTITY = ->(v){v} 27 | 28 | GROUND_TAGS = %w[_ s ? i d b ' array map] 29 | 30 | def initialize(options={}) 31 | custom_handlers = options[:handlers] || {} 32 | custom_handlers.each {|k,v| validate_handler(k,v)} 33 | MUTEX.synchronize do 34 | if HANDLER_CACHE.has_key?(custom_handlers) 35 | @handlers = HANDLER_CACHE[custom_handlers] 36 | else 37 | @handlers = ReadHandlers::DEFAULT_READ_HANDLERS.merge(custom_handlers) 38 | end 39 | 40 | end 41 | @default_handler = options[:default_handler] || ReadHandlers::DEFAULT_READ_HANDLER 42 | end 43 | 44 | # @api private 45 | class Tag 46 | attr_reader :value 47 | def initialize(value) 48 | @value = value 49 | end 50 | end 51 | 52 | # Decodes a transit value to a corresponding object 53 | # 54 | # @param node a transit value to be decoded 55 | # @param cache 56 | # @param as_map_key 57 | # @return decoded object 58 | def decode(node, cache=RollingCache.new, as_map_key=false) 59 | case node 60 | when String 61 | if cache.has_key?(node) 62 | cache.read(node) 63 | else 64 | parsed = if !node.start_with?(ESC) 65 | node 66 | elsif node.start_with?(TAG) 67 | Tag.new(node[2..-1]) 68 | elsif handler = @handlers[node[1]] 69 | handler.from_rep(node[2..-1]) 70 | elsif node.start_with?(ESC_ESC, ESC_SUB, ESC_RES) 71 | node[1..-1] 72 | else 73 | @default_handler.from_rep(node[1], node[2..-1]) 74 | end 75 | if cache.cacheable?(node, as_map_key) 76 | cache.write(parsed) 77 | end 78 | parsed 79 | end 80 | when Array 81 | return node if node.empty? 82 | e0 = decode(node.shift, cache, false) 83 | if e0 == MAP_AS_ARRAY 84 | decode(Hash[*node], cache) 85 | elsif Tag === e0 86 | v = decode(node.shift, cache) 87 | if handler = @handlers[e0.value] 88 | handler.from_rep(v) 89 | else 90 | @default_handler.from_rep(e0.value,v) 91 | end 92 | else 93 | [e0] + node.map {|e| decode(e, cache, as_map_key)} 94 | end 95 | when Hash 96 | if node.size == 1 97 | k = decode(node.keys.first, cache, true) 98 | v = decode(node.values.first, cache, false) 99 | if Tag === k 100 | if handler = @handlers[k.value] 101 | handler.from_rep(v) 102 | else 103 | @default_handler.from_rep(k.value,v) 104 | end 105 | else 106 | {k => v} 107 | end 108 | else 109 | node.keys.each do |k| 110 | node.store(decode(k, cache, true), decode(node.delete(k), cache)) 111 | end 112 | node 113 | end 114 | else 115 | node 116 | end 117 | end 118 | 119 | def validate_handler(key, handler) 120 | raise ArgumentError.new(CAN_NOT_OVERRIDE_GROUND_TYPES_MESSAGE) if GROUND_TAGS.include?(key) 121 | end 122 | 123 | CAN_NOT_OVERRIDE_GROUND_TYPES_MESSAGE = <<-MSG 124 | You can not supply custom read handlers for ground types. 125 | MSG 126 | 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/transit/marshaler/base.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | module Transit 16 | # Transit::Writer marshals Ruby objects as transit values to an output stream. 17 | # @see https://github.com/cognitect/transit-format 18 | module Marshaler 19 | 20 | HANDLER_CACHE = {} 21 | VERBOSE_HANDLER_CACHE = {} 22 | MUTEX = Mutex.new 23 | 24 | # @api private 25 | # Included in VerboseJson subclasses. Defined here to make it 26 | # available in CRuby and JRuby environments. 27 | module VerboseHandlers 28 | def build_handlers(custom_handlers) 29 | if VERBOSE_HANDLER_CACHE.has_key?(custom_handlers) 30 | VERBOSE_HANDLER_CACHE[custom_handlers] 31 | else 32 | handlers = super(custom_handlers).reduce({}) do |h, (k,v)| 33 | if v.respond_to?(:verbose_handler) && vh = v.verbose_handler 34 | h.store(k, vh) 35 | else 36 | h.store(k, v) 37 | end 38 | h 39 | end 40 | VERBOSE_HANDLER_CACHE[custom_handlers] = handlers 41 | handlers 42 | end 43 | end 44 | end 45 | 46 | # @api private 47 | module Base 48 | def parse_options(opts) 49 | MUTEX.synchronize do 50 | @handlers = build_handlers(opts[:handlers]) 51 | end 52 | @handlers.values.each { |h| h.handlers=(@handlers) if h.respond_to?(:handlers=) } 53 | end 54 | 55 | def build_handlers(custom_handlers) 56 | if HANDLER_CACHE.has_key?(custom_handlers) 57 | HANDLER_CACHE[custom_handlers] 58 | else 59 | handlers = WriteHandlers::DEFAULT_WRITE_HANDLERS.dup 60 | handlers.merge!(custom_handlers) if custom_handlers 61 | HANDLER_CACHE[custom_handlers] = handlers 62 | handlers 63 | end 64 | end 65 | 66 | def find_handler(obj) 67 | obj.class.ancestors.each do |a| 68 | if handler = @handlers[a] 69 | return handler 70 | end 71 | end 72 | nil 73 | end 74 | 75 | def escape(s) 76 | if s.start_with?(SUB,ESC,RES) && s != "#{SUB} " 77 | "#{ESC}#{s}" 78 | else 79 | s 80 | end 81 | end 82 | 83 | def emit_nil(as_map_key, cache) 84 | as_map_key ? emit_string(ESC, "_", nil, true, cache) : emit_value(nil) 85 | end 86 | 87 | def emit_string(prefix, tag, value, as_map_key, cache) 88 | encoded = "#{prefix}#{tag}#{value}" 89 | if cache.cacheable?(encoded, as_map_key) 90 | emit_value(cache.write(encoded), as_map_key) 91 | else 92 | emit_value(encoded, as_map_key) 93 | end 94 | end 95 | 96 | def emit_boolean(handler, b, as_map_key, cache) 97 | as_map_key ? emit_string(ESC, "?", handler.string_rep(b), true, cache) : emit_value(b) 98 | end 99 | 100 | def emit_double(d, as_map_key, cache) 101 | as_map_key ? emit_string(ESC, "d", d, true, cache) : emit_value(d) 102 | end 103 | 104 | def emit_array(a, cache) 105 | emit_array_start(a.size) 106 | a.each {|e| marshal(e, false, cache)} 107 | emit_array_end 108 | end 109 | 110 | def emit_map(m, cache) 111 | emit_map_start(m.size) 112 | m.each do |k,v| 113 | marshal(k, true, cache) 114 | marshal(v, false, cache) 115 | end 116 | emit_map_end 117 | end 118 | 119 | def emit_tagged_value(tag, rep, cache) 120 | emit_array_start(2) 121 | emit_string(ESC, "#", tag, false, cache) 122 | marshal(rep, false, cache) 123 | emit_array_end 124 | end 125 | 126 | def emit_encoded(handler, tag, obj, as_map_key, cache) 127 | if tag.length == 1 128 | rep = handler.rep(obj) 129 | if String === rep 130 | emit_string(ESC, tag, rep, as_map_key, cache) 131 | elsif as_map_key || @prefer_strings 132 | if str_rep = handler.string_rep(obj) 133 | emit_string(ESC, tag, str_rep, as_map_key, cache) 134 | else 135 | raise "Cannot be encoded as String: " + {:tag => tag, :rep => rep, :obj => obj}.to_s 136 | end 137 | else 138 | emit_tagged_value(tag, handler.rep(obj), cache) 139 | end 140 | elsif as_map_key 141 | raise "Cannot be used as a map key: " + {:tag => tag, :rep => rep, :obj => obj}.to_s 142 | else 143 | emit_tagged_value(tag, handler.rep(obj), cache) 144 | end 145 | end 146 | 147 | def marshal(obj, as_map_key, cache) 148 | if handler = find_handler(obj) 149 | tag = handler.tag(obj) 150 | case tag 151 | when "_" 152 | emit_nil(as_map_key, cache) 153 | when "?" 154 | emit_boolean(handler, obj, as_map_key, cache) 155 | when "s" 156 | emit_string(nil, nil, escape(handler.rep(obj)), as_map_key, cache) 157 | when "i" 158 | emit_int(tag, handler.rep(obj), as_map_key, cache) 159 | when "d" 160 | emit_double(handler.rep(obj), as_map_key, cache) 161 | when "'" 162 | emit_tagged_value(tag, handler.rep(obj), cache) 163 | when "array" 164 | emit_array(handler.rep(obj), cache) 165 | when "map" 166 | emit_map(handler.rep(obj), cache) 167 | else 168 | emit_encoded(handler, tag, obj, as_map_key, cache) 169 | end 170 | else 171 | raise "Can not find a Write Handler for #{obj.inspect}." 172 | end 173 | end 174 | 175 | def marshal_top(obj, cache=RollingCache.new) 176 | if handler = find_handler(obj) 177 | if tag = handler.tag(obj) 178 | if tag.length == 1 179 | marshal(TaggedValue.new(QUOTE, obj), false, cache) 180 | else 181 | marshal(obj, false, cache) 182 | end 183 | flush 184 | else 185 | raise "Handler must provide a non-nil tag: #{handler.inspect}" 186 | end 187 | else 188 | raise "Can not find a Write Handler for #{obj.inspect}." 189 | end 190 | end 191 | end 192 | end 193 | end 194 | -------------------------------------------------------------------------------- /lib/transit/marshaler/cruby/json.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require 'oj' 16 | 17 | module Transit 18 | module Marshaler 19 | class BaseJson 20 | include Transit::Marshaler::Base 21 | 22 | def initialize(io, opts) 23 | @oj = Oj::StreamWriter.new(io,opts.delete(:oj_opts) || {}) 24 | @state = [] 25 | @max_int = JSON_MAX_INT 26 | @min_int = JSON_MIN_INT 27 | @prefer_strings = true 28 | parse_options(opts) 29 | end 30 | 31 | def emit_array_start(size) 32 | @state << :array 33 | @oj.push_array 34 | end 35 | 36 | def emit_array_end 37 | @state.pop 38 | @oj.pop 39 | end 40 | 41 | def emit_map_start(size) 42 | @state << :map 43 | @oj.push_object 44 | end 45 | 46 | def emit_map_end 47 | @state.pop 48 | @oj.pop 49 | end 50 | 51 | def emit_int(tag, i, as_map_key, cache) 52 | if as_map_key || i > @max_int || i < @min_int 53 | emit_string(ESC, tag, i, as_map_key, cache) 54 | else 55 | emit_value(i, as_map_key) 56 | end 57 | end 58 | 59 | def emit_value(obj, as_map_key=false) 60 | if @state.last == :array 61 | @oj.push_value(obj) 62 | else 63 | as_map_key ? @oj.push_key(obj) : @oj.push_value(obj) 64 | end 65 | end 66 | 67 | def flush 68 | # no-op 69 | end 70 | end 71 | 72 | # @api private 73 | class Json < BaseJson 74 | def emit_map(m, cache) 75 | emit_array_start(-1) 76 | emit_value("^ ", false) 77 | m.each do |k,v| 78 | marshal(k, true, cache) 79 | marshal(v, false, cache) 80 | end 81 | emit_array_end 82 | end 83 | end 84 | 85 | # @api private 86 | class VerboseJson < BaseJson 87 | include Transit::Marshaler::VerboseHandlers 88 | 89 | def emit_string(prefix, tag, value, as_map_key, cache) 90 | emit_value("#{prefix}#{tag}#{value}", as_map_key) 91 | end 92 | 93 | def emit_tagged_value(tag, rep, cache) 94 | emit_map_start(1) 95 | emit_string(ESC, "#", tag, true, cache) 96 | marshal(rep, false, cache) 97 | emit_map_end 98 | end 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/transit/marshaler/cruby/messagepack.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require 'msgpack' 16 | 17 | module Transit 18 | module Marshaler 19 | class MessagePack 20 | include Transit::Marshaler::Base 21 | 22 | def initialize(io, opts) 23 | @io = io 24 | @packer = ::MessagePack::Packer.new(io) 25 | @max_int = MAX_INT 26 | @min_int = MIN_INT 27 | @prefer_strings = false 28 | parse_options(opts) 29 | end 30 | 31 | def emit_array_start(size) 32 | @packer.write_array_header(size) 33 | end 34 | 35 | def emit_array_end 36 | # no-op 37 | end 38 | 39 | def emit_map_start(size) 40 | @packer.write_map_header(size) 41 | end 42 | 43 | def emit_map_end 44 | # no-op 45 | end 46 | 47 | def emit_int(tag, i, as_map_key, cache) 48 | if i > @max_int || i < @min_int 49 | emit_string(ESC, tag, i, as_map_key, cache) 50 | else 51 | emit_value(i, as_map_key) 52 | end 53 | end 54 | 55 | def emit_value(obj, as_map_key=:ignore) 56 | @packer.write(obj) 57 | end 58 | 59 | def flush 60 | @packer.flush 61 | @io.flush 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/transit/marshaler/jruby/json.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | module Transit 16 | module Marshaler 17 | 18 | # @api private 19 | module JsonBase 20 | def initialize(io, opts) 21 | parse_options(opts) 22 | end 23 | end 24 | 25 | # @api private 26 | class Json 27 | include Transit::Marshaler::Base 28 | include Transit::Marshaler::JsonBase 29 | end 30 | 31 | # @api private 32 | class VerboseJson 33 | include Transit::Marshaler::Base 34 | include Transit::Marshaler::JsonBase 35 | include Transit::Marshaler::VerboseHandlers 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/transit/marshaler/jruby/messagepack.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | module Transit 16 | module Marshaler 17 | class MessagePack 18 | include Transit::Marshaler::Base 19 | 20 | def initialize(io, opts) 21 | parse_options(opts) 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/transit/read_handlers.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | module Transit 16 | # @see Transit::WriteHandlers 17 | module ReadHandlers 18 | class Default 19 | def from_rep(tag,val) TaggedValue.new(tag, val) end 20 | end 21 | class NilHandler 22 | def from_rep(_) nil end 23 | end 24 | class KeywordHandler 25 | def from_rep(v) v.to_sym end 26 | end 27 | class BooleanHandler 28 | def from_rep(v) v == "t" end 29 | end 30 | class ByteArrayHandler 31 | def from_rep(v) ByteArray.from_base64(v) end 32 | end 33 | class FloatHandler 34 | def from_rep(v) Float(v) end 35 | end 36 | class IntegerHandler 37 | def from_rep(v) v.to_i end 38 | end 39 | class BigIntegerHandler 40 | def from_rep(v) v.to_i end 41 | end 42 | class BigDecimalHandler 43 | def from_rep(v) BigDecimal(v) end 44 | end 45 | class SpecialNumbersHandler 46 | def from_rep(v) 47 | case v 48 | when "NaN" then Float::NAN 49 | when "INF" then Float::INFINITY 50 | when "-INF" then -Float::INFINITY 51 | else raise ArgumentError.new("Don't know how to handle #{v.inspect} for the \"z\" tag") 52 | end 53 | end 54 | end 55 | class IdentityHandler 56 | def from_rep(v) v end 57 | end 58 | class SymbolHandler 59 | def from_rep(v) Transit::Symbol.new(v) end 60 | end 61 | class TimeStringHandler 62 | def from_rep(v) DateTime.iso8601(v) end 63 | end 64 | class TimeIntHandler 65 | def from_rep(v) DateTimeUtil.from_millis(v.to_i) end 66 | end 67 | class UuidHandler 68 | def from_rep(v) UUID.new(v) end 69 | end 70 | class UriHandler 71 | def from_rep(v) Addressable::URI.parse(v) end 72 | end 73 | class SetHandler 74 | def from_rep(v) Set.new(v) end 75 | end 76 | class LinkHandler 77 | def from_rep(v) Link.new(v) end 78 | end 79 | class CmapHandler 80 | def from_rep(v) Hash[*v] end 81 | end 82 | class RatioHandler 83 | def from_rep(v) Rational(v[0], v[1]) end 84 | end 85 | 86 | DEFAULT_READ_HANDLERS = { 87 | "_" => NilHandler.new, 88 | ":" => KeywordHandler.new, 89 | "?" => BooleanHandler.new, 90 | "b" => ByteArrayHandler.new, 91 | "d" => FloatHandler.new, 92 | "i" => IntegerHandler.new, 93 | "n" => BigIntegerHandler.new, 94 | "f" => BigDecimalHandler.new, 95 | "c" => IdentityHandler.new, 96 | "$" => SymbolHandler.new, 97 | "t" => TimeStringHandler.new, 98 | "m" => TimeIntHandler.new, 99 | "u" => UuidHandler.new, 100 | "r" => UriHandler.new, 101 | "'" => IdentityHandler.new, 102 | "z" => SpecialNumbersHandler.new, 103 | "set" => SetHandler.new, 104 | "link" => LinkHandler.new, 105 | "list" => IdentityHandler.new, 106 | "cmap" => CmapHandler.new, 107 | "ratio" => RatioHandler.new 108 | }.freeze 109 | 110 | DEFAULT_READ_HANDLER = Default.new 111 | 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /lib/transit/reader.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | module Transit 16 | # Transit::Reader converts incoming transit data into appropriate 17 | # values/objects in Ruby. 18 | # @see https://github.com/cognitect/transit-format 19 | class Reader 20 | extend Forwardable 21 | 22 | # @!method read 23 | # Reads transit values from an IO (file, stream, etc), and 24 | # converts each one to the appropriate Ruby object. 25 | # 26 | # With a block, yields each object to the block as it is processed. 27 | # 28 | # Without a block, returns a single object. 29 | # 30 | # @example 31 | # reader = Transit::Reader.new(:json, io) 32 | # reader.read {|obj| do_something_with(obj)} 33 | # 34 | # reader = Transit::Reader.new(:json, io) 35 | # obj = reader.read 36 | def_delegators :@reader, :read 37 | 38 | # @param [Symbol] format required any of :msgpack, :json, :json_verbose 39 | # @param [IO] io required 40 | # @param [Hash] opts optional 41 | # Creates a new Reader configured to read from io, 42 | # expecting format (:json, :msgpack). 43 | # 44 | # Use opts to register custom read handlers, associating each one 45 | # with its tag. 46 | # 47 | # @example 48 | # 49 | # json_reader = Transit::Reader.new(:json, io) 50 | # # ^^ reads both :json and :json_verbose formats ^^ 51 | # msgpack_writer = Transit::Reader.new(:msgpack, io) 52 | # writer_with_custom_handlers = Transit::Reader.new(:json, io, 53 | # :handlers => {"point" => PointReadHandler}) 54 | # 55 | # @see Transit::ReadHandlers 56 | def initialize(format, io, opts={}) 57 | @reader = case format 58 | when :json, :json_verbose 59 | Unmarshaler::Json.new(io, opts) 60 | else 61 | Unmarshaler::MessagePack.new(io, opts) 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/transit/rolling_cache.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | module Transit 16 | # @api private 17 | class RollingCache 18 | extend Forwardable 19 | 20 | def_delegators "@key_to_value", :has_key?, :size 21 | 22 | FIRST_ORD = 48 23 | LAST_ORD = 91 24 | CACHE_CODE_DIGITS = 44; 25 | CACHE_SIZE = CACHE_CODE_DIGITS * CACHE_CODE_DIGITS; 26 | MIN_SIZE_CACHEABLE = 4 27 | 28 | def initialize 29 | clear 30 | end 31 | 32 | def read(key) 33 | @key_to_value[key] 34 | end 35 | 36 | def write(val) 37 | @value_to_key[val] || begin 38 | clear if @key_to_value.size >= CACHE_SIZE 39 | key = next_key(@key_to_value.size) 40 | @value_to_key[val] = key 41 | @key_to_value[key] = val 42 | end 43 | end 44 | 45 | def cache_key?(str, _=false) 46 | str[0] == SUB && str != MAP_AS_ARRAY 47 | end 48 | 49 | def cacheable?(str, as_map_key=false) 50 | str.size >= MIN_SIZE_CACHEABLE && (as_map_key || str.start_with?("~#","~$","~:")) 51 | end 52 | 53 | private 54 | 55 | def clear 56 | @key_to_value = {} 57 | @value_to_key = {} 58 | end 59 | 60 | def next_key(i) 61 | hi = i / CACHE_CODE_DIGITS; 62 | lo = i % CACHE_CODE_DIGITS; 63 | if hi == 0 64 | "^#{(lo+FIRST_ORD).chr}" 65 | else 66 | "^#{(hi+FIRST_ORD).chr}#{(lo+FIRST_ORD).chr}" 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/transit/transit_types.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | module Transit 16 | class Wrapper 17 | extend Forwardable 18 | 19 | def_delegators :@value, :hash, :to_sym, :to_s 20 | 21 | attr_reader :value 22 | 23 | def initialize(value) 24 | @value = value 25 | end 26 | 27 | def ==(other) 28 | other.is_a?(self.class) && @value == other.value 29 | end 30 | alias eql? == 31 | 32 | def inspect 33 | "<#{self.class} \"#{to_s}\">" 34 | end 35 | end 36 | 37 | # Represents a transit symbol extension type. 38 | # @see https://github.com/cognitect/transit-format 39 | class Symbol < Wrapper 40 | def initialize(sym) 41 | super sym.to_sym 42 | end 43 | 44 | def namespace 45 | @namespace ||= parsed[-2] 46 | end 47 | 48 | def name 49 | @name ||= parsed[-1] || "/" 50 | end 51 | 52 | private 53 | 54 | def parsed 55 | @parsed ||= @value.to_s.split("/") 56 | end 57 | end 58 | 59 | # Represents a transit byte array extension type. 60 | # @see https://github.com/cognitect/transit-format 61 | class ByteArray < Wrapper 62 | def self.from_base64(data) 63 | new(Base64.decode64(data)) 64 | end 65 | 66 | def to_base64 67 | Base64.encode64(@value) 68 | end 69 | 70 | # For human-readable display only. Use value() for programmatic 71 | # consumption of the decoded value. 72 | # 73 | # Forces the platform's default external encoding, which is 74 | # potentially lossy, but also guarantees that something will be 75 | # printed instead of raising an error when there is no encoding 76 | # information provided. 77 | def to_s 78 | @value.dup.force_encoding(Encoding.default_external) 79 | end 80 | end 81 | 82 | # Represents a transit UUID extension type. 83 | # @see https://github.com/cognitect/transit-format 84 | class UUID 85 | def self.random 86 | new 87 | end 88 | 89 | def initialize(uuid_or_most_significant_bits=nil,least_significant_bits=nil) 90 | case uuid_or_most_significant_bits 91 | when String 92 | @string_rep = uuid_or_most_significant_bits 93 | when Array 94 | @numeric_rep = uuid_or_most_significant_bits.map {|n| twos_complement(n)} 95 | when Numeric 96 | @numeric_rep = [twos_complement(uuid_or_most_significant_bits), twos_complement(least_significant_bits)] 97 | when nil 98 | @string_rep = SecureRandom.uuid 99 | else 100 | raise "Can't build UUID from #{uuid_or_most_significant_bits.inspect}" 101 | end 102 | end 103 | 104 | def to_s 105 | @string_rep ||= numbers_to_string 106 | end 107 | 108 | def most_significant_bits 109 | @most_significant_bits ||= numeric_rep[0] 110 | end 111 | 112 | def least_significant_bits 113 | @least_significant_bits ||= numeric_rep[1] 114 | end 115 | 116 | def inspect 117 | @inspect ||= "<#{self.class} \"#{to_s}\">" 118 | end 119 | 120 | def ==(other) 121 | return false unless other.is_a?(self.class) 122 | if @numeric_rep 123 | other.most_significant_bits == most_significant_bits && 124 | other.least_significant_bits == least_significant_bits 125 | else 126 | other.to_s == @string_rep 127 | end 128 | end 129 | alias eql? == 130 | 131 | def hash 132 | most_significant_bits.hash + least_significant_bits.hash 133 | end 134 | 135 | private 136 | 137 | def numeric_rep 138 | @numeric_rep ||= string_to_numbers 139 | end 140 | 141 | def numbers_to_string 142 | most_significant_bits = @numeric_rep[0] 143 | least_significant_bits = @numeric_rep[1] 144 | digits(most_significant_bits >> 32, 8) + "-" + 145 | digits(most_significant_bits >> 16, 4) + "-" + 146 | digits(most_significant_bits, 4) + "-" + 147 | digits(least_significant_bits >> 48, 4) + "-" + 148 | digits(least_significant_bits, 12) 149 | end 150 | 151 | def string_to_numbers 152 | str = @string_rep.delete("-") 153 | [twos_complement(str[ 0..15].hex), twos_complement(str[16..31].hex)] 154 | end 155 | 156 | def digits(val, digits) 157 | hi = 1 << (digits*4) 158 | (hi | (val & (hi - 1))).to_s(16)[1..-1] 159 | end 160 | 161 | def twos_complement(integer_value, num_of_bits=64) 162 | max_signed = 2**(num_of_bits-1) 163 | max_unsigned = 2**num_of_bits 164 | (integer_value >= max_signed) ? integer_value - max_unsigned : integer_value 165 | end 166 | end 167 | 168 | # Represents a transit hypermedia link extension type. 169 | # @see https://github.com/cognitect/transit-format 170 | # @see http://amundsen.com/media-types/collection/format/#arrays-links 171 | class Link 172 | KEYS = ["href", "rel", "name", "render", "prompt"] 173 | RENDER_VALUES = ["link", "image"] 174 | 175 | # @overload Link.new(hash) 176 | # @param [Hash] hash 177 | # Valid keys are: 178 | # "href" required, String or Addressable::URI 179 | # "rel" required, String 180 | # "name" optional, String 181 | # "render" optional, String (only "link" or "image") 182 | # "prompt" optional, String 183 | # @overload Link.new(href, rel, name, render, prompt) 184 | # @param [String, Addressable::URI] href required 185 | # @param [String] rel required 186 | # @param [String] name optional 187 | # @param [String] render optional (only "link" or "image") 188 | # @param [String] prompt optional 189 | def initialize(*args) 190 | @values = if args[0].is_a?(Hash) 191 | reconcile_values(args[0]) 192 | elsif args.length >= 2 && (args[0].is_a?(Addressable::URI) || args[0].is_a?(String)) 193 | reconcile_values(Hash[KEYS.zip(args)]) 194 | else 195 | raise ArgumentError, "The first argument to Link.new can be a URI, String or a Hash. When the first argument is a URI or String, the second argument, rel, must present." 196 | end 197 | end 198 | 199 | def href; @href ||= @values["href"] end 200 | def rel; @rel ||= @values["rel"] end 201 | def name; @name ||= @values["name"] end 202 | def render; @render ||= @values["render"] end 203 | def prompt; @prompt ||= @values["prompt"] end 204 | 205 | def to_h 206 | @values 207 | end 208 | 209 | def ==(other) 210 | other.is_a?(Link) && other.to_h == to_h 211 | end 212 | alias eql? == 213 | 214 | def hash 215 | @values.hash 216 | end 217 | 218 | private 219 | 220 | def reconcile_values(map) 221 | map.dup.tap do |m| 222 | m["href"] = Addressable::URI.parse(m["href"]) if m["href"].is_a?(String) 223 | if m["render"] 224 | render = m["render"].downcase 225 | if RENDER_VALUES.include?(render) 226 | m["render"] = render 227 | else 228 | raise ArgumentError, "render must be either #{RENDER_VALUES[0]} or #{RENDER_VALUES[1]}" 229 | end 230 | end 231 | end.freeze 232 | end 233 | end 234 | 235 | # Represents a transit tag and value. Returned by default when a 236 | # reader encounters a tag for which there is no registered 237 | # handler. Can also be used in a custom write handler to force 238 | # representation to use a transit ground type using a rep for which 239 | # there is no registered handler (e.g., an iterable for the 240 | # representation of an array). 241 | # @see https://github.com/cognitect/transit-format 242 | class TaggedValue 243 | attr_reader :tag, :rep 244 | def initialize(tag, rep) 245 | @tag = tag 246 | @rep = rep 247 | end 248 | 249 | def ==(other) 250 | other.is_a?(self.class) && other.tag == @tag && other.rep == @rep 251 | end 252 | alias eql? == 253 | 254 | def hash 255 | @tag.hash + @rep.hash 256 | end 257 | end 258 | end 259 | -------------------------------------------------------------------------------- /lib/transit/unmarshaler/cruby/json.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require 'oj' 16 | 17 | module Transit 18 | module Unmarshaler 19 | # Transit::Reader::MessagePackUnmarshaler is responsible to read data on CRuby 20 | # @see https://github.com/cognitect/transit-format 21 | 22 | # @api private 23 | class Json 24 | class ParseHandler 25 | def each(&block) @yield_v = block end 26 | def add_value(v) @yield_v[v] if @yield_v end 27 | 28 | def hash_start() {} end 29 | def hash_set(h,k,v) h.store(k,v) end 30 | def array_start() [] end 31 | def array_append(a,v) a << v end 32 | 33 | def error(message, line, column) 34 | raise Exception.new(message, line, column) 35 | end 36 | end 37 | 38 | def initialize(io, opts) 39 | @io = io 40 | @decoder = Transit::Decoder.new(opts) 41 | @parse_handler = ParseHandler.new 42 | end 43 | 44 | # @see Reader#read 45 | def read 46 | if block_given? 47 | @parse_handler.each {|v| yield @decoder.decode(v)} 48 | else 49 | @parse_handler.each {|v| return @decoder.decode(v)} 50 | end 51 | Oj.sc_parse(@parse_handler, @io) {|_stack|} 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/transit/unmarshaler/cruby/messagepack.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require 'msgpack' 16 | 17 | module Transit 18 | module Unmarshaler 19 | # Transit::Reader::MessagePackUnmarshaler is responsible to read data on CRuby 20 | # @see https://github.com/cognitect/transit-format 21 | 22 | # @api private 23 | class MessagePack 24 | def initialize(io, opts) 25 | @decoder = Transit::Decoder.new(opts) 26 | @unpacker = ::MessagePack::Unpacker.new(io) 27 | end 28 | 29 | # @see Reader#read 30 | def read 31 | if block_given? 32 | @unpacker.each {|v| yield @decoder.decode(v)} 33 | else 34 | @decoder.decode(@unpacker.read) 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/transit/write_handlers.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | module Transit 16 | # WriteHandlers convert instances of Ruby types to their 17 | # corresponding Transit semantic types, and ReadHandlers read 18 | # convert transit values back into instances of Ruby 19 | # types. transit-ruby ships with default sets of WriteHandlers for 20 | # each of the Ruby types that map naturally to transit types, and 21 | # ReadHandlers for each transit type. For the common case, the 22 | # built-in handlers will suffice, but you can add your own extension 23 | # types and/or override the built-in handlers. 24 | # 25 | # ## Custom handlers 26 | # 27 | # For example, Ruby has Date, Time, and DateTime, each with their 28 | # own semantics. Transit has an instance type, which does not 29 | # differentiate between Date and Time, so transit-ruby writes Dates, 30 | # Times, and DateTimes as transit instances, and reads transit 31 | # instances as DateTimes. If your application cares that Dates are 32 | # different from DateTimes, you could register custom write and read 33 | # handlers, overriding the built-in DateHandler and adding a new 34 | # DateReadHandler. 35 | # 36 | # ### Write handlers 37 | # 38 | # Write handlers are required to expose tag, rep, and string_rep methods: 39 | # 40 | # ```ruby 41 | # class DateWriteHandler 42 | # def tag(_) "D" end 43 | # def rep(o) o.to_s end 44 | # def string_rep(o) o.to_s end 45 | # def verbose_handler(_) nil end # optional - see Verbose write handlers, below 46 | # end 47 | # ``` 48 | # 49 | # tag returns the tag used to identify the transit type 50 | # (built-in or extension). It accepts the object being written, 51 | # which allows the handler to return different tags for different 52 | # semantics, e.g. the built-in IntHandler, which returns the tag "i" 53 | # for numbers that fit within a 64-bit signed integer and "n" for 54 | # anything outside that range. 55 | # 56 | # rep accepts the object being written and returns its wire 57 | # representation. This can be a scalar value (identified by a 58 | # one-character tag) or a map (Ruby Hash) or an array (identified by 59 | # a multi-character tag). 60 | # 61 | # string_rep accepts the object being written and returns a 62 | # string representation. Used when the object is a key in a map. 63 | # 64 | # ### Read handlers 65 | # 66 | # Read handlers are required to expose a single from_rep method: 67 | # 68 | # ```ruby 69 | # class DateReadHandler 70 | # def from_rep(rep) 71 | # Date.parse(rep) 72 | # end 73 | # end 74 | # ``` 75 | # 76 | # from_rep accepts the wire representation (without the tag), and 77 | # uses it to build an appropriate Ruby object. 78 | # 79 | # ### Usage 80 | # 81 | # ```ruby 82 | # io = StringIO.new('','w+') 83 | # writer = Transit::Writer.new(:json, io, :handlers => {Date => DateWriteHandler.new}) 84 | # writer.write(Date.new(2014,7,22)) 85 | # io.string 86 | # # => "[\"~#'\",\"~D2014-07-22\"]\n" 87 | # 88 | # reader = Transit::Reader.new(:json, StringIO.new(io.string), :handlers => {"D" => DateReadHandler.new}) 89 | # reader.read 90 | # # => # 91 | # ``` 92 | # 93 | # ## Custom types and representations 94 | # 95 | # Transit supports scalar and structured representations. The Date 96 | # example, above, demonstrates a String representation (scalar) of a 97 | # Date. This works well because it is a natural representation, but 98 | # it might not be a good solution for a more complex type, e.g. a 99 | # Point. While you _could_ represent a Point as a String, e.g. 100 | # ("x:37,y:42"), it would be more efficient and arguably 101 | # more natural to represent it as an array of Integers: 102 | # 103 | # ```ruby 104 | # require 'ostruct' 105 | # Point = Struct.new(:x,:y) do 106 | # def to_a; [x,y] end 107 | # end 108 | # 109 | # class PointWriteHandler 110 | # def tag(_) "point" end 111 | # def rep(o) o.to_a end 112 | # def string_rep(_) nil end 113 | # end 114 | # 115 | # class PointReadHandler 116 | # def from_rep(rep) 117 | # Point.new(*rep) 118 | # end 119 | # end 120 | # 121 | # io = StringIO.new('','w+') 122 | # writer = Transit::Writer.new(:json_verbose, io, :handlers => {Point => PointWriteHandler.new}) 123 | # writer.write(Point.new(37,42)) 124 | # io.string 125 | # # => "{\"~#point\":[37,42]}\n" 126 | # 127 | # reader = Transit::Reader.new(:json, StringIO.new(io.string), 128 | # :handlers => {"point" => PointReadHandler.new}) 129 | # reader.read 130 | # # => # 131 | # ``` 132 | # 133 | # Note that Date used a one-character tag, "D", whereas Point uses a 134 | # multi-character tag, "point". Transit expects one-character tags 135 | # to have scalar representations (string, integer, float, boolean, 136 | # etc) and multi-character tags to have structural representations, 137 | # i.e. maps (Ruby Hashes) or arrays. 138 | # 139 | # ## Verbose write handlers 140 | # 141 | # Write handlers can, optionally, support the JSON-VERBOSE format by 142 | # providing a verbose write handler. Transit uses this for instances 143 | # (Ruby Dates, Times, DateTimes) to differentiate between the more 144 | # efficient format using an int representing milliseconds since 1970 145 | # in JSON mode from the more readable format using a String in 146 | # JSON-VERBOSE mode. 147 | # 148 | # ```ruby 149 | # inst = DateTime.new(1985,04,12,23,20,50,"0") 150 | # 151 | # io = StringIO.new('','w+') 152 | # writer = Transit::Writer.new(:json, io) 153 | # writer.write(inst) 154 | # io.string 155 | # #=> "[\"~#'\",\"~m482196050000\"]\n" 156 | # 157 | # io = StringIO.new('','w+') 158 | # writer = Transit::Writer.new(:json_verbose, io) 159 | # writer.write(inst) 160 | # io.string 161 | # #=> "{\"~#'\":\"~t1985-04-12T23:20:50.000Z\"}\n" 162 | # ``` 163 | # 164 | # When you want a more human-readable format for your own custom 165 | # types in JSON-VERBOSE mode, create a second write handler and add 166 | # a verbose_handler method to the first handler that 167 | # returns an instance of the verbose handler: 168 | # 169 | # ```ruby 170 | # Element = Struct.new(:id, :name) 171 | # 172 | # class ElementWriteHandler 173 | # def tag(_) "el" end 174 | # def rep(v) v.id end 175 | # def string_rep(v) v.name end 176 | # def verbose_handler() ElementVerboseWriteHandler.new end 177 | # end 178 | # 179 | # class ElementVerboseWriteHandler < ElementWriteHandler 180 | # def rep(v) v.name end 181 | # end 182 | # 183 | # write_handlers = {Element => ElementWriteHandler.new} 184 | # 185 | # e = Element.new(3, "Lithium") 186 | # 187 | # io = StringIO.new('','w+') 188 | # writer = Transit::Writer.new(:json, io, :handlers => write_handlers) 189 | # writer.write(e) 190 | # io.string 191 | # # => "[\"~#el\",3]\n" 192 | # 193 | # io = StringIO.new('','w+') 194 | # writer = Transit::Writer.new(:json_verbose, io, :handlers => write_handlers) 195 | # writer.write(e) 196 | # io.string 197 | # # => "{\"~#el\":\"Lithium\"}\n" 198 | # ``` 199 | # 200 | # Note that you register the same handler collection; transit-ruby takes care of 201 | # asking for the verbose_handler for the :json_verbose format. 202 | module WriteHandlers 203 | class NilHandler 204 | def tag(_) "_" end 205 | def rep(_) nil end 206 | def string_rep(n) nil end 207 | end 208 | 209 | class KeywordHandler 210 | def tag(_) ":" end 211 | def rep(s) s.to_s end 212 | def string_rep(s) rep(s) end 213 | end 214 | 215 | class StringHandler 216 | def tag(_) "s" end 217 | def rep(s) s end 218 | def string_rep(s) s end 219 | end 220 | 221 | class TrueHandler 222 | def tag(_) "?" end 223 | def rep(_) true end 224 | def string_rep(_) "t" end 225 | end 226 | 227 | class FalseHandler 228 | def tag(_) "?" end 229 | def rep(_) false end 230 | def string_rep(_) "f" end 231 | end 232 | 233 | class IntHandler 234 | def tag(i) i > MAX_INT || i < MIN_INT ? "n" : "i" end 235 | def rep(i) i > MAX_INT || i < MIN_INT ? i.to_s : i end 236 | def string_rep(i) i.to_s end 237 | end 238 | 239 | class FloatHandler 240 | def tag(f) 241 | return "z" if f.nan? 242 | case f 243 | when Float::INFINITY, -Float::INFINITY 244 | "z" 245 | else 246 | "d" 247 | end 248 | end 249 | 250 | def rep(f) 251 | return "NaN" if f.nan? 252 | case f 253 | when Float::INFINITY then "INF" 254 | when -Float::INFINITY then "-INF" 255 | else f 256 | end 257 | end 258 | 259 | def string_rep(f) rep(f).to_s end 260 | end 261 | 262 | class BigDecimalHandler 263 | def tag(_) "f" end 264 | def rep(f) f.to_s("f") end 265 | def string_rep(f) rep(f) end 266 | end 267 | 268 | class RationalHandler 269 | def tag(_) "ratio" end 270 | def rep(r) [r.numerator, r.denominator] end 271 | def string_rep(_) nil end 272 | end 273 | 274 | # TimeHandler, DateTimeHandler, and DateHandler all have different 275 | # implementations of string_rep. Here is the rationale: 276 | # 277 | # For all three, want to write out the same format 278 | # e.g. 2014-04-18T18:51:29.478Z, and we want the milliseconds to truncate 279 | # rather than round, eg 29.4786 seconds should be 29.478, not 29.479. 280 | # - "sss is the number of complete milliseconds since the start of the 281 | # second as three decimal digits." 282 | # - http://www.ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 283 | # 284 | # Some data points (see benchmarks/encoding_time.rb) 285 | # - Time and DateTime each offer iso8601 methods, but strftime is faster. 286 | # - DateTime's strftime (and iso8601) round millis 287 | # - Time's strftime (and iso8601) truncate millis 288 | # - we don't care about truncate v round for dates (which have 000 ms) 289 | # - date.to_datetime.strftime(...) is considerably faster than date.to_time.strftime(...) 290 | class TimeHandler 291 | def tag(_) "m" end 292 | def rep(t) DateTimeUtil.to_millis(t) end 293 | def string_rep(t) rep(t).to_s end 294 | def verbose_handler() VerboseTimeHandler.new end 295 | end 296 | 297 | class DateTimeHandler < TimeHandler 298 | def verbose_handler() VerboseDateTimeHandler.new end 299 | end 300 | 301 | class DateHandler < TimeHandler 302 | def verbose_handler() VerboseDateHandler.new end 303 | end 304 | 305 | class VerboseTimeHandler 306 | def tag(_) "t" end 307 | def rep(t) 308 | # .getutc because we don't want to modify t 309 | t.getutc.strftime(Transit::TIME_FORMAT) 310 | end 311 | def string_rep(t) rep(t) end 312 | end 313 | 314 | class VerboseDateTimeHandler < VerboseTimeHandler 315 | def rep(t) 316 | # .utc because to_time already creates a new object 317 | t.to_time.utc.strftime(Transit::TIME_FORMAT) 318 | end 319 | end 320 | 321 | class VerboseDateHandler < VerboseTimeHandler 322 | def rep(d) 323 | # to_datetime because DateTime's strftime is faster 324 | # thank Time's, and millis are 000 so it doesn't matter 325 | # if we truncate or round. 326 | d.to_datetime.strftime(Transit::TIME_FORMAT) 327 | end 328 | end 329 | 330 | class UuidHandler 331 | def tag(_) "u" end 332 | def rep(u) [u.most_significant_bits, u.least_significant_bits] end 333 | def string_rep(u) u.to_s end 334 | end 335 | 336 | class LinkHandler 337 | def tag(_) "link" end 338 | def rep(l) l.to_h end 339 | def string_rep(_) nil end 340 | end 341 | 342 | class UriHandler 343 | def tag(_) "r" end 344 | def rep(u) u.to_s end 345 | def string_rep(u) rep(u) end 346 | end 347 | 348 | class AddressableUriHandler 349 | def tag(_) "r" end 350 | def rep(u) u.to_s end 351 | def string_rep(u) rep(u) end 352 | end 353 | 354 | class ByteArrayHandler 355 | def tag(_) "b" end 356 | if Transit::jruby? 357 | def rep(b) 358 | b.value.to_java_bytes 359 | end 360 | else 361 | def rep(b) 362 | b.to_base64 363 | end 364 | end 365 | def string_rep(b) rep(b) end 366 | end 367 | 368 | class TransitSymbolHandler 369 | def tag(_) "$" end 370 | def rep(s) s.to_s end 371 | def string_rep(s) rep(s) end 372 | end 373 | 374 | class ArrayHandler 375 | def tag(_) "array" end 376 | def rep(a) a end 377 | def string_rep(_) nil end 378 | end 379 | 380 | class MapHandler 381 | def handlers=(handlers) 382 | @handlers = handlers 383 | end 384 | 385 | def stringable_keys?(m) 386 | m.keys.all? {|k| (@handlers[k.class].tag(k).length == 1) } 387 | end 388 | 389 | def tag(m) 390 | stringable_keys?(m) ? "map" : "cmap" 391 | end 392 | 393 | def rep(m) 394 | stringable_keys?(m) ? m : m.reduce([]) {|a, kv| a.concat(kv)} 395 | end 396 | 397 | def string_rep(_) nil end 398 | end 399 | 400 | class SetHandler 401 | def tag(_) "set" end 402 | def rep(s) s.to_a end 403 | def string_rep(_) nil end 404 | end 405 | 406 | class TaggedValueHandler 407 | def tag(tv) tv.tag end 408 | def rep(tv) tv.rep end 409 | def string_rep(_) nil end 410 | end 411 | 412 | # Ruby >= 2.4 uses Integer for any integer 413 | # Ruby < 2.4 uses Fixnum and Bignum, which are subs of Integer 414 | # See: https://bugs.ruby-lang.org/issues/12005 415 | DEFAULT_INTEGER_HANDLERS = 416 | if 1.class == Integer 417 | {Integer => IntHandler.new} 418 | else 419 | {(Module.const_get "Fixnum") => IntHandler.new, 420 | (Module.const_get "Bignum") => IntHandler.new} 421 | end 422 | 423 | DEFAULT_WRITE_HANDLERS = DEFAULT_INTEGER_HANDLERS.merge({ 424 | NilClass => NilHandler.new, 425 | ::Symbol => KeywordHandler.new, 426 | String => StringHandler.new, 427 | TrueClass => TrueHandler.new, 428 | FalseClass => FalseHandler.new, 429 | Float => FloatHandler.new, 430 | BigDecimal => BigDecimalHandler.new, 431 | Rational => RationalHandler.new, 432 | Time => TimeHandler.new, 433 | DateTime => DateTimeHandler.new, 434 | Date => DateHandler.new, 435 | UUID => UuidHandler.new, 436 | Link => LinkHandler.new, 437 | URI => UriHandler.new, 438 | Addressable::URI => AddressableUriHandler.new, 439 | ByteArray => ByteArrayHandler.new, 440 | Transit::Symbol => TransitSymbolHandler.new, 441 | Array => ArrayHandler.new, 442 | Hash => MapHandler.new, 443 | Set => SetHandler.new, 444 | TaggedValue => TaggedValueHandler.new 445 | }).freeze 446 | end 447 | end 448 | -------------------------------------------------------------------------------- /lib/transit/writer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | module Transit 16 | # Transit::Writer marshals Ruby objects as transit values to an output stream. 17 | # @see https://github.com/cognitect/transit-format 18 | class Writer 19 | 20 | # @param [Symbol] format required :json, :json_verbose, or :msgpack 21 | # @param [IO] io required 22 | # @param [Hash] opts optional 23 | # 24 | # Creates a new Writer configured to write to io in 25 | # format (:json, :json_verbose, 26 | # :msgpack). 27 | # 28 | # Use opts to register custom write handlers, associating each one 29 | # with its type. 30 | # 31 | # @example 32 | # json_writer = Transit::Writer.new(:json, io) 33 | # json_verbose_writer = Transit::Writer.new(:json_verbose, io) 34 | # msgpack_writer = Transit::Writer.new(:msgpack, io) 35 | # writer_with_custom_handlers = Transit::Writer.new(:json, io, 36 | # :handlers => {Point => PointWriteHandler}) 37 | # 38 | # @see Transit::WriteHandlers 39 | def initialize(format, io, opts={}) 40 | @marshaler = case format 41 | when :json 42 | Marshaler::Json.new(io, {:handlers => {}, 43 | :oj_opts => {:indent => -1}}.merge(opts)) 44 | when :json_verbose 45 | Marshaler::VerboseJson.new(io, {:handlers => {}}.merge(opts)) 46 | else 47 | Marshaler::MessagePack.new(io, {:handlers => {}}.merge(opts)) 48 | end 49 | end 50 | 51 | # Converts a Ruby object to a transit value and writes it to this 52 | # Writer's output stream. 53 | # 54 | # @param obj the value to write 55 | # @example 56 | # writer = Transit::Writer.new(:json, io) 57 | # writer.write(Date.new(2014,7,22)) 58 | if Transit::jruby? 59 | def write(obj) 60 | @marshaler.write(obj) 61 | end 62 | else 63 | def write(obj) 64 | @marshaler.marshal_top(obj) 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | unless File.exist?('../transit-format/examples/0.8/simple') 16 | puts <<-MSG 17 | Before you can run the rspec examples, you need to install the 18 | the https://github.com/cognitect/transit-format repo in a sibling 19 | directory, e.g. 20 | 21 | cd .. 22 | git clone https://github.com/cognitect/transit-format 23 | 24 | That repo contains exemplars used by transit-ruby's rspec examples 25 | (in ../transit-format/examples/0.8/simple), so then you can: 26 | 27 | cd transit-ruby 28 | rspec 29 | 30 | MSG 31 | exit 32 | end 33 | 34 | require 'json' 35 | require 'rspec' 36 | require 'date' # because wrong expects it but doesn't load it :/ 37 | require 'wrong/adapters/rspec' 38 | require 'transit' 39 | require 'spec_helper-local' if File.exist?(File.expand_path('../spec_helper-local.rb', __FILE__)) 40 | 41 | RSpec.configure do |c| 42 | c.alias_example_to :fit, :focus => true 43 | c.filter_run_including :focus => true, :focused => true 44 | c.run_all_when_everything_filtered = true 45 | c.mock_with :nothing 46 | end 47 | 48 | ALPHA_NUM = 'abcdefghijklmnopqrstuvwxyzABCDESFHIJKLMNOPQRSTUVWXYZ_0123456789' 49 | 50 | def random_alphanum 51 | ALPHA_NUM[rand(ALPHA_NUM.size)] 52 | end 53 | 54 | def random_string(max_length=10) 55 | l = rand(max_length) + 1 56 | (Array.new(l).map {|x| random_alphanum}).join 57 | end 58 | 59 | def random_strings(max_length=10, n=100) 60 | Array.new(n).map {random_string(max_length)} 61 | end 62 | 63 | def random_symbol(max_length=10) 64 | random_string(max_length).to_sym 65 | end 66 | 67 | def ints_centered_on(m, n=5) 68 | ((m-n)..(m+n)).to_a 69 | end 70 | 71 | def array_of_symbols(m, n=m) 72 | seeds = (0...m).map {|i| ("key%04d" % i).to_sym} 73 | seeds.cycle.take(n) 74 | end 75 | 76 | def hash_of_size(n) 77 | Hash[array_of_symbols(n).zip((0..n).to_a)] 78 | end 79 | 80 | Person = Struct.new("Person", :first_name, :last_name, :birthdate) 81 | 82 | class PersonHandler 83 | def tag(_) "person"; end 84 | def rep(p) {:first_name => p.first_name, :last_name => p.last_name, :birthdate => p.birthdate} end 85 | def string_rep(p) nil end 86 | end 87 | -------------------------------------------------------------------------------- /spec/transit/date_time_util_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require 'spec_helper' 16 | 17 | module Transit 18 | describe DateTimeUtil do 19 | describe "[to|from]_millis" do 20 | it "round trips properly" do 21 | 100.times do 22 | n = DateTime.now 23 | a = Transit::DateTimeUtil.to_millis(n) 24 | b = Transit::DateTimeUtil.from_millis(a) 25 | c = Transit::DateTimeUtil.to_millis(b) 26 | d = Transit::DateTimeUtil.from_millis(c) 27 | assert { a == c } 28 | assert { b == d } 29 | sleep(0.0001) 30 | end 31 | end 32 | end 33 | 34 | describe "to_millis" do 35 | let(:time) { Time.at(1388631845) + 0.678 } 36 | 37 | it "supports DateTime" do 38 | assert { Transit::DateTimeUtil.to_millis(time.to_datetime) == 1388631845678 } 39 | end 40 | 41 | it "supports Time" do 42 | assert { Transit::DateTimeUtil.to_millis(time) == 1388631845678 } 43 | end 44 | 45 | it "supports Date" do 46 | assert { Transit::DateTimeUtil.to_millis(Date.new(2014,1,2)) == 1388620800000 } 47 | end 48 | end 49 | 50 | describe "from_millis" do 51 | it "converts to utc" do 52 | t = DateTime.now 53 | m = Transit::DateTimeUtil.to_millis(t) 54 | f = Transit::DateTimeUtil.from_millis(m) 55 | assert { f.zone == '+00:00' } 56 | end 57 | 58 | it "handles millis properly" do 59 | assert { Transit::DateTimeUtil.from_millis(1388631845674) == DateTime.new(2014,1,2,3,4,5.674).new_offset(0) } 60 | assert { Transit::DateTimeUtil.from_millis(1388631845675) == DateTime.new(2014,1,2,3,4,5.675).new_offset(0) } 61 | assert { Transit::DateTimeUtil.from_millis(1388631845676) == DateTime.new(2014,1,2,3,4,5.676).new_offset(0) } 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/transit/decoder_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require 'spec_helper' 16 | 17 | module Transit 18 | describe Decoder do 19 | def decode(o) 20 | Decoder.new.decode(o) 21 | end 22 | 23 | describe "caching" do 24 | it "decodes cacheable map keys" do 25 | assert { decode([{"this" => "a"},{"^0" => "b"}]) == [{"this" => "a"},{"this" => "b"}] } 26 | end 27 | 28 | it "does not cache non-map-keys" do 29 | assert { decode([{"a" => "~^!"},{"b" => "~^?"}]) == [{"a" => "^!"},{"b" => "^?"}] } 30 | end 31 | end 32 | 33 | describe "formats" do 34 | describe "JSON_M" do 35 | it "converts an array starting with '^ ' to a map" do 36 | assert { decode(["^ ", :a, :b, :c, :d]) == {:a => :b, :c => :d} } 37 | end 38 | end 39 | end 40 | 41 | describe "unrecognized input" do 42 | it "decodes an unrecognized string to a TaggedValue" do 43 | assert { decode("~Unrecognized") == TaggedValue.new("U", "nrecognized") } 44 | end 45 | end 46 | 47 | describe "ints" do 48 | it "decodes n as an Integer" do 49 | 1.upto(64).each do |pow| 50 | assert { decode("~n#{2**pow}").kind_of? Integer } 51 | end 52 | end 53 | it "decodes i as an Integer" do 54 | 1.upto(63).each do |pow| 55 | assert { decode("~i#{2**pow - 1}").kind_of? Integer } 56 | end 57 | end 58 | end 59 | 60 | describe "Tag" do 61 | # Regression example for https://github.com/cognitect/transit-ruby/pull/19 62 | it "is does not == its String value" do 63 | assert { Decoder::Tag.new("foo") != "foo" } 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/transit/exemplar_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2014 Cognitect. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS-IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # -*- coding: utf-8 -*- 17 | require 'spec_helper' 18 | 19 | # Assumes that the examples are in the simple_examples dir at the top. 20 | 21 | TOP_DIR=File.dirname(File.dirname(File.dirname(__FILE__))) 22 | 23 | ARRAY_SIMPLE = [1, 2, 3] 24 | ARRAY_MIXED = [0, 1, 2.0, true, false, 'five', :six, Transit::Symbol.new(:seven), '~eight', nil] 25 | ARRAY_NESTED = [ARRAY_SIMPLE, ARRAY_MIXED] 26 | SMALL_STRINGS = ["","a","ab","abc","abcd","abcde","abcdef"] 27 | POWERS_OF_TWO = (0..65).map {|x| 2**x} 28 | INTERESTING_INTS = (POWERS_OF_TWO.map {|x| ints_centered_on(x, 2)}).flatten 29 | 30 | UUIDS = [ 31 | Transit::UUID.new('5a2cbea3-e8c6-428b-b525-21239370dd55'), 32 | Transit::UUID.new('d1dc64fa-da79-444b-9fa4-d4412f427289'), 33 | Transit::UUID.new('501a978e-3a3e-4060-b3be-1cf2bd4b1a38'), 34 | Transit::UUID.new('b3ba141a-a776-48e4-9fae-a28ea8571f58')] 35 | 36 | URIS = [ 37 | Addressable::URI.parse('http://example.com'), 38 | Addressable::URI.parse('ftp://example.com'), 39 | Addressable::URI.parse('file:///path/to/file.txt'), 40 | Addressable::URI.parse('http://www.詹姆斯.com/')] 41 | 42 | DATES = [-6106017600000, 0, 946728000000, 1396909037000].map {|x| Transit::DateTimeUtil.from_millis(x)} 43 | 44 | SYMBOLS = [:a, :ab ,:abc ,:abcd, :abcde, :a1, :b2, :c3, :a_b] 45 | TRANSIT_SYMBOLS = SYMBOLS.map {|x| Transit::Symbol.new(x)} 46 | 47 | SET_SIMPLE = Set.new(ARRAY_SIMPLE) 48 | SET_MIXED = Set.new(ARRAY_MIXED) 49 | SET_NESTED= Set.new([SET_SIMPLE, SET_MIXED]) 50 | 51 | MAP_SIMPLE = {a: 1, b: 2, c: 3} 52 | MAP_MIXED = {:a=>1, :b=>"a string", :c=>true} 53 | MAP_NESTED = {simple: MAP_SIMPLE, mixed: MAP_MIXED} 54 | 55 | Exemplar = Struct.new(:name, :expected_value) 56 | 57 | EXEMPLARS = [ 58 | Exemplar.new('nil', nil), 59 | Exemplar.new('true', true), 60 | Exemplar.new('false', false), 61 | Exemplar.new('zero', 0), 62 | Exemplar.new('one', 1), 63 | Exemplar.new('one_string', 'hello'), 64 | Exemplar.new('one_keyword', :hello), 65 | Exemplar.new('one_symbol', Transit::Symbol.new('hello')), 66 | Exemplar.new('one_date', DateTime.new(2000,1,1,12)), # Transit::DateTimeUtil.from_millis(946728000000)), 67 | Exemplar.new("vector_simple", ARRAY_SIMPLE), 68 | Exemplar.new("vector_empty", []), 69 | Exemplar.new("vector_mixed", ARRAY_MIXED), 70 | Exemplar.new("vector_nested", ARRAY_NESTED), 71 | Exemplar.new("small_strings", SMALL_STRINGS ), 72 | Exemplar.new("strings_tilde", SMALL_STRINGS.map{|s| "~#{s}"}), 73 | Exemplar.new("strings_hash", SMALL_STRINGS.map{|s| "##{s}"}), 74 | Exemplar.new("strings_hat", SMALL_STRINGS.map{|s| "^#{s}"}), 75 | Exemplar.new("small_ints", ints_centered_on(0)), 76 | Exemplar.new("ints", (0...128).to_a), 77 | Exemplar.new("ints_interesting", INTERESTING_INTS), 78 | Exemplar.new("ints_interesting_neg", INTERESTING_INTS.map {|x| -1 * x}), 79 | Exemplar.new("doubles_small", ints_centered_on(0).map {|x| Float(x)}), 80 | Exemplar.new("doubles_interesting", [-3.14159, 3.14159, 4E11, 2.998E8, 6.626E-34]), 81 | Exemplar.new('one_uuid', UUIDS.first), 82 | Exemplar.new('uuids', UUIDS), 83 | Exemplar.new('one_uri', URIS.first), 84 | Exemplar.new('uris', URIS), 85 | Exemplar.new('dates_interesting', DATES), 86 | Exemplar.new('symbols', TRANSIT_SYMBOLS), 87 | Exemplar.new('keywords', SYMBOLS), 88 | Exemplar.new('list_simple', ARRAY_SIMPLE), 89 | Exemplar.new('list_empty', []), 90 | Exemplar.new('list_mixed', ARRAY_MIXED), 91 | Exemplar.new('list_nested', [ARRAY_SIMPLE, ARRAY_MIXED]), 92 | Exemplar.new('set_simple', SET_SIMPLE), 93 | Exemplar.new("set_empty", Set.new), 94 | Exemplar.new("set_mixed", SET_MIXED), 95 | Exemplar.new("set_nested", SET_NESTED), 96 | Exemplar.new('map_simple', MAP_SIMPLE), 97 | Exemplar.new('map_mixed', MAP_MIXED), 98 | Exemplar.new('map_nested', MAP_NESTED), 99 | Exemplar.new('map_string_keys', {"first"=>1, "second"=>2, "third"=>3}), 100 | Exemplar.new('map_numeric_keys', {1=>"one", 2=>"two"}), 101 | Exemplar.new('map_vector_keys', {[1,1] => 'one', [2, 2] => 'two'}), 102 | Exemplar.new('map_10_items', hash_of_size(10)), 103 | Exemplar.new("maps_two_char_sym_keys", [{Transit::Symbol.new(:aa)=>1, Transit::Symbol.new(:bb)=>2}, 104 | {Transit::Symbol.new(:aa)=>3, Transit::Symbol.new(:bb)=>4}, 105 | {Transit::Symbol.new(:aa)=>5, Transit::Symbol.new(:bb)=>6}]), 106 | Exemplar.new("maps_three_char_sym_keys", [{Transit::Symbol.new(:aaa)=>1, Transit::Symbol.new(:bbb)=>2}, 107 | {Transit::Symbol.new(:aaa)=>3, Transit::Symbol.new(:bbb)=>4}, 108 | {Transit::Symbol.new(:aaa)=>5, Transit::Symbol.new(:bbb)=>6}]), 109 | Exemplar.new("maps_four_char_sym_keys", [{Transit::Symbol.new(:aaaa)=>1, Transit::Symbol.new(:bbbb)=>2}, 110 | {Transit::Symbol.new(:aaaa)=>3, Transit::Symbol.new(:bbbb)=>4}, 111 | {Transit::Symbol.new(:aaaa)=>5, Transit::Symbol.new(:bbbb)=>6}]), 112 | Exemplar.new("maps_two_char_string_keys", [{'aa'=>1, 'bb'=>2}, {'aa'=>3, 'bb'=>4}, {'aa'=>5, 'bb'=>6}]), 113 | Exemplar.new("maps_three_char_string_keys", [{'aaa'=>1, 'bbb'=>2}, {'aaa'=>3, 'bbb'=>4}, {'aaa'=>5, 'bbb'=>6}]), 114 | Exemplar.new("maps_four_char_string_keys", [{'aaaa'=>1, 'bbbb'=>2}, {'aaaa'=>3, 'bbbb'=>4}, {'aaaa'=>5, 'bbbb'=>6}]), 115 | Exemplar.new("maps_unrecognized_keys", 116 | [Transit::TaggedValue.new("abcde", :anything), Transit::TaggedValue.new("fghij", :"anything-else")]), 117 | Exemplar.new("map_unrecognized_vals", {key: "~Unrecognized"}), 118 | Exemplar.new("vector_unrecognized_vals", ["~Unrecognized"]), 119 | Exemplar.new("vector_1935_keywords_repeated_twice", array_of_symbols(1935, 3870)), 120 | Exemplar.new("vector_1936_keywords_repeated_twice", array_of_symbols(1936, 3872)), 121 | Exemplar.new("vector_1937_keywords_repeated_twice", array_of_symbols(1937, 3874)), 122 | Exemplar.new("vector_special_numbers", [Float::NAN, Float::INFINITY, -Float::INFINITY]) 123 | ] 124 | 125 | [10, 1935, 1936, 1937].each do |i| 126 | EXEMPLARS << Exemplar.new( "map_#{i}_nested", {f: hash_of_size(i), s: hash_of_size(i)}) 127 | end 128 | 129 | def verify_exemplar(exemplar, type, suffix) 130 | path = "../transit-format/examples/0.8/simple/#{exemplar.name}#{suffix}" 131 | it "reads what we expect from #{path}" do 132 | raise "Can't open #{path}" unless File.exist?(path) 133 | File.open(path) do |io| 134 | actual_value = Transit::Reader.new(type, io).read 135 | assert { exemplar.expected_value == actual_value } 136 | end 137 | end 138 | end 139 | 140 | module Transit 141 | shared_examples "exemplars" do |type, suffix| 142 | EXEMPLARS.each {|ex| verify_exemplar(ex, type, suffix)} 143 | end 144 | 145 | describe "JSON exemplars" do 146 | include_examples "exemplars", :json, '.json' 147 | end 148 | 149 | describe "JSON-VERBOSE exemplars" do 150 | include_examples "exemplars", :json_verbose, '.verbose.json' 151 | end 152 | 153 | describe "MessagePack exemplars" do 154 | include_examples "exemplars", :msgpack, '.mp' 155 | end 156 | end 157 | -------------------------------------------------------------------------------- /spec/transit/marshaler_spec.rb: -------------------------------------------------------------------------------- 1 | module Transit 2 | describe Marshaler do 3 | it "caches non-verbose handlers" do 4 | io = StringIO.new 5 | first = Transit::Marshaler::Json.new(io,{}).instance_variable_get("@handlers") 6 | second = Transit::Marshaler::Json.new(io,{}).instance_variable_get("@handlers") 7 | third = Transit::Marshaler::MessagePack.new(io,{}).instance_variable_get("@handlers") 8 | assert { first } 9 | assert { first.equal?(second) } 10 | assert { second.equal?(third) } 11 | end 12 | 13 | it "caches verbose handlers" do 14 | io = StringIO.new 15 | first = Transit::Marshaler::VerboseJson.new(io,{}).instance_variable_get("@handlers") 16 | second = Transit::Marshaler::VerboseJson.new(io,{}).instance_variable_get("@handlers") 17 | assert { first } 18 | assert { first.equal?(second) } 19 | end 20 | 21 | it "caches verbose and non-verbose handlers separately" do 22 | io = StringIO.new 23 | first = Transit::Marshaler::Json.new(io,{}).instance_variable_get("@handlers") 24 | second = Transit::Marshaler::VerboseJson.new(io,{}).instance_variable_get("@handlers") 25 | assert { first } 26 | assert { second } 27 | assert { !first.equal?(second) } 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/transit/reader_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require 'spec_helper' 16 | 17 | module Transit 18 | describe Reader do 19 | shared_examples "read without a block" do |type| 20 | it "reads a single top-level #{type} element" do 21 | input = {:this => [1,2,3,{:that => "the other"}]} 22 | 23 | io = StringIO.new('', 'w+') 24 | writer = Transit::Writer.new(type, io) 25 | writer.write(input) 26 | 27 | reader = Transit::Reader.new(type, StringIO.new(io.string)) 28 | assert { reader.read == input } 29 | end 30 | end 31 | 32 | describe "reading without a block" do 33 | include_examples "read without a block", :json 34 | include_examples "read without a block", :json_verbose 35 | include_examples "read without a block", :msgpack 36 | end 37 | 38 | shared_examples "read with a block" do |type| 39 | it "reads multiple top-level #{type} elements from a single IO" do 40 | inputs = ["abc", 41 | 123456789012345678901234567890, 42 | [:this, :that], 43 | {:this => [1,2,3,{:that => "the other"}]}] 44 | outputs = [] 45 | 46 | io = StringIO.new('', 'w+') 47 | writer = Transit::Writer.new(type, io) 48 | inputs.each {|i| writer.write(i)} 49 | reader = Transit::Reader.new(type, StringIO.new(io.string)) 50 | if Transit::jruby? 51 | # Ignore expected EOFException raised after the StringIO is exhausted 52 | reader.read {|val| outputs << val} rescue nil 53 | else 54 | reader.read {|val| outputs << val} 55 | end 56 | 57 | assert { outputs == inputs } 58 | end 59 | end 60 | 61 | describe "reading with a block" do 62 | include_examples "read with a block", :json 63 | include_examples "read with a block", :json_verbose 64 | include_examples "read with a block", :msgpack 65 | end 66 | 67 | describe 'handler registration' do 68 | describe 'overrides' do 69 | describe 'ground types' do 70 | Decoder::GROUND_TAGS.each do |ground| 71 | it "prevents override of #{ground} handler" do 72 | assert { 73 | rescuing { 74 | Reader.new(:json, StringIO.new, :handlers => {ground => Object.new}) 75 | }.message =~ /ground types/ } 76 | end 77 | end 78 | end 79 | 80 | it 'supports override of default string handlers' do 81 | io = StringIO.new("[\"~rhttp://foo.com\"]","r+") 82 | reader = Reader.new(:json, io, :handlers => {"r" => Class.new { def from_rep(v) "DECODED: #{v}" end}.new}) 83 | assert { reader.read == ["DECODED: http://foo.com"] } 84 | end 85 | 86 | it 'supports override of default hash handlers' do 87 | my_uuid_class = Class.new(String) 88 | my_uuid = my_uuid_class.new(UUID.new.to_s) 89 | io = StringIO.new({"~#u" => my_uuid.to_s}.to_json) 90 | reader = Reader.new(:json, io, :handlers => {"u" => Class.new { define_method(:from_rep) {|v| my_uuid_class.new(v)}}.new}) 91 | assert { reader.read == my_uuid } 92 | end 93 | 94 | it 'supports override of the default handler' do 95 | io = StringIO.new("~Xabc".to_json) 96 | reader = Reader.new(:json, io, :default_handler => Class.new { def from_rep(tag,val) raise "Unacceptable: #{s}" end}.new) 97 | assert { rescuing {reader.read }.message =~ /Unacceptable/ } 98 | end 99 | end 100 | 101 | describe 'extensions' do 102 | it 'supports string-based extensions' do 103 | io = StringIO.new("~D2014-03-15".to_json) 104 | reader = Reader.new(:json, io, :handlers => {"D" => Class.new { def from_rep(v) Date.parse(v) end}.new}) 105 | assert { reader.read == Date.new(2014,3,15) } 106 | end 107 | 108 | it 'supports hash based extensions' do 109 | io = StringIO.new({"~#Times2" => 44}.to_json) 110 | reader = Reader.new(:json, io, :handlers => {"Times2" => Class.new { def from_rep(v) v * 2 end}.new}) 111 | assert { reader.read == 88 } 112 | end 113 | 114 | it 'supports hash based extensions that return nil' do 115 | io = StringIO.new({"~#Nil" => :anything}.to_json) 116 | reader = Reader.new(:json, io, :handlers => {"Nil" => Class.new { def from_rep(_) nil end}.new}) 117 | assert { reader.read == nil } 118 | end 119 | 120 | it 'supports hash based extensions that return false' do 121 | io = StringIO.new({"~#False" => :anything}.to_json) 122 | reader = Reader.new(:json, io, :handlers => {"False" => Class.new { def from_rep(_) false end}.new}) 123 | assert { reader.read == false } 124 | end 125 | 126 | it 'supports complex hash values' do 127 | io = StringIO.new([ 128 | {"~#person"=> 129 | {"~:first_name" => "Transit","~:last_name" => "Ruby","~:birthdate" => "~D2014-01-02"}}, 130 | {"^0"=> 131 | {"^1" => "Transit","^2" => "Ruby","^3" => "~D2014-01-03"}}].to_json) 132 | 133 | person_handler = Class.new do 134 | def from_rep(v) 135 | Person.new(v[:first_name],v[:last_name],v[:birthdate]) 136 | end 137 | end 138 | date_handler = Class.new do 139 | def from_rep(v) Date.parse(v) end 140 | end 141 | reader = Reader.new(:json, io, 142 | :handlers => { 143 | "person" => person_handler.new, 144 | "D" => date_handler.new}) 145 | expected = [Person.new("Transit", "Ruby", Date.new(2014,1,2)), 146 | Person.new("Transit", "Ruby", Date.new(2014,1,3))] 147 | assert { reader.read == expected } 148 | end 149 | end 150 | 151 | describe 'Dates/Times' do 152 | it "delivers a UTC DateTime for a non-UTC date string" do 153 | io = StringIO.new(["~t2014-04-14T12:20:50.152-05:00"].to_json) 154 | reader = Reader.new(:json, io) 155 | expect(Transit::DateTimeUtil.to_millis(reader.read.first)).to eq(Transit::DateTimeUtil.to_millis(DateTime.new(2014,4,14,17,20,50.152,"Z"))) 156 | end 157 | end 158 | 159 | describe 'edge cases found in generative testing' do 160 | it 'caches nested structures correctly' do 161 | io = StringIO.new(["~#cmap",[["~#point",["~n10","~n11"]],"~:foobar",["^1",["~n10","~n13"]],"^2"]].to_json) 162 | reader = Reader.new(:json, io) 163 | expected = {TaggedValue.new("point", [10,11]) => :foobar, 164 | TaggedValue.new("point", [10,13]) => :foobar} 165 | assert { reader.read == expected } 166 | end 167 | end 168 | end 169 | end 170 | end 171 | -------------------------------------------------------------------------------- /spec/transit/rolling_cache_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require 'spec_helper' 16 | 17 | module Transit 18 | describe RollingCache do 19 | describe 'writing' do 20 | it 'returns the value the first time it sees it' do 21 | assert { RollingCache.new.write('abcd') == 'abcd' } 22 | end 23 | 24 | it 'returns a key the 2nd thru n times it sees a value' do 25 | rc = RollingCache.new 26 | assert { rc.write('abcd') == 'abcd' } 27 | 28 | key = rc.write('abcd') 29 | assert { key != 'abcd' } 30 | 31 | 100.times do 32 | assert { rc.write('abcd') == key } 33 | end 34 | end 35 | 36 | it 'can handle CACHE_SIZE different values' do 37 | rc = RollingCache.new 38 | RollingCache::CACHE_SIZE.times do |i| 39 | assert { rc.write("value#{i}") == "value#{i}" } 40 | end 41 | 42 | assert { rc.size == RollingCache::CACHE_SIZE } 43 | end 44 | 45 | it 'resets after CACHE_SIZE different values' do 46 | rc = RollingCache.new 47 | (RollingCache::CACHE_SIZE+1).times do |i| 48 | assert{ rc.write("value#{i}") == "value#{i}" } 49 | end 50 | 51 | assert { rc.size == 1 } 52 | end 53 | end 54 | 55 | describe ".cacheable?" do 56 | it 'returns false for small strings' do 57 | cache = RollingCache.new 58 | 59 | names = random_strings(3, 500) 60 | 1000.times do |i| 61 | name = names.sample 62 | assert { !cache.cacheable?(name, false) } 63 | assert { !cache.cacheable?(name, true) } 64 | end 65 | end 66 | 67 | it 'returns false for non map-keys' do 68 | cache = RollingCache.new 69 | 70 | names = random_strings(200, 500) 71 | 1000.times do |i| 72 | name = names.sample 73 | assert { !cache.cacheable?(name, false) } 74 | end 75 | end 76 | end 77 | 78 | describe ".cache_key?" do 79 | it 'special cases map-as-array key as false' do 80 | cache = RollingCache.new 81 | assert { !cache.cache_key?(Transit::MAP_AS_ARRAY) } 82 | end 83 | end 84 | 85 | describe 'reading' do 86 | it 'returns the value, given a key that has a value in the cache' do 87 | rc = RollingCache.new 88 | rc.write 'abcd' 89 | key = rc.write 'abcd' 90 | assert { rc.read(key) == 'abcd' } 91 | end 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /spec/transit/round_trip_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require 'spec_helper' 16 | 17 | def nan?(obj) 18 | obj.respond_to?(:nan?) and obj.nan? 19 | end 20 | 21 | def round_trip(obj, type, opts={}) 22 | obj_before = obj 23 | 24 | io = StringIO.new('', 'w+') 25 | writer = Transit::Writer.new(type, io, :handlers => opts[:write_handlers]) 26 | writer.write(obj) 27 | 28 | # ensure that we don't modify the object being written 29 | if nan?(obj_before) 30 | assert { obj.nan? } 31 | else 32 | assert { obj == obj_before } 33 | end 34 | reader = Transit::Reader.new(type, StringIO.new(io.string), :handlers => opts[:read_handlers]) 35 | reader.read 36 | end 37 | 38 | def assert_equal_times(actual,expected) 39 | return false unless expected.is_a?(Date) || expected.is_a?(Time) || expected.is_a?(DateTime) 40 | assert { Transit::DateTimeUtil.to_millis(actual) == Transit::DateTimeUtil.to_millis(expected) } 41 | assert { actual.zone == expected.zone } 42 | end 43 | 44 | def assert_nan(actual,expected) 45 | return false unless nan?(expected) 46 | expect(actual.respond_to?(:nan?)).to eq(true) 47 | expect(actual.nan?).to eq(true) 48 | end 49 | 50 | def validate(expected, actual) 51 | assert_equal_times(actual, expected) || assert_nan(actual, expected) || (expect(actual).to eq(expected)) 52 | end 53 | 54 | def round_trips(label, obj, type, opts={}) 55 | expected = opts[:expected] || obj 56 | 57 | it "round trips #{label} at top level", :focus => !!opts[:focus], :pending => opts[:pending] do 58 | validate(expected, round_trip(obj, type, opts)) 59 | end 60 | 61 | it "round trips #{label} as a map key", :focus => !!opts[:focus], :pending => opts[:pending] do 62 | validate(expected, round_trip({obj => 0}, type, opts).keys.first) 63 | end 64 | 65 | it "round trips #{label} as a map value", :focus => !!opts[:focus], :pending => opts[:pending] do 66 | validate(expected, round_trip({a: obj}, type, opts).values.first) 67 | end 68 | 69 | it "round trips #{label} as an array value", :focus => !!opts[:focus], :pending => opts[:pending] do 70 | validate(expected, round_trip([obj], type, opts).first) 71 | end 72 | end 73 | 74 | module Transit 75 | PhoneNumber = Struct.new(:area, :prefix, :suffix) 76 | def PhoneNumber.parse(p) 77 | area, prefix, suffix = p.split(".") 78 | PhoneNumber.new(area, prefix, suffix) 79 | end 80 | 81 | class PhoneNumberHandler 82 | def tag(_) "P" end 83 | def rep(p) "#{p.area}.#{p.prefix}.#{p.suffix}" end 84 | def string_rep(p) rep(p) end 85 | end 86 | 87 | class PhoneNumberReadHandler 88 | def from_rep(v) PhoneNumber.parse(v) end 89 | end 90 | 91 | class PersonReadHandler 92 | def from_rep(v) 93 | Person.new(v[:first_name],v[:last_name],v[:birthdate]) 94 | end 95 | end 96 | 97 | shared_examples "round trips" do |type| 98 | round_trips("nil", nil, type) 99 | round_trips("a keyword", random_symbol, type) 100 | round_trips("a string", random_string, type) 101 | round_trips("a string starting with ~", "~#{random_string}", type) 102 | round_trips("a string starting with ^", "^#{random_string}", type) 103 | round_trips("a string starting with `", "`#{random_string}", type) 104 | round_trips("true", true, type) 105 | round_trips("false", false, type) 106 | round_trips("a small int", 1, type) 107 | round_trips("a big int", 123456789012345, type) 108 | round_trips("a very big int", 123456789012345679012345678890, type) 109 | round_trips("a float", 1234.56, type) 110 | round_trips("NaN", Float::NAN, type) 111 | round_trips("Infinity", Float::INFINITY, type) 112 | round_trips("-Infinity", -Float::INFINITY, type) 113 | round_trips("a bigdec", BigDecimal("123.45"), type) 114 | round_trips("an instant (DateTime local)", DateTime.new(2014,1,2,3,4,5, "-5"), type, 115 | :expected => DateTime.new(2014,1,2, (3+5) ,4,5, "+00:00")) 116 | round_trips("an instant (DateTime gmt)", DateTime.new(2014,1,2,3,4,5), type) 117 | round_trips("an instant (Time local)", Time.new(2014,1,2,3,4,5, "-05:00"), type, 118 | :expected => DateTime.new(2014,1,2, (3+5) ,4,5, "+00:00")) 119 | round_trips("an instant (Time gmt)", Time.new(2014,1,2,3,4,5, "+00:00"), type, 120 | :expected => DateTime.new(2014,1,2,3,4,5, "+00:00")) 121 | round_trips("a Date", Date.new(2014,1,2), type, :expected => DateTime.new(2014,1,2)) 122 | round_trips("a uuid", UUID.new, type) 123 | round_trips("a link", Link.new(Addressable::URI.parse("http://example.org/search"), "search"), type) 124 | round_trips("a link", Link.new(Addressable::URI.parse("http://example.org/search"), "search", nil, "image"), type) 125 | round_trips("a link with string uri", Link.new("http://example.org/search", "search", nil, "image"), type) 126 | round_trips("a uri (url)", Addressable::URI.parse("http://example.com"), type) 127 | round_trips("a uri (file)", Addressable::URI.parse("file:///path/to/file.txt"), type) 128 | round_trips("a bytearray", ByteArray.new(random_string(50)), type) 129 | round_trips("a Transit::Symbol", Transit::Symbol.new(random_string), type) 130 | round_trips("a hash w/ stringable keys", {"this" => "~hash", "1" => 2}, type) 131 | round_trips("a set", Set.new([1,2,3]), type) 132 | round_trips("a set of sets", Set.new([Set.new([1,2]), Set.new([3,4])]), type) 133 | round_trips("an array", [1,2,3], type) 134 | round_trips("a char", TaggedValue.new("c", "x"), type, :expected => "x") 135 | round_trips("a list", TaggedValue.new("list", [1,2,3]), type, :expected => [1,2,3]) 136 | round_trips("an array of maps w/ cacheable keys", [{"this" => "a"},{"this" => "b"}], type) 137 | 138 | round_trips("edge case chars", %w[` ~ ^ #], type) 139 | 140 | round_trips("an extension scalar", PhoneNumber.new("555","867","5309"), type, 141 | :write_handlers => {PhoneNumber => PhoneNumberHandler.new}, 142 | :read_handlers => {"P" => PhoneNumberReadHandler.new}) 143 | round_trips("an extension struct", Person.new("First","Last",:today), type, 144 | :write_handlers => {Person => PersonHandler.new}, 145 | :read_handlers => {"person" => PersonReadHandler.new}) 146 | round_trips("a hash with simple values", {'a' => 1, 'b' => 2, 'name' => 'russ'}, type) 147 | round_trips("a hash with Transit::Symbols", {Transit::Symbol.new("foo") => Transit::Symbol.new("bar")}, type) 148 | round_trips("a hash with 53 bit ints", {2**53-1 => 2**53-2}, type) 149 | round_trips("a hash with 54 bit ints", {2**53 => 2**53+1}, type) 150 | round_trips("a map with composite keys", {{a: :b} => {c: :d}}, type) 151 | round_trips("a TaggedValue", TaggedValue.new("unrecognized",:value), type) 152 | round_trips("an unrecognized hash encoding", {"~#unrecognized" => :value}, type) 153 | round_trips("an unrecognized string encoding", "~Xunrecognized", type) 154 | 155 | round_trips("a nested structure (map on top)", {a: [1, [{b: "~c"}]]}, type) 156 | round_trips("a nested structure (array on top)", [37, {a: [1, [{b: "~c"}]]}], type) 157 | round_trips("a map that looks like transit data", [{"~#set"=>[1,2,3]},{"~#set"=>[4,5,6]}], type) 158 | round_trips("a ratio of big value", [{"~#ratio"=>["~n36893488147419103231","~n73786976294838206463"]}], type) 159 | end 160 | 161 | describe "Transit using json" do 162 | include_examples "round trips", :json 163 | end 164 | 165 | describe "Transit using json_verbose" do 166 | include_examples "round trips", :json_verbose 167 | end 168 | 169 | describe "Transit using msgpack" do 170 | include_examples "round trips", :msgpack 171 | end 172 | end 173 | -------------------------------------------------------------------------------- /spec/transit/transit_types_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2014 Cognitect. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS-IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | require 'spec_helper' 17 | 18 | module Transit 19 | describe Transit::Symbol do 20 | it 'can be made from a symbol' do 21 | 500.times do 22 | sym = random_symbol 23 | assert { Transit::Symbol.new(sym).to_sym == sym } 24 | end 25 | end 26 | 27 | it 'can be made from a string' do 28 | 500.times do 29 | str = random_string 30 | assert { Transit::Symbol.new(str).to_sym == str.to_sym } 31 | end 32 | end 33 | 34 | it 'is equal to another rendition of itself' do 35 | 500.times do 36 | sym = random_symbol 37 | assert { Transit::Symbol.new(sym) == Transit::Symbol.new(sym)} 38 | end 39 | end 40 | 41 | it 'behaves as a hash key' do 42 | keys = Set.new(Array.new(1000).map {|x| random_symbol}) 43 | 44 | test_hash = {} 45 | keys.each_with_index {|k, i| test_hash[Transit::Symbol.new(k)] = i} 46 | 47 | keys.each_with_index do |k, i| 48 | new_key = Transit::Symbol.new(k) 49 | value = test_hash[new_key] 50 | assert { value == i } 51 | end 52 | end 53 | 54 | it "provides namespace" do 55 | assert { Transit::Symbol.new("foo/bar").namespace == "foo" } 56 | assert { Transit::Symbol.new("foo").namespace == nil } 57 | end 58 | 59 | it "provides name" do 60 | assert { Transit::Symbol.new("foo").name == "foo" } 61 | assert { Transit::Symbol.new("foo/bar").name == "bar" } 62 | end 63 | 64 | it "special cases '/'" do 65 | assert { Transit::Symbol.new("/").name == "/" } 66 | assert { Transit::Symbol.new("/").namespace == nil } 67 | end 68 | end 69 | 70 | describe UUID do 71 | it 'round trips strings' do 72 | 10.times do 73 | uuid = UUID.random 74 | assert { UUID.new(uuid.to_s) == uuid } 75 | end 76 | end 77 | 78 | it 'round trips ints' do 79 | 10.times do 80 | uuid = UUID.random 81 | assert { UUID.new(uuid.most_significant_bits, uuid.least_significant_bits) == uuid } 82 | end 83 | end 84 | end 85 | 86 | describe Link do 87 | let(:href) { Addressable::URI.parse("http://example.org/search") } 88 | let(:string_href) { "http://example.org/search" } 89 | let(:rel) { "search" } 90 | let(:prompt) { "Enter search string" } 91 | let(:name) { "this is my name" } 92 | 93 | it 'can be made from some given arugments' do 94 | link = Link.new(href, rel) 95 | assert { link.href == href } 96 | assert { link.rel == rel } 97 | assert { link.prompt == nil } 98 | assert { link.name == nil } 99 | assert { link.render == nil } 100 | end 101 | 102 | it 'can be made from all 5 given correct arguments' do 103 | link = Link.new(href, rel, name, "Image", prompt) 104 | assert { link.href == href } 105 | assert { link.rel == rel } 106 | assert { link.name == name } 107 | assert { link.render == "image" } 108 | assert { link.prompt == prompt } 109 | end 110 | 111 | it 'can be made with uri in string' do 112 | link = Link.new(string_href, rel) 113 | assert { link.href == Addressable::URI.parse(string_href) } 114 | end 115 | 116 | it 'raises exception if href and rel are not given' do 117 | assert { rescuing { Link.new }.is_a? ArgumentError } 118 | assert { rescuing { Link.new("foo") }.is_a? ArgumentError } 119 | end 120 | 121 | it 'raises exception if render is not a valid value (link|image)' do 122 | assert { rescuing { Link.new(href, rel, nil, "document") }.is_a? ArgumentError } 123 | end 124 | 125 | it 'leaves the input map alone' do 126 | input = {"href" => "http://example.com", "rel" => "???", "render" => "LINK"} 127 | Link.new(input) 128 | assert { input["href"] == "http://example.com" } 129 | assert { input["render"] == "LINK" } 130 | end 131 | 132 | it 'produces a frozen map' do 133 | assert { Link.new("/path", "the-rel").to_h.frozen? } 134 | end 135 | end 136 | 137 | describe ByteArray do 138 | it 'uses the default_external encoding for to_s' do 139 | src = ByteArray.new("foo".force_encoding("ascii")).to_base64 140 | target = ByteArray.from_base64(src) 141 | assert { target.to_s.encoding == Encoding.default_external } 142 | end 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /spec/transit/writer_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Cognitect. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS-IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | require 'spec_helper' 16 | require 'json' 17 | 18 | module Transit 19 | describe Writer do 20 | let(:io) { StringIO.new('', 'w+') } 21 | let(:writer) { Writer.new(:json_verbose, io) } 22 | 23 | describe "marshaling transit types" do 24 | def self.bytes 25 | @bytes ||= SecureRandom.random_bytes 26 | end 27 | 28 | def self.marshals_scalar(label, value, rep, opts={}) 29 | it "marshals #{label}", :focus => opts[:focus] do 30 | writer.write(value) 31 | assert { JSON.parse(io.string) == {"~#'" => rep} } 32 | end 33 | end 34 | 35 | def self.marshals_structure(label, value, rep, opts={}) 36 | it "marshals #{label}", :focus => opts[:focus] do 37 | writer.write(value) 38 | assert { JSON.parse(io.string) == rep } 39 | end 40 | end 41 | 42 | marshals_scalar("a UUID", 43 | UUID.new("dda5a83f-8f9d-4194-ae88-5745c8ca94a7"), 44 | "~udda5a83f-8f9d-4194-ae88-5745c8ca94a7") 45 | marshals_scalar("a Transit::Symbol", Transit::Symbol.new("foo"), "~$foo" ) 46 | marshals_scalar("a Fixnum", 9007199254740999, "~i9007199254740999") 47 | marshals_scalar("a Bignum", 9223372036854775806, "~i9223372036854775806") 48 | marshals_scalar("a Very Bignum", 4256768765123454321897654321234567, "~n4256768765123454321897654321234567") 49 | # Ruby's base64 encoding adds line feed in every 60 encoded 50 | # characters, while Java, 51 | # org.apache.commons.codec.binary.Base64, doesn't add any. Java 52 | # method has option to add line feed, but every 76 characters. 53 | # this divergence may be inevitable 54 | if Transit::jruby? 55 | marshals_scalar("a ByteArray", ByteArray.new(bytes), "~b#{ByteArray.new(bytes).to_base64}".gsub(/\n/, "")) 56 | else 57 | marshals_scalar("a ByteArray", ByteArray.new(bytes), "~b#{ByteArray.new(bytes).to_base64}") 58 | end 59 | marshals_scalar("an URI", Addressable::URI.parse("http://example.com/search"), "~rhttp://example.com/search") 60 | marshals_structure("a link", 61 | Link.new(Addressable::URI.parse("http://example.com/search"), "search", nil, "link", nil), 62 | {"~#link" => 63 | {"href" => "~rhttp://example.com/search", 64 | "rel" => "search", 65 | "name" => nil, 66 | "render" => "link", 67 | "prompt" => nil}}) 68 | marshals_structure("a TaggedValue", TaggedValue.new("tag", "value"), {"~#tag" => "value"}) 69 | marshals_structure("a ratio by Rational class", Rational(1, 3), {"~#ratio" => [1, 3]}) 70 | marshals_structure("a Rational with big number", Rational(4953778853208128465, 636801457410081246), {"~#ratio" => ["~i4953778853208128465", "~i636801457410081246"]}) 71 | end 72 | 73 | describe "custom handlers" do 74 | it "raises when a handler provides nil as a tag" do 75 | handler = Class.new do 76 | def tag(_) nil end 77 | end 78 | writer = Writer.new(:json_verbose, io, :handlers => {Date => handler.new}) 79 | if Transit::jruby? 80 | assert { rescuing { writer.write(Date.today) }.message =~ /Not supported/ } 81 | else 82 | assert { rescuing { writer.write(Date.today) }.message =~ /must provide a non-nil tag/ } 83 | end 84 | end 85 | 86 | it "supports custom handlers for core types" do 87 | handler = Class.new do 88 | def tag(_) "s" end 89 | def rep(s) "MYSTRING: #{s}" end 90 | def string_rep(s) rep(s) end 91 | end 92 | writer = Writer.new(:json_verbose, io, :handlers => {String => handler.new}) 93 | writer.write("this") 94 | assert { JSON.parse(io.string).values.first == "MYSTRING: this" } 95 | end 96 | 97 | it "supports custom handlers for custom types" do 98 | handler = Class.new do 99 | def tag(_) "person" end 100 | def rep(s) {:first_name => s.first_name} end 101 | def string_rep(s) s.first_name end 102 | end 103 | writer = Writer.new(:json_verbose, io, :handlers => {Person => handler.new}) 104 | writer.write(Person.new("Russ")) 105 | assert { JSON.parse(io.string) == {"~#person" => { "~:first_name" => "Russ" } } } 106 | end 107 | 108 | it "supports verbose handlers" do 109 | phone_class = Class.new do 110 | attr_reader :p 111 | def initialize(p) 112 | @p = p 113 | end 114 | end 115 | handler = Class.new do 116 | def tag(_) "phone" end 117 | def rep(v) v.p end 118 | def string_rep(v) v.p.to_s end 119 | def verbose_handler 120 | Class.new do 121 | def tag(_) "phone" end 122 | def rep(v) "PHONE: #{v.p}" end 123 | def string_rep(v) rep(v) end 124 | end.new 125 | end 126 | end 127 | 128 | writer = Writer.new(:json, io, :handlers => {phone_class => handler.new}) 129 | writer.write(phone_class.new(123456789)) 130 | assert { JSON.parse(io.string) == ["~#phone", 123456789] } 131 | 132 | io.rewind 133 | 134 | writer = Writer.new(:json_verbose, io, :handlers => {phone_class => handler.new}) 135 | writer.write(phone_class.new(123456789)) 136 | assert { JSON.parse(io.string) == {"~#phone" => "PHONE: 123456789"} } 137 | end 138 | end 139 | 140 | describe "formats" do 141 | describe "JSON" do 142 | let(:writer) { Writer.new(:json, io) } 143 | 144 | it "writes a map as an array prefixed with '^ '" do 145 | writer.write({:a => :b, 3 => 4}) 146 | assert { JSON.parse(io.string) == ["^ ", "~:a", "~:b", "~i3", 4] } 147 | end 148 | 149 | it "writes a single-char tagged-value as a string" do 150 | writer.write([TaggedValue.new("a","bc")]) 151 | assert { JSON.parse(io.string) == ["~abc"] } 152 | end 153 | 154 | it "writes a multi-char tagged-value as a 2-element array" do 155 | writer.write(TaggedValue.new("abc","def")) 156 | assert { JSON.parse(io.string) == ["~#abc", "def"] } 157 | end 158 | 159 | it "writes a Date as an encoded hash with ms" do 160 | writer.write([Date.new(2014,1,2)]) 161 | assert { JSON.parse(io.string) == ["~m1388620800000"] } 162 | end 163 | 164 | it "writes a Time as an encoded hash with ms" do 165 | writer.write([(Time.at(1388631845) + 0.678)]) 166 | assert { JSON.parse(io.string) == ["~m1388631845678"] } 167 | end 168 | 169 | it "writes a DateTime as an encoded hash with ms" do 170 | writer.write([(Time.at(1388631845) + 0.678).to_datetime]) 171 | assert { JSON.parse(io.string) == ["~m1388631845678"] } 172 | end 173 | 174 | it "writes a quote as a tagged array" do 175 | writer.write("this") 176 | assert { JSON.parse(io.string) == ["~#'", "this"] } 177 | end 178 | 179 | it "writes an int map-key as an encoded string" do 180 | writer.write({1 => :one}) 181 | assert { io.string =~ /~i1/ } 182 | end 183 | end 184 | 185 | describe "JSON_VERBOSE" do 186 | let(:writer) { Writer.new(:json_verbose, io) } 187 | 188 | it "does not use the cache" do 189 | writer.write([{"this" => "that"}, {"this" => "the other"}]) 190 | assert { JSON.parse(io.string) == [{"this" => "that"}, {"this" => "the other"}] } 191 | end 192 | 193 | it "writes a single-char tagged-value as a string" do 194 | writer.write([TaggedValue.new("a","bc")]) 195 | assert { JSON.parse(io.string) == ["~abc"] } 196 | end 197 | 198 | it "writes a multi-char tagged-value as a map" do 199 | writer.write(TaggedValue.new("abc","def")) 200 | assert { JSON.parse(io.string) == {"~#abc" => "def"} } 201 | end 202 | 203 | it "writes a Date as an encoded human-readable strings" do 204 | writer.write([Date.new(2014,1,2)]) 205 | assert { JSON.parse(io.string) == ["~t2014-01-02T00:00:00.000Z"] } 206 | end 207 | 208 | it "writes a Time as an encoded human-readable strings" do 209 | writer.write([(Time.at(1388631845) + 0.678)]) 210 | assert { JSON.parse(io.string) == ["~t2014-01-02T03:04:05.678Z"] } 211 | end 212 | 213 | it "writes a DateTime as an encoded human-readable strings" do 214 | writer.write([(Time.at(1388631845) + 0.678).to_datetime]) 215 | assert { JSON.parse(io.string) == ["~t2014-01-02T03:04:05.678Z"] } 216 | end 217 | 218 | it "writes a quote as a tagged map" do 219 | writer.write("this") 220 | assert { JSON.parse(io.string) == {"~#'" => "this"} } 221 | end 222 | 223 | it "writes an int map-key as an encoded string" do 224 | writer.write({1 => :one}) 225 | assert { io.string =~ /~i1/ } 226 | end 227 | end 228 | 229 | # JRuby skips these 3 examples since they use raw massage pack 230 | # api. Also, JRuby doesn't have good counterpart. 231 | describe "MESSAGE_PACK", :unless => Transit::jruby? do 232 | let(:writer) { Writer.new(:msgpack, io) } 233 | 234 | it "writes a single-char tagged-value as a 2-element array" do 235 | writer.write(TaggedValue.new("a","bc")) 236 | assert { MessagePack::Unpacker.new(StringIO.new(io.string)).read == ["~#'", "~abc"] } 237 | end 238 | 239 | it "writes a multi-char tagged-value as a 2-element array" do 240 | writer.write(TaggedValue.new("abc","def")) 241 | assert { MessagePack::Unpacker.new(StringIO.new(io.string)).read == ["~#abc", "def"] } 242 | end 243 | 244 | it "writes a top-level scalar as a quote-tagged value" do 245 | writer.write("this") 246 | assert { MessagePack::Unpacker.new(StringIO.new(io.string)).read == ["~#'", "this"] } 247 | end 248 | 249 | it "writes an int map-key as an int" do 250 | writer.write({1 => :one}) 251 | assert { io.string.each_char.drop(1).first == "\u0001" } 252 | end 253 | end 254 | 255 | describe "ints" do 256 | it "encodes ints <= max signed 64 bit with 'i'" do 257 | 1.upto(5).to_a.reverse.each do |n| 258 | io.rewind 259 | writer.write([(2**63) - n]) 260 | assert { JSON.parse(io.string).first[1] == "i" } 261 | end 262 | end 263 | 264 | it "encodes ints > max signed 64 bit with 'n'" do 265 | 0.upto(4).each do |n| 266 | io.rewind 267 | writer.write([(2**63) + n]) 268 | assert { JSON.parse(io.string).first[1] == "n" } 269 | end 270 | end 271 | end 272 | 273 | describe "escaped strings" do 274 | [ESC, SUB, RES, "#{SUB} "].each do |c| 275 | it "escapes a String starting with #{c}" do 276 | writer.write("#{c}whatever") 277 | assert { JSON.parse(io.string) == {"#{TAG}#{QUOTE}" => "~#{c}whatever"}} 278 | end 279 | end 280 | end 281 | 282 | describe "edge cases" do 283 | it 'writes correct json for TaggedValues in a map-as-array (json)' do 284 | writer = Writer.new(:json, io) 285 | v = {7924023966712353515692932 => TaggedValue.new("ratio", [1, 3]), 286 | 100 => TaggedValue.new("ratio", [1, 2])} 287 | writer.write(v) 288 | expected = ["^ ", 289 | "~n7924023966712353515692932", ["~#ratio", [1,3]], 290 | "~i100",["^1", [1,2]]] 291 | actual = io.string 292 | assert { JSON.parse(io.string) == expected } 293 | end 294 | 295 | it 'writes out strings starting with `' do 296 | v = "`~hello" 297 | writer.write([v]) 298 | assert { JSON.parse(io.string).first == "~`~hello" } 299 | end 300 | 301 | it 'raises when there is no handler for the type at the top level' do 302 | if Transit::jruby? 303 | assert { rescuing { writer.write(Class.new.new) }.message =~ /Not supported/ } 304 | else 305 | assert { rescuing { writer.write(Class.new.new) }.message =~ /Can not find a Write Handler/ } 306 | end 307 | end 308 | 309 | it 'raises when there is no handler for the type' do 310 | if Transit::jruby? 311 | assert { rescuing { writer.write([Class.new.new]) }.message =~ /Not supported/ } 312 | else 313 | assert { rescuing { writer.write([Class.new.new]) }.message =~ /Can not find a Write Handler/ } 314 | end 315 | end 316 | end 317 | end 318 | end 319 | end 320 | -------------------------------------------------------------------------------- /transit-ruby.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | files = `git ls-files -- lib/*`.split("\n") + 4 | ["README.md","CHANGELOG.md","LICENSE",".yardopts",".yard_redcarpet_ext", "lib/transit.jar", "Jarfile"] 5 | cruby_files = files.grep /cruby/ 6 | jruby_files = files.grep(/jruby/) + ["lib/transit.jar", "Jarfile"] 7 | 8 | Gem::Specification.new do |spec| 9 | spec.name = "transit-ruby" 10 | spec.version = "0.8.dev" 11 | spec.authors = ["Russ Olsen","David Chelimsky","Yoko Harada"] 12 | spec.email = ["russ@cognitect.com","dchelimsky@cognitect.com","yoko@cognitect.com"] 13 | spec.summary = %q{Transit marshalling for Ruby} 14 | spec.description = %q{Transit marshalling for Ruby} 15 | spec.homepage = "http://github.com/cognitect/transit-ruby" 16 | spec.license = "Apache License 2.0" 17 | 18 | spec.required_ruby_version = '>= 2.1.10' 19 | 20 | if defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby" 21 | spec.files = files - cruby_files 22 | spec.platform = 'java' 23 | spec.add_dependency "lock_jar", "~> 0.12.0" 24 | spec.add_development_dependency "rake-compiler", "~> 0.9.2" 25 | private_key_path = nil # not yet supported 26 | public_key_path = nil # not yet supported 27 | else 28 | spec.files = files - jruby_files 29 | spec.add_dependency "oj", "~> 2.18" 30 | spec.add_dependency "msgpack", "~> 1.1.0" 31 | spec.add_development_dependency "yard", "~> 0.9.11" 32 | private_key_path = File.expand_path(File.join(ENV['HOME'], '.gem', 'transit-ruby', 'gem-private_key.pem')) 33 | public_key_path = File.expand_path(File.join(ENV['HOME'], '.gem', 'transit-ruby', 'gem-public_cert.pem')) 34 | end 35 | 36 | spec.test_files = `git ls-files -- spec/*`.split("\n") 37 | spec.require_paths = ["lib"] 38 | 39 | spec.add_dependency "addressable", "~> 2.3" 40 | spec.add_development_dependency "rake", "~> 10.1" 41 | spec.add_development_dependency "rspec", "~> 3.0" 42 | spec.add_development_dependency "wrong", "~> 0.7.1" 43 | 44 | if ENV['SIGN_GEM'] && private_key_path && File.exist?(private_key_path) 45 | spec.signing_key = private_key_path 46 | spec.cert_chain = [public_key_path] 47 | end 48 | end 49 | --------------------------------------------------------------------------------