├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── TODO.md ├── bertrpc ├── client.go ├── decoder.go ├── decoder_bert.go ├── decoder_bert_test.go ├── decoder_test.go ├── encoder.go ├── encoder_test.go ├── etf.go └── rpc.go ├── examples └── clients │ └── ejabberd-register │ └── ejabberd-register.go └── go.mod /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Go template 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at contact@process-one.net. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We'd love for you to contribute to our source code and to make our project even better than it is 4 | today! Here are the guidelines we'd like you to follow: 5 | 6 | * [Code of Conduct](#coc) 7 | * [Questions and Problems](#question) 8 | * [Issues and Bugs](#issue) 9 | * [Feature Requests](#feature) 10 | * [Issue Submission Guidelines](#submit) 11 | * [Pull Request Submission Guidelines](#submit-pr) 12 | * [Signing the CLA](#cla) 13 | 14 | ## Code of Conduct 15 | 16 | Help us keep our community open-minded and inclusive. Please read and follow our [Code of Conduct][coc]. 17 | 18 | ## Questions, Bugs, Features 19 | 20 | ### Got a Question or Problem? 21 | 22 | Do not open issues for general support questions as we want to keep GitHub issues for bug reports 23 | and feature requests. You've got much better chances of getting your question answered on dedicated 24 | support platforms, the best being [Stack Overflow][stackoverflow]. 25 | 26 | Stack Overflow is a much better place to ask questions since: 27 | 28 | - there are thousands of people willing to help on Stack Overflow 29 | - questions and answers stay available for public viewing so your question / answer might help 30 | someone else 31 | - Stack Overflow's voting system assures that the best answers are prominently visible. 32 | 33 | To save your and our time, we will systematically close all issues that are requests for general 34 | support and redirect people to the section you are reading right now. 35 | 36 | ### Found an Issue or Bug? 37 | 38 | If you find a bug in the source code, you can help us by submitting an issue to our 39 | [GitHub Repository][github]. Even better, you can submit a Pull Request with a fix. 40 | 41 | ### Missing a Feature? 42 | 43 | You can request a new feature by submitting an issue to our [GitHub Repository][github-issues]. 44 | 45 | If you would like to implement a new feature then consider what kind of change it is: 46 | 47 | * **Major Changes** that you wish to contribute to the project should be discussed first in an 48 | [GitHub issue][github-issues] that clearly outlines the changes and benefits of the feature. 49 | * **Small Changes** can directly be crafted and submitted to the [GitHub Repository][github] 50 | as a Pull Request. See the section about [Pull Request Submission Guidelines](#submit-pr). 51 | 52 | ## Issue Submission Guidelines 53 | 54 | Before you submit your issue search the archive, maybe your question was already answered. 55 | 56 | If your issue appears to be a bug, and hasn't been reported, open a new issue. Help us to maximize 57 | the effort we can spend fixing issues and adding new features, by not reporting duplicate issues. 58 | 59 | The "[new issue][github-new-issue]" form contains a number of prompts that you should fill out to 60 | make it easier to understand and categorize the issue. 61 | 62 | ## Pull Request Submission Guidelines 63 | 64 | By submitting a pull request for a code or doc contribution, you need to have the right 65 | to grant your contribution's copyright license to ProcessOne. Please check [ProcessOne CLA][cla] 66 | for details. 67 | 68 | Before you submit your pull request consider the following guidelines: 69 | 70 | * Search [GitHub][github-pr] for an open or closed Pull Request 71 | that relates to your submission. You don't want to duplicate effort. 72 | * Make your changes in a new git branch: 73 | 74 | ```shell 75 | git checkout -b my-fix-branch master 76 | ``` 77 | * Test your changes and, if relevant, expand the automated test suite. 78 | * Create your patch commit, including appropriate test cases. 79 | * If the changes affect public APIs, change or add relevant documentation. 80 | * Commit your changes using a descriptive commit message. 81 | 82 | ```shell 83 | git commit -a 84 | ``` 85 | Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files. 86 | 87 | * Push your branch to GitHub: 88 | 89 | ```shell 90 | git push origin my-fix-branch 91 | ``` 92 | 93 | * In GitHub, send a pull request to `master` branch. This will trigger the continuous integration and run the test. 94 | We will also notify you if you have not yet signed the [contribution agreement][cla]. 95 | 96 | * If you find that the continunous integration has failed, look into the logs to find out 97 | if your changes caused test failures, the commit message was malformed etc. If you find that the 98 | tests failed or times out for unrelated reasons, you can ping a team member so that the build can be 99 | restarted. 100 | 101 | * If we suggest changes, then: 102 | 103 | * Make the required updates. 104 | * Test your changes and test cases. 105 | * Commit your changes to your branch (e.g. `my-fix-branch`). 106 | * Push the changes to your GitHub repository (this will update your Pull Request). 107 | 108 | You can also amend the initial commits and force push them to the branch. 109 | 110 | ```shell 111 | git rebase master -i 112 | git push origin my-fix-branch -f 113 | ``` 114 | 115 | This is generally easier to follow, but separate commits are useful if the Pull Request contains 116 | iterations that might be interesting to see side-by-side. 117 | 118 | That's it! Thank you for your contribution! 119 | 120 | ## Signing the Contributor License Agreement (CLA) 121 | 122 | Upon submitting a Pull Request, we will ask you to sign our CLA if you haven't done 123 | so before. It's a quick process, we promise, and you will be able to do it all online 124 | 125 | You can read [ProcessOne Contribution License Agreement][cla] in PDF. 126 | 127 | This is part of the legal framework of the open-source ecosystem that adds some red tape, 128 | but protects both the contributor and the company / foundation behind the project. It also 129 | gives us the option to relicense the code with a more permissive license in the future. 130 | 131 | 132 | [coc]: https://github.com/processone/go-erlang/blob/master/CODE_OF_CONDUCT.md 133 | [stackoverflow]: https://stackoverflow.com/ 134 | [github]: https://github.com/processone/go-erlang 135 | [github-issues]: https://github.com/processone/go-erlang/issues 136 | [github-new-issue]: https://github.com/processone/go-erlang/issues/new 137 | [github-pr]: https://github.com/processone/go-erlang/pulls 138 | [cla]: https://www.process-one.net/resources/ejabberd-cla.pdf 139 | [license]: https://github.com/processone/go-erlang/blob/master/LICENSE 140 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go-Erlang 2 | 3 | Go-Erlang is a set of tools for Go <-> Erlang interoperability. 4 | 5 | The core of the library is the Erlang External Term Format. It is the internal format data to exchange Erlang terms 6 | over the network. This binary format is used for example in Erlang distribution protocol. 7 | 8 | ## Installation 9 | 10 | ## Usage 11 | 12 | ## BERT and BERT-RPC library for Go 13 | 14 | BERT library for Go is designed for simple data exchange between Go and Erlang/Elixir applications. 15 | 16 | BERT stands for Binary ERlang Term. It is a Remote Procedure Call mechanism to support interop between Erlang code\ 17 | and other programming languages. 18 | 19 | BERT library implements serialization and deserialization, as well as a subset / variation of the BERT-RPC protocol 20 | for Go. 21 | 22 | Here are the important points to note: 23 | - This version supports BERT-RPC over HTTP. It is not optimal for performance, but can rely on standard HTTP tooling 24 | features like connection pools, authentication, load balancing, etc. 25 | - This version implements the type I needed in Erlang External Term Format for interop with 26 | [ejabberd](https://github.com/processone/ejabberd/). 27 | 28 | ### Why use BERT? 29 | 30 | If you want to exchange data with Erlang node, it is handy to use a format that support all the Erlang types, including 31 | atoms. Without having the concept of atoms explicitly in the data exchange protocol, you end up adding wrapper tuples 32 | and conversions on the Erlang side that become very painful. 33 | 34 | If you do not need to interop with Erlang, we would recommend using Protobuf or MsgPack. 35 | 36 | ## TODO 37 | 38 | Support various transport for BERT-RPC client: 39 | - HTTP 40 | - TCP/IP 41 | - MQTT 42 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | ## Go module 4 | 5 | + Initial version for simple calls. 6 | - Rename repository and module to 'erlang', 'erl', 'gerl' or 'goei' (for Go <-> Erlang Interface) 7 | - Add support for slices / list 8 | - Add support for BigInt 9 | - Add support for Maps 10 | - Make BERP header (4 byte length) optional. BERP header is not needed on HTTP, as framing will be done at HTTP level. 11 | However, I need to consider if I should always add it for consistency. It would also allow grouping several calls 12 | in a single HTTP request. 13 | - Support zlib compression. 14 | - Add Server example. 15 | - Performance optimization. 16 | - Support new error package (Go 2). 17 | - Test and handle Erlang exceptions in function calls. 18 | - Give the ability to test against protocol error vs Erlang returned errors. 19 | 20 | ## ejabberd_rpc module 21 | 22 | - Support reading Erlang cookie to protect call behind bearer or basic auth 23 | - Add ability to configure whitelist of modules that admin is allowed to call through RPC. 24 | - Document configuration 25 | - Support overloading cookie to have specific credential for that RPC endpoint. 26 | - Add JWT token support 27 | - Support BERT-RPC over MQTT 28 | - TODO Improve help for clients: The client need to be able to retrieve a list of enabled modules to be able to display 29 | proper help with available commands. 30 | - High level Erlang API call: Allow using maps (or at least proplists). Make them easier to configure. 31 | - Support Unix socket (example Erlang usage: https://stackoverflow.com/a/38286954/559289) 32 | 33 | ## Generic Erlang / Elixir TCP server 34 | 35 | - Prepare Erlang module and ejabberd dependency: bert-server 36 | - Use Ranch as a dependency or start from scratch to avoid dependencies? 37 | 38 | ## Examples 39 | 40 | - Interop with Erlang and ejabberd 41 | - Interop with Elixir 42 | -------------------------------------------------------------------------------- /bertrpc/client.go: -------------------------------------------------------------------------------- 1 | package bertrpc // import "gosrc.io/erlang/bertrpc" 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // Client create an HTTP client to that holds configuration parameters to make Bert-RPC calls. 8 | type Client struct { 9 | // This is the endpoint used to access bert-rpc server 10 | // For now, we only support HTTP endpoints. 11 | Endpoint string 12 | // This is the security token used to pass call (HTTP bearer token auth) 13 | Token string 14 | 15 | // TODO: make httpclient configurable 16 | } 17 | 18 | // TODO: Support getting token for authentication. 19 | func New(endpoint string) Client { 20 | client := Client{Endpoint: endpoint} 21 | return client 22 | } 23 | 24 | // call is the internal structure to hold bert-rpc call parameters 25 | type call struct { 26 | module string 27 | function string 28 | args []interface{} 29 | } 30 | 31 | func (Client) NewCall(module string, function string, args ...interface{}) call { 32 | return call{module: module, function: function, args: args} 33 | } 34 | 35 | func (c Client) Exec(call call, result interface{}) error { 36 | // Prepare BERT-RPC Packet 37 | buf, err := EncodeCall(call.module, call.function, call.args...) 38 | 39 | if err != nil { 40 | return err 41 | } 42 | 43 | // Use HTTP POST to trigger BERT-RPC call over HTTP 44 | resp, err := http.Post(c.Endpoint, "application/bert", &buf) 45 | if err != nil { 46 | return err 47 | } 48 | defer resp.Body.Close() 49 | 50 | return DecodeReply(resp.Body, result) 51 | } 52 | -------------------------------------------------------------------------------- /bertrpc/decoder.go: -------------------------------------------------------------------------------- 1 | package bertrpc // import "gosrc.io/erlang/bertrpc" 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "reflect" 9 | ) 10 | 11 | var ErrRange = errors.New("value out of range") 12 | 13 | func Decode(r io.Reader, term interface{}) error { 14 | byte1 := make([]byte, 1) 15 | _, err := r.Read(byte1) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | // Read Erlang Term Format "magic byte" 21 | if byte1[0] != byte(TagETFVersion) { 22 | // Bad Version tag (aka 'magic number') 23 | return fmt.Errorf("incorrect Erlang Term version tag: %d", byte1[0]) 24 | } 25 | 26 | return decodeData(r, term) 27 | } 28 | 29 | func decodeData(r io.Reader, term interface{}) error { 30 | // Resolve pointers 31 | val := reflect.ValueOf(term) 32 | if val.Kind() == reflect.Ptr { 33 | val = val.Elem() 34 | } 35 | 36 | switch val.Kind() { 37 | 38 | case reflect.Int8: 39 | return ErrRange 40 | case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64: 41 | i, err := decodeInt(r) 42 | if err == nil { 43 | val.SetInt(i) 44 | } 45 | return err 46 | case reflect.String: 47 | s, err := decodeString(r) 48 | if err == nil { 49 | val.SetString(s) 50 | } 51 | return err 52 | case reflect.Struct: 53 | // Wrapper for basic types 54 | if val.Type().Name() == "String" { 55 | return decodeBertString(r, val) 56 | } 57 | return decodeStruct(r, val) 58 | 59 | default: 60 | return fmt.Errorf("unhandled decoding target: %s", val.Kind()) 61 | } 62 | } 63 | 64 | // ============================================================================ 65 | // Decode basic types 66 | 67 | // TODO: Pass bitsize here to trigger overflow operations errors 68 | func decodeInt(r io.Reader) (int64, error) { 69 | // Read Tag 70 | byte1 := make([]byte, 1) 71 | _, err := r.Read(byte1) 72 | if err != nil { 73 | return 0, err 74 | } 75 | 76 | // Compare expected type 77 | switch int(byte1[0]) { 78 | 79 | case TagSmallInteger: 80 | _, err = r.Read(byte1) 81 | if err != nil { 82 | return 0, err 83 | } 84 | return int64(byte1[0]), nil 85 | 86 | case TagInteger: 87 | byte4 := make([]byte, 4) 88 | n, err := r.Read(byte4) 89 | if err != nil { 90 | return 0, err 91 | } 92 | if n < 4 { 93 | return 0, fmt.Errorf("cannot decode integer, only %d bytes read", n) 94 | } 95 | var32 := int32(binary.BigEndian.Uint32(byte4)) 96 | return int64(var32), nil 97 | } 98 | 99 | return 0, fmt.Errorf("incorrect type") 100 | } 101 | 102 | // We can decode several Erlang types in a string: Atom (Deprecated), AtomUTF8, Binary, CharList. 103 | func decodeString(r io.Reader) (string, error) { 104 | // Read Tag 105 | byte1 := make([]byte, 1) 106 | _, err := r.Read(byte1) 107 | if err != nil { 108 | return "", err 109 | } 110 | 111 | // Compare expected type 112 | dataType := int(byte1[0]) 113 | switch dataType { 114 | 115 | case TagSmallAtomUTF8: 116 | data, err := decodeString1(r) 117 | return string(data), err 118 | 119 | case TagDeprecatedAtom, TagAtomUTF8, TagString: 120 | data, err := decodeString2(r) 121 | return string(data), err 122 | 123 | case TagBinary: 124 | data, err := decodeString4(r) 125 | return string(data), err 126 | 127 | case TagList: 128 | data, err := decodeCharList(r) 129 | return string(data), err 130 | } 131 | 132 | return "", fmt.Errorf("incorrect type: %d", dataType) 133 | } 134 | 135 | func decodeString1(r io.Reader) ([]byte, error) { 136 | // Length: 137 | byte1 := make([]byte, 1) 138 | _, err := r.Read(byte1) 139 | if err != nil { 140 | return []byte{}, err 141 | } 142 | length := int(byte1[0]) 143 | 144 | // Content: 145 | data := make([]byte, length) 146 | n, err := r.Read(data) 147 | if err != nil && err != io.EOF { 148 | return []byte{}, err 149 | } 150 | if n < length { 151 | return []byte{}, fmt.Errorf("truncated data") 152 | } 153 | return data, nil 154 | 155 | } 156 | 157 | // Decode a string with length on 16 bits. 158 | func decodeString2(r io.Reader) ([]byte, error) { 159 | // Length: 160 | l := make([]byte, 2) 161 | _, err := r.Read(l) 162 | if err != nil { 163 | return []byte{}, err 164 | } 165 | length := int(binary.BigEndian.Uint16(l)) 166 | 167 | // Content: 168 | data := make([]byte, length) 169 | n, err := r.Read(data) 170 | if err != nil && err != io.EOF { 171 | return []byte{}, err 172 | } 173 | if n < length { 174 | return []byte{}, fmt.Errorf("truncated data") 175 | } 176 | 177 | return data, nil 178 | } 179 | 180 | // Decode a string with length on 32 bits. 181 | func decodeString4(r io.Reader) ([]byte, error) { 182 | // Length: 183 | l := make([]byte, 4) 184 | _, err := r.Read(l) 185 | if err != nil { 186 | return []byte{}, err 187 | } 188 | length := int(binary.BigEndian.Uint32(l)) 189 | 190 | // Content: 191 | data := make([]byte, length) 192 | n, err := r.Read(data) 193 | if err != nil && err != io.EOF { 194 | return []byte{}, err 195 | } 196 | if n < length { 197 | return []byte{}, fmt.Errorf("truncated data") 198 | } 199 | 200 | return data, nil 201 | } 202 | 203 | // Decode a string with length on 32 bits. 204 | func decodeCharList(r io.Reader) ([]rune, error) { 205 | // Count: 206 | byte4 := make([]byte, 4) 207 | n, err := r.Read(byte4) 208 | if err != nil { 209 | return []rune{}, err 210 | } 211 | if n < 4 { 212 | return []rune{}, fmt.Errorf("truncated List data") 213 | } 214 | count := int(binary.BigEndian.Uint32(byte4)) 215 | 216 | s := []rune("") 217 | // Last element in list should be termination marker, so we loop (count - 1) times 218 | for i := 1; i <= count; i++ { 219 | // Assumption: We are decoding a into a string, so we expect all elements to be integers; 220 | // We can fail otherwise. 221 | char, err := decodeInt(r) 222 | if err != nil { 223 | return []rune{}, err 224 | } 225 | // Erlang does not encode utf8 charlist into a series of bytes, but use large integers. 226 | // We need to process the integer list as runes. 227 | s = append(s, rune(char)) 228 | } 229 | // Check that we have the list termination mark 230 | if err := decodeNil(r); err != nil { 231 | return s, err 232 | } 233 | 234 | return s, nil 235 | } 236 | 237 | func decodeBertString(r io.Reader, val reflect.Value) error { 238 | // Read Tag 239 | byte1 := make([]byte, 1) 240 | _, err := r.Read(byte1) 241 | if err != nil { 242 | return err 243 | } 244 | 245 | var strValue string 246 | var strType int 247 | 248 | // Compare expected type 249 | dataType := int(byte1[0]) 250 | switch dataType { 251 | 252 | case TagSmallAtomUTF8: 253 | data, err := decodeString1(r) 254 | if err != nil { 255 | return err 256 | } 257 | strValue = string(data) 258 | strType = StringTypeAtom 259 | 260 | case TagDeprecatedAtom, TagAtomUTF8: 261 | data, err := decodeString2(r) 262 | if err != nil { 263 | return err 264 | } 265 | strValue = string(data) 266 | strType = StringTypeAtom 267 | 268 | case TagString: 269 | data, err := decodeString2(r) 270 | if err != nil { 271 | return err 272 | } 273 | strValue = string(data) 274 | strType = StringTypeString 275 | 276 | case TagBinary: 277 | data, err := decodeString4(r) 278 | if err != nil { 279 | return err 280 | } 281 | strValue = string(data) 282 | strType = StringTypeString 283 | 284 | case TagList: 285 | data, err := decodeCharList(r) 286 | if err != nil { 287 | return err 288 | } 289 | strValue = string(data) 290 | strType = StringTypeString 291 | 292 | default: 293 | return fmt.Errorf("cannot decode %s to bert.String", tagName(dataType)) 294 | } 295 | 296 | field := val.FieldByName("Value") 297 | field.SetString(strValue) 298 | field = val.FieldByName("ErlangType") 299 | field.SetInt(int64(strType)) 300 | 301 | return nil 302 | } 303 | 304 | // Read a nil value and return error in case of unexpected value. 305 | // Nil is expected as a marker for end of lists. 306 | func decodeNil(r io.Reader) error { 307 | // Read Tag 308 | byte1 := make([]byte, 1) 309 | _, err := r.Read(byte1) 310 | if err != nil { 311 | return err 312 | } 313 | 314 | if byte1[0] != byte(TagNil) { 315 | return fmt.Errorf("could not find nil: %d", byte1[0]) 316 | } 317 | 318 | return nil 319 | } 320 | -------------------------------------------------------------------------------- /bertrpc/decoder_bert.go: -------------------------------------------------------------------------------- 1 | package bertrpc // import "gosrc.io/erlang/bertrpc" 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "reflect" 9 | ) 10 | 11 | var ErrReturn = errors.New("function returns 'error'") 12 | 13 | // A Bert call reply is either: 14 | // {reply, Result} 15 | // {error, {Type, Code, Class, Detail, Backtrace}} 16 | // If we pass an empty struct it means we do not care about the reply and we will not try to decode 17 | // Erlang return. 18 | func DecodeReply(r io.Reader, term interface{}) error { 19 | // Guard against nil decoding target as it does not guide the decoding 20 | if term == nil { 21 | return fmt.Errorf("target type for decoding cannot be nil") 22 | } 23 | 24 | // 1. Read BERP length 25 | byte4 := make([]byte, 4) 26 | n, err := r.Read(byte4) 27 | if err != nil { 28 | return err 29 | } 30 | if n < 4 { 31 | return fmt.Errorf("truncated data") 32 | } 33 | // TODO: Keep track of the length of the data read, to be able to skip to the end on failure. 34 | _ = int(binary.BigEndian.Uint32(byte4)) 35 | 36 | // 2. Read Erlang Term Format "magic byte" 37 | byte1 := make([]byte, 1) 38 | _, err = r.Read(byte1) 39 | if err != nil { 40 | return err 41 | } 42 | if byte1[0] != byte(TagETFVersion) { 43 | // Bad Version tag (aka 'magic number') 44 | return fmt.Errorf("incorrect Erlang Term version tag: %d", byte1[0]) 45 | } 46 | 47 | // 3. Read the reply tuple header 48 | length, err := readTupleInfo(r) 49 | if err != nil { 50 | return err 51 | } 52 | if length != 2 { 53 | return errors.New("unexpected bert reply tuple size") 54 | } 55 | 56 | // 4. Read the first Atom 57 | tag, err := readAtom(r) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | // 5. Decode the reply or the error 63 | switch tag { 64 | case "reply": 65 | // Read the result of the function call 66 | if err := decodeData(r, term); err != nil { 67 | return err 68 | } 69 | 70 | return nil 71 | case "error": 72 | // TODO Decode Bert Error and add test on errors 73 | return errors.New("TODO Decode Bert error") 74 | default: 75 | return fmt.Errorf("incorrect reply tag: %s", tag) 76 | } 77 | } 78 | 79 | // ============================================================================ 80 | // Decode Erlang Term format into a Go structure 81 | 82 | // TODO ignore unexported fields 83 | func decodeStruct(r io.Reader, val reflect.Value) error { 84 | // If the struct is empty, we assume caller is not interested in the result 85 | // and we do not try to decode anything. 86 | if val.NumField() == 0 { 87 | return nil 88 | } 89 | 90 | // Get the first field of the interface we are decoding to, to determine 91 | // if we are decoding a target value. 92 | // It must be a string and be tagged as erlang:"tag" 93 | structType := val.Type() 94 | field1 := structType.Field(0) 95 | tag, ok := field1.Tag.Lookup("erlang") 96 | if ok && tag == "tag" && field1.Type.Kind() == reflect.String { 97 | return decodeTaggedValue(r, val) 98 | } 99 | return decodeUntaggedStruct(r, val) 100 | } 101 | 102 | func decodeTaggedValue(r io.Reader, val reflect.Value) error { 103 | // We need to read Erlang data type. If we have an atom, it will be the tag. 104 | // If we have a tuple, We expect first element to be the tag. 105 | // If we have something else, we try to decode it in an untagged field. 106 | // Read the type of data 107 | byte1 := make([]byte, 1) 108 | _, err := r.Read(byte1) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | switch int(byte1[0]) { 114 | // We are directly decoding the tag, return it inside the struct: 115 | case TagDeprecatedAtom, TagAtomUTF8, TagSmallAtomUTF8: 116 | return readTagAtom(r, int(byte1[0]), val) 117 | case TagSmallTuple, TagLargeTuple: 118 | return readTagTuple(r, int(byte1[0]), val) 119 | } 120 | // We did not find any field to decode the tag to 121 | return fmt.Errorf("decodeTaggedValue could not read atom or taggedTuple") 122 | } 123 | 124 | func readTagAtom(r io.Reader, erlangType int, val reflect.Value) error { 125 | switch erlangType { 126 | // We are directly decoding the tag, return it inside the struct: 127 | case TagDeprecatedAtom, TagAtomUTF8: 128 | data, err := decodeString2(r) 129 | if err != nil { 130 | return err 131 | } 132 | field1 := val.Field(0) 133 | field1.SetString(string(data)) 134 | return nil 135 | case TagSmallAtomUTF8: 136 | data, err := decodeString1(r) 137 | if err != nil { 138 | return err 139 | } 140 | field1 := val.Field(0) 141 | field1.SetString(string(data)) 142 | return nil 143 | default: 144 | return fmt.Errorf("readTagAtom unexpected mismatch: %d", erlangType) 145 | } 146 | } 147 | 148 | func readTagTuple(r io.Reader, erlangType int, val reflect.Value) error { 149 | // Get tuple length 150 | byte1 := make([]byte, 1) 151 | length := 0 152 | switch erlangType { 153 | case TagSmallTuple: 154 | _, err := r.Read(byte1) 155 | if err != nil { 156 | return err 157 | } 158 | length = int(byte1[0]) 159 | case TagLargeTuple: 160 | byte4 := make([]byte, 4) 161 | n, err := r.Read(byte4) 162 | if err != nil { 163 | return err 164 | } 165 | if n < 4 { 166 | return fmt.Errorf("truncated data") 167 | } 168 | length = int(binary.BigEndian.Uint32(byte4)) 169 | default: 170 | return fmt.Errorf("readTagTuple unexpected mismatch: %d", erlangType) 171 | } 172 | 173 | // An empty tuple cannot have a tag 174 | if length == 0 { 175 | return fmt.Errorf("tag cannot be found in an empty tuple") 176 | } 177 | 178 | // Extract first field as tag 179 | data, err := readAtom(r) 180 | tag := string(data) 181 | if err != nil { 182 | return fmt.Errorf("cannot read atom as first tuple element") 183 | } 184 | field1 := val.Field(0) 185 | field1.SetString(tag) 186 | 187 | // Match all others fields against the tag name constraint to decode the fields one by one 188 | structType := val.Type() 189 | for i := 1; i < structType.NumField(); i++ { 190 | field := structType.Field(i) 191 | if t, ok := field.Tag.Lookup("erlang"); ok { 192 | if t == "tag:"+tag { 193 | currField := val.Field(i) 194 | if currField.Kind() == reflect.Ptr { 195 | currField = currField.Elem() 196 | } 197 | if currField.CanAddr() { 198 | err := decodeData(r, currField.Addr().Interface()) 199 | if err != nil { 200 | return err 201 | } 202 | } 203 | } 204 | } 205 | } 206 | return nil 207 | } 208 | 209 | /* 210 | func readOtherData(r io.Reader, tagName int, val reflect.Value) error { 211 | if val.Kind() == reflect.Ptr { 212 | val = val.Elem() 213 | } 214 | 215 | switch val.Kind() { 216 | 217 | case reflect.Int8: 218 | return ErrRange 219 | case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64: 220 | i, err := decodeInt(r) // TODO Point to partial decodeInt, passing the Erlang type that was already read 221 | if err == nil { 222 | val.SetInt(i) 223 | } 224 | return err 225 | case reflect.String: 226 | s, err := decodeString(r) // TODO Point to partial decodeString, passing the Erlang type that was already read 227 | if err == nil { 228 | val.SetString(s) 229 | } 230 | return err 231 | 232 | default: 233 | return fmt.Errorf("readOtherData unexpected mismatch: %s", val.Kind()) 234 | } 235 | } 236 | */ 237 | 238 | // ============================================================================ 239 | 240 | func decodeUntaggedStruct(r io.Reader, val reflect.Value) error { 241 | // 1. Get the Erlang type of the tuple 242 | byte1 := make([]byte, 1) 243 | _, err := r.Read(byte1) 244 | if err != nil { 245 | return err 246 | } 247 | 248 | length := 0 249 | switch int(byte1[0]) { 250 | case TagSmallTuple: 251 | _, err := r.Read(byte1) 252 | if err != nil { 253 | return err 254 | } 255 | length = int(byte1[0]) 256 | case TagLargeTuple: 257 | byte4 := make([]byte, 4) 258 | n, err := r.Read(byte4) 259 | if err != nil { 260 | return err 261 | } 262 | if n < 4 { 263 | return fmt.Errorf("truncated data") 264 | } 265 | length = int(binary.BigEndian.Uint32(byte4)) 266 | 267 | default: 268 | return fmt.Errorf("cannot decode type %s to struct %s", tagName(int(byte1[0])), val.Type()) 269 | } 270 | 271 | return decodeStructElts(r, length, val) 272 | } 273 | 274 | func decodeStructElts(r io.Reader, length int, val reflect.Value) error { 275 | // If the tuple does not contain the expected number of fields in our struct 276 | if length != val.NumField() { 277 | return fmt.Errorf("cannot decode tuple of length %d to struct", length) 278 | } 279 | 280 | // For each field, try to decode it recursively 281 | for i := 0; i < length; i++ { 282 | valueField := val.Field(i) 283 | if valueField.Kind() == reflect.Ptr { 284 | valueField = valueField.Elem() 285 | } 286 | if valueField.CanAddr() { 287 | err := decodeData(r, valueField.Addr().Interface()) 288 | if err != nil { 289 | return err 290 | } 291 | } 292 | } 293 | return nil 294 | } 295 | 296 | // ============================================================================ 297 | // Helpers 298 | 299 | // Verify that we are reading a tuple and return the length of the tuple 300 | func readTupleInfo(r io.Reader) (int, error) { 301 | // 1. Read the type of data 302 | byte1 := make([]byte, 1) 303 | _, err := r.Read(byte1) 304 | if err != nil { 305 | return 0, err 306 | } 307 | 308 | // 2. Return 309 | tupleLength := 0 310 | switch int(byte1[0]) { 311 | case TagSmallTuple: 312 | _, err := r.Read(byte1) 313 | if err != nil { 314 | return 0, err 315 | } 316 | tupleLength = int(byte1[0]) 317 | case TagLargeTuple: 318 | byte4 := make([]byte, 4) 319 | n, err := r.Read(byte4) 320 | if err != nil { 321 | return 0, err 322 | } 323 | if n < 4 { 324 | return 0, fmt.Errorf("truncated data") 325 | } 326 | tupleLength = int(binary.BigEndian.Uint32(byte4)) 327 | 328 | default: 329 | return 0, fmt.Errorf("cannot decode type %d to struct", int(byte1[0])) 330 | } 331 | 332 | return tupleLength, nil 333 | } 334 | 335 | func readAtom(r io.Reader) (string, error) { 336 | // Read the type of data 337 | byte1 := make([]byte, 1) 338 | _, err := r.Read(byte1) 339 | if err != nil { 340 | return "", err 341 | } 342 | 343 | switch int(byte1[0]) { 344 | case TagDeprecatedAtom, TagAtomUTF8: 345 | data, err := decodeString2(r) 346 | if err != nil { 347 | return "", err 348 | } 349 | return string(data), nil 350 | case TagSmallAtomUTF8: 351 | data, err := decodeString1(r) 352 | if err != nil { 353 | return "", err 354 | } 355 | return string(data), nil 356 | 357 | default: 358 | return "", fmt.Errorf("cannot decode type %d as atom", int(byte1[0])) 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /bertrpc/decoder_bert_test.go: -------------------------------------------------------------------------------- 1 | package bertrpc_test // import "gosrc.io/erlang/bertrpc_test" 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "gosrc.io/erlang/bertrpc" 8 | ) 9 | 10 | // TODO: Refactor the test to work with both the Erlang raw term format and the Bert reply packet 11 | func TestDecodeSimple(t *testing.T) { 12 | // {reply, valid} 13 | input := []byte{0, 0, 0, 19, 131, 104, 2, 100, 0, 5, 114, 101, 112, 108, 121, 100, 0, 5, 118, 97, 14 | 108, 105, 100} 15 | 16 | var result struct { 17 | Value string `erlang:"tag"` 18 | } 19 | 20 | buf := bytes.NewBuffer(input) 21 | err := bertrpc.DecodeReply(buf, &result) 22 | if err != nil { 23 | t.Errorf("bert decoding failed: %s", err) 24 | return 25 | } 26 | if result.Value != "valid" { 27 | t.Errorf("unexpected result: %s", result.Value) 28 | } 29 | } 30 | 31 | func TestDecodeErrorReply(t *testing.T) { 32 | // {reply, {error, exists}} 33 | input := []byte{0, 0, 0, 30, 131, 104, 2, 100, 0, 5, 114, 101, 112, 108, 121, 104, 2, 100, 0, 5, 101, 114, 114, 111, 114, 100, 34 | 0, 6, 101, 120, 105, 115, 116, 115} 35 | 36 | var result struct { 37 | Tag string `erlang:"tag"` 38 | Reason string `erlang:"tag:error"` 39 | Result string `erlang:"tag:ok"` 40 | } 41 | buf := bytes.NewBuffer(input) 42 | err := bertrpc.DecodeReply(buf, &result) 43 | if err != nil { 44 | t.Errorf("bert decoding failed: %s", err) 45 | return 46 | } 47 | 48 | if result.Tag != "error" { 49 | t.Errorf("unexpected tag value: '%s'", result.Tag) 50 | } 51 | if result.Reason != "exists" { 52 | t.Errorf("unexpected error reason: '%s'", result.Reason) 53 | } 54 | if result.Result != "" { 55 | t.Errorf("result is expected to be empty: '%s'", result.Result) 56 | } 57 | } 58 | 59 | func TestDecodeOkReply(t *testing.T) { 60 | // {reply, {ok, 110}} 61 | input := []byte{0, 0, 0, 20, 131, 104, 2, 100, 0, 5, 114, 101, 112, 108, 121, 104, 2, 100, 0, 2, 111, 107, 62 | 97, 110} 63 | 64 | var result struct { 65 | Tag string `erlang:"tag"` 66 | Reason string `erlang:"tag:error"` 67 | Count int `erlang:"tag:ok"` 68 | } 69 | buf := bytes.NewBuffer(input) 70 | err := bertrpc.DecodeReply(buf, &result) 71 | if err != nil { 72 | t.Errorf("bert decoding failed: %s", err) 73 | return 74 | } 75 | if result.Count != 110 { 76 | t.Errorf("unexpected count value: %d (%d)", result.Count, 110) 77 | } 78 | if result.Reason != "" { 79 | t.Errorf("reason is expected to be empty: '%s'", result.Reason) 80 | } 81 | } 82 | 83 | func TestDecodeReplyToNil(t *testing.T) { 84 | // {reply, {ok, 110}} 85 | input := []byte{0, 0, 0, 20, 131, 104, 2, 100, 0, 5, 114, 101, 112, 108, 121, 104, 2, 100, 0, 2, 111, 107, 86 | 97, 110} 87 | 88 | buf := bytes.NewBuffer(input) 89 | err := bertrpc.DecodeReply(buf, nil) 90 | if err == nil { 91 | t.Errorf("bert decoding to nil should fail") 92 | } 93 | } 94 | 95 | func TestDecodeOkStruct(t *testing.T) { 96 | // {reply, {"t1@localhost", "t2@localhost"}} 97 | input := []byte{0, 0, 0, 47, 131, 104, 2, 100, 0, 5, 114, 101, 112, 108, 121, 104, 2, 109, 0, 0, 0, 12, 98 | 116, 49, 64, 108, 111, 99, 97, 108, 104, 111, 115, 116, 109, 0, 0, 0, 12, 116, 99 | 50, 64, 108, 111, 99, 97, 108, 104, 111, 115, 116} 100 | 101 | var result struct { 102 | From string 103 | To string 104 | } 105 | buf := bytes.NewBuffer(input) 106 | err := bertrpc.DecodeReply(buf, &result) 107 | if err != nil { 108 | t.Errorf("bert decoding failed: %s", err) 109 | return 110 | } 111 | 112 | if result.From != "t1@localhost" { 113 | t.Errorf("incorrect from: %s", result.From) 114 | } 115 | if result.To != "t2@localhost" { 116 | t.Errorf("incorrect from: %s", result.To) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /bertrpc/decoder_test.go: -------------------------------------------------------------------------------- 1 | package bertrpc_test // import "gosrc.io/erlang/bertrpc_test" 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "gosrc.io/erlang/bertrpc" 9 | ) 10 | 11 | // Small Erlang Term type is Uint8. It cannot fit into an int8 12 | func TestDecodeInt8(t *testing.T) { 13 | var i int8 14 | buf := bytes.NewBuffer([]byte{131, 97, 255}) 15 | if err := bertrpc.Decode(buf, &i); err != bertrpc.ErrRange { 16 | t.Errorf("Decoding an Erlang small integer into int8 should fail") 17 | } 18 | } 19 | 20 | func TestDecodeInt(t *testing.T) { 21 | tests := []struct { 22 | input []byte 23 | want int64 24 | }{ 25 | {input: []byte{131, 97, 42}, want: 42}, 26 | {input: []byte{131, 97, 255}, want: 255}, 27 | {input: []byte{131, 98, 255, 255, 255, 0}, want: -256}, 28 | {input: []byte{131, 98, 0, 0, 1, 0}, want: 256}, 29 | {input: []byte{131, 98, 128, 0, 0, 0}, want: -2147483648}, 30 | {input: []byte{131, 98, 127, 255, 255, 255}, want: 2147483647}, 31 | } 32 | 33 | for _, tc := range tests { 34 | var i int 35 | buf := bytes.NewBuffer(tc.input) 36 | if err := bertrpc.Decode(buf, &i); err != nil { 37 | t.Errorf("cannot decode Erlang term: %s", err) 38 | return 39 | } 40 | 41 | if int64(i) != tc.want { 42 | t.Errorf("incorrect decoded value: %d. expected: %d", i, tc.want) 43 | } 44 | } 45 | } 46 | 47 | // TODO: Implement decode same types to []byte and bert.Atom 48 | func TestDecodeToString(t *testing.T) { 49 | longUTF8 := strings.Repeat("🖖", 64) 50 | tests := []struct { 51 | input []byte 52 | want string 53 | }{ 54 | {input: []byte{131, 100, 0, 0}, want: ""}, 55 | {input: []byte{131, 100, 0, 2, 111, 107}, want: "ok"}, 56 | {input: []byte{131, 119, 4, 240, 159, 150, 150}, want: "🖖"}, 57 | {input: append([]byte{131, 118, 1, 0}, []byte(longUTF8)...), want: longUTF8}, 58 | {input: []byte{131, 107, 0, 5, 72, 101, 108, 108, 111}, want: "Hello"}, 59 | {input: []byte{131, 109, 0, 0, 0, 5, 72, 101, 108, 108, 111}, want: "Hello"}, 60 | {input: []byte{131, 109, 0, 0, 0, 10, 240, 159, 150, 150, 32, 72, 101, 108, 108, 111}, want: "🖖 Hello"}, 61 | {input: []byte{131, 108, 0, 0, 0, 3, 98, 0, 1, 245, 150, 97, 72, 97, 105, 106}, want: "🖖Hi"}, 62 | } 63 | 64 | for _, tc := range tests { 65 | var a string 66 | buf := bytes.NewBuffer(tc.input) 67 | if err := bertrpc.Decode(buf, &a); err != nil { 68 | t.Errorf("cannot decode Erlang term: %s", err) 69 | return 70 | } 71 | 72 | if a != tc.want { 73 | t.Errorf("incorrect decoded value: %#v. expected: %#v", a, tc.want) 74 | } 75 | } 76 | } 77 | 78 | func TestDecodeEmptyTuple(t *testing.T) { 79 | input := []byte{131, 104, 0} 80 | want := struct{}{} 81 | 82 | var tuple struct{} 83 | buf := bytes.NewBuffer(input) 84 | if err := bertrpc.Decode(buf, &tuple); err != nil { 85 | t.Errorf("cannot decode Erlang term: %s", err) 86 | return 87 | } 88 | 89 | if tuple != want { 90 | t.Errorf("cannot decode empty tuple: %v", tuple) 91 | } 92 | } 93 | 94 | // Decode a tuple with two elements. 95 | func TestDecodeTuple2(t *testing.T) { 96 | input := []byte{131, 104, 2, 100, 0, 5, 101, 114, 114, 111, 114, 100, 0, 9, 110, 111, 97 | 116, 95, 102, 111, 117, 110, 100} 98 | want := struct { 99 | Result string 100 | Reason string 101 | }{"error", "not_found"} 102 | 103 | var tuple struct { 104 | Result string 105 | Reason string 106 | } 107 | buf := bytes.NewBuffer(input) 108 | if err := bertrpc.Decode(buf, &tuple); err != nil { 109 | t.Errorf("cannot decode Erlang term: %s", err) 110 | return 111 | } 112 | 113 | if tuple != want { 114 | t.Errorf("cannot decode empty tuple: %v", tuple) 115 | } 116 | } 117 | 118 | func TestFailOnLengthMismatch(t *testing.T) { 119 | input := []byte{131, 104, 2, 100, 0, 5, 101, 114, 114, 111, 114, 100, 0, 9, 110, 111, 120 | 116, 95, 102, 111, 117, 110, 100} 121 | 122 | var tuple struct { 123 | Result string 124 | Reason string 125 | Extra string 126 | } 127 | buf := bytes.NewBuffer(input) 128 | if err := bertrpc.Decode(buf, &tuple); err == nil { 129 | t.Errorf("decoding tuple into struct with different number of field should fail") 130 | } 131 | } 132 | 133 | type result1 struct { 134 | Tag string `erlang:"tag"` 135 | Result string `erlang:"tag:ok"` 136 | Reason string `erlang:"tag:error"` 137 | } 138 | 139 | func TestDecodeResult(t *testing.T) { 140 | tests := []struct { 141 | name string 142 | input []byte 143 | want result1 144 | }{ 145 | // Erlang function returns: 146 | {name: "ok", input: []byte{131, 100, 0, 2, 111, 107}, want: result1{Tag: "ok"}}, 147 | {name: "error", input: []byte{131, 100, 0, 5, 101, 114, 114, 111, 114}, want: result1{Tag: "error"}}, 148 | {name: "info", input: []byte{131, 100, 0, 4, 105, 110, 102, 111}, want: result1{Tag: "info"}}, 149 | {name: "{ok, Result}", input: []byte{131, 104, 2, 100, 0, 2, 111, 107, 100, 0, 5, 102, 111, 117, 110, 100}, 150 | want: result1{Tag: "ok", Result: "found"}}, 151 | {name: "{error, Reason}", input: []byte{131, 104, 2, 100, 0, 5, 101, 114, 114, 111, 114, 100, 0, 9, 110, 111, 116, 95, 152 | 102, 111, 117, 110, 100}, want: result1{Tag: "error", Reason: "not_found"}}, 153 | } 154 | 155 | for _, tc := range tests { 156 | t.Run(tc.name, func(st *testing.T) { 157 | var res result1 158 | buf := bytes.NewBuffer(tc.input) 159 | 160 | if err := bertrpc.Decode(buf, &res); err != nil { 161 | st.Errorf("cannot decode function call result: %s", err) 162 | return 163 | } 164 | 165 | if tc.want.Tag != res.Tag { 166 | st.Errorf("incorrect Tag: %v (!= %v)", res.Tag, tc.want.Tag) 167 | } 168 | if tc.want.Result != res.Result { 169 | st.Errorf("incorrect Result: %v (!= %v)", res.Result, tc.want.Result) 170 | } 171 | if tc.want.Reason != res.Reason { 172 | st.Errorf("incorrect Reason: %v (!= %v)", res.Reason, tc.want.Reason) 173 | } 174 | }) 175 | } 176 | } 177 | 178 | func TestDecodeTupleResult(t *testing.T) { 179 | input := []byte{131, 104, 4, 97, 1, 97, 2, 97, 3, 97, 4} 180 | want := struct { 181 | A int 182 | B int 183 | C int 184 | D int 185 | }{1, 2, 3, 4} 186 | 187 | var tuple struct { 188 | A int 189 | B int 190 | C int 191 | D int 192 | } 193 | buf := bytes.NewBuffer(input) 194 | 195 | if err := bertrpc.Decode(buf, &tuple); err != nil { 196 | t.Errorf("cannot decode Erlang term: %s", err) 197 | return 198 | } 199 | 200 | if tuple != want { 201 | t.Errorf("result does not match expectation: %v", tuple) 202 | } 203 | } 204 | 205 | // We have a bert.String type that allow developer to know if the return struct was an atom when this matters. 206 | // For example, it can be use to make a difference between the atom result not_found and the value "not_found". 207 | func TestDecodeAtomVsString(t *testing.T) { 208 | tests := []struct { 209 | name string 210 | input []byte 211 | want bertrpc.String 212 | }{ 213 | {name: "false as atom", input: []byte{131, 100, 0, 5, 102, 97, 108, 115, 101}, want: bertrpc.A("false")}, 214 | {name: "false as result", input: []byte{131, 107, 0, 5, 102, 97, 108, 115, 101}, want: bertrpc.S("false")}, 215 | } 216 | 217 | for _, tc := range tests { 218 | t.Run(tc.name, func(st *testing.T) { 219 | var res bertrpc.String 220 | buf := bytes.NewBuffer(tc.input) 221 | 222 | if err := bertrpc.Decode(buf, &res); err != nil { 223 | st.Errorf("cannot decode function call result: %s", err) 224 | return 225 | } 226 | 227 | if tc.want != res { 228 | st.Errorf("incorrect result: %#v (!= %#v)", res, tc.want) 229 | } 230 | }) 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /bertrpc/encoder.go: -------------------------------------------------------------------------------- 1 | package bertrpc // import "gosrc.io/erlang/bertrpc" 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "reflect" 8 | ) 9 | 10 | // Encode serializes a term as a ETF structure 11 | func Encode(term interface{}) ([]byte, error) { 12 | var buf bytes.Buffer 13 | if err := EncodeTo(term, &buf); err != nil { 14 | return []byte{}, err 15 | } 16 | return buf.Bytes(), nil 17 | } 18 | 19 | // Use Erlang External Term Format 20 | // Reference: http://erlang.org/doc/apps/erts/erl_ext_dist.html 21 | func EncodeTo(term interface{}, buf *bytes.Buffer) error { 22 | // Header for External Erlang Term Format 23 | buf.Write([]byte{TagETFVersion}) 24 | 25 | // Encode the data 26 | if err := encodePayloadTo(term, buf); err != nil { 27 | return err 28 | } 29 | return nil 30 | } 31 | 32 | func encodePayloadTo(term interface{}, buf *bytes.Buffer) error { 33 | var err error 34 | switch t := term.(type) { 35 | 36 | case String: 37 | if t.ErlangType == StringTypeAtom { 38 | err = encodeAtom(buf, t.Value) 39 | } else { 40 | err = encodeString(buf, t.Value) 41 | } 42 | 43 | case string: 44 | err = encodeString(buf, t) 45 | 46 | case int: 47 | err = encodeInt(buf, int32(t)) 48 | case int8: 49 | err = encodeInt(buf, int32(t)) 50 | case int16: 51 | err = encodeInt(buf, int32(t)) 52 | case int32: 53 | err = encodeInt(buf, t) 54 | case uint: 55 | err = encodeInt(buf, int32(t)) 56 | case uint8: 57 | err = encodeInt(buf, int32(t)) 58 | case uint16: 59 | err = encodeInt(buf, int32(t)) 60 | case uint32: 61 | err = encodeInt(buf, int32(t)) 62 | 63 | case Tuple: 64 | err = encodeTuple(buf, t) 65 | 66 | default: 67 | // Defines how to encode Go pointer types 68 | v := reflect.ValueOf(term) 69 | switch v.Kind() { 70 | case reflect.Slice: 71 | // TODO: handle reflect.Array 72 | var list []interface{} 73 | list, err = makeGenericSlice(term) 74 | if err != nil { 75 | err = fmt.Errorf("error converting slice: %v - %v:\n%v", v.Kind(), v.Type().Name(), err) 76 | break 77 | } 78 | err = encodeList(buf, list) 79 | default: 80 | err = fmt.Errorf("unhandled type: %v - %v", v.Kind(), v.Type().Name()) 81 | } 82 | } 83 | return err 84 | } 85 | 86 | func encodeAtom(buf *bytes.Buffer, str string) error { 87 | // Encode atom header 88 | if len(str) <= 255 { 89 | // Encode small UTF8 atom 90 | buf.WriteByte(TagSmallAtomUTF8) 91 | buf.WriteByte(byte(len(str))) 92 | } else { 93 | // Encode standard UTF8 atom 94 | buf.WriteByte(TagAtomUTF8) 95 | if err := binary.Write(buf, binary.BigEndian, uint16(len(str))); err != nil { 96 | return err 97 | } 98 | } 99 | 100 | // Write atom 101 | buf.WriteString(str) 102 | return nil 103 | } 104 | 105 | func encodeString(buf *bytes.Buffer, str string) error { 106 | buf.WriteByte(TagBinary) 107 | if err := binary.Write(buf, binary.BigEndian, uint32(len(str))); err != nil { 108 | return err 109 | } 110 | buf.WriteString(str) 111 | return nil 112 | } 113 | 114 | func encodeInt(buf *bytes.Buffer, i int32) error { 115 | if i >= 0 && i <= 255 { 116 | buf.WriteByte(TagSmallInteger) 117 | buf.WriteByte(byte(i)) 118 | } else { 119 | buf.WriteByte(TagInteger) 120 | if err := binary.Write(buf, binary.BigEndian, i); err != nil { 121 | return err 122 | } 123 | } 124 | return nil 125 | } 126 | 127 | func encodeTuple(buf *bytes.Buffer, tuple Tuple) error { 128 | // Tuple header 129 | size := len(tuple.Elems) 130 | if size <= 255 { 131 | // Encode small tuple 132 | buf.WriteByte(TagSmallTuple) 133 | buf.WriteByte(byte(size)) 134 | } else { 135 | // Encode large tuple 136 | buf.WriteByte(TagLargeTuple) 137 | if err := binary.Write(buf, binary.BigEndian, int32(size)); err != nil { 138 | return err 139 | } 140 | } 141 | 142 | // Tuple content 143 | for _, elem := range tuple.Elems { 144 | if err := encodePayloadTo(elem, buf); err != nil { 145 | return err 146 | } 147 | } 148 | return nil 149 | } 150 | 151 | func encodeList(buf *bytes.Buffer, list []interface{}) error { 152 | var err error 153 | // TODO: Special case for empty list: v.Len() ? Should not be needed 154 | 155 | // List header 156 | buf.WriteByte(TagList) 157 | if err := binary.Write(buf, binary.BigEndian, int32(len(list))); err != nil { 158 | return err 159 | } 160 | 161 | // List content 162 | for _, elem := range list { 163 | if err := encodePayloadTo(elem, buf); err != nil { 164 | return err 165 | } 166 | } 167 | // nil terminates the list: 168 | buf.Write([]byte{TagNil}) 169 | return err 170 | } 171 | 172 | // ============================================================================ 173 | // Helpers 174 | 175 | func makeGenericSlice(slice interface{}) ([]interface{}, error) { 176 | s := reflect.ValueOf(slice) 177 | switch s.Kind() { 178 | case reflect.Slice, reflect.Array: 179 | generic := make([]interface{}, s.Len()) 180 | 181 | for i := 0; i < s.Len(); i++ { 182 | generic[i] = s.Index(i).Interface() 183 | } 184 | 185 | return generic, nil 186 | default: 187 | return []interface{}{}, 188 | fmt.Errorf("cannot make a generic slice from something that is not a slice: %v", s.Kind()) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /bertrpc/encoder_test.go: -------------------------------------------------------------------------------- 1 | package bertrpc_test // import "gosrc.io/erlang/bertrpc_test" 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "gosrc.io/erlang/bertrpc" 8 | ) 9 | 10 | func TestEncodeSmallAtom(t *testing.T) { 11 | atom := bertrpc.A("atom") 12 | data, err := bertrpc.Encode(atom) 13 | if err != nil { 14 | t.Error(err) 15 | } 16 | // Use new utf8 atom (119), instead of old deprecated atom (100) 17 | expected := []byte{131, 119, 4, 97, 116, 111, 109} 18 | if !bytes.Equal(data, expected) { 19 | t.Errorf("EncodeSmallAtom: expected %v, actual %v", expected, data) 20 | } 21 | } 22 | 23 | // We encode strings to binary, but we can force them to charlist (see TestEncodeCharList) 24 | func TestEncodeString(t *testing.T) { 25 | data, err := bertrpc.Encode("string") 26 | if err != nil { 27 | t.Error(err) 28 | } 29 | expected := []byte{131, 109, 0, 0, 0, 6, 115, 116, 114, 105, 110, 103} 30 | if !bytes.Equal(data, expected) { 31 | t.Errorf("EncodeString: expected %v, actual %v", expected, data) 32 | } 33 | } 34 | 35 | func TestEncodeInt(t *testing.T) { 36 | var tests = []struct { 37 | n int 38 | expected []byte 39 | }{ 40 | {-2147483648, []byte{bertrpc.TagETFVersion, bertrpc.TagInteger, 128, 0, 0, 0}}, 41 | {-1, []byte{bertrpc.TagETFVersion, bertrpc.TagInteger, 255, 255, 255, 255}}, 42 | {1, []byte{bertrpc.TagETFVersion, bertrpc.TagSmallInteger, 1}}, 43 | {42, []byte{bertrpc.TagETFVersion, bertrpc.TagSmallInteger, 42}}, 44 | {255, []byte{bertrpc.TagETFVersion, bertrpc.TagSmallInteger, 255}}, 45 | {256, []byte{bertrpc.TagETFVersion, bertrpc.TagInteger, 0, 0, 1, 0}}, 46 | {1000, []byte{bertrpc.TagETFVersion, bertrpc.TagInteger, 0, 0, 3, 232}}, 47 | {2147483647, []byte{bertrpc.TagETFVersion, bertrpc.TagInteger, 127, 255, 255, 255}}, 48 | } 49 | 50 | for _, tt := range tests { 51 | data, err := bertrpc.Encode(tt.n) 52 | if err != nil { 53 | t.Error(err) 54 | } 55 | if !bytes.Equal(data, tt.expected) { 56 | t.Errorf("EncodeInt %d: expected %v, actual %v", tt.n, tt.expected, data) 57 | } 58 | } 59 | } 60 | 61 | func TestEncodeMiscInt(t *testing.T) { 62 | var tests = []struct { 63 | n interface{} 64 | expected []byte 65 | }{ 66 | // TODO: Include standalone header in that test 131. 67 | {int16(-256), []byte{bertrpc.TagETFVersion, bertrpc.TagInteger, 255, 255, 255, 0}}, 68 | {int8(-1), []byte{bertrpc.TagETFVersion, bertrpc.TagInteger, 255, 255, 255, 255}}, 69 | {uint8(1), []byte{bertrpc.TagETFVersion, bertrpc.TagSmallInteger, 1}}, 70 | {uint16(256), []byte{bertrpc.TagETFVersion, bertrpc.TagInteger, 0, 0, 1, 0}}, 71 | {uint32(2147483647), []byte{bertrpc.TagETFVersion, bertrpc.TagInteger, 127, 255, 255, 255}}, 72 | {int32(2147483647), []byte{bertrpc.TagETFVersion, bertrpc.TagInteger, 127, 255, 255, 255}}, 73 | } 74 | 75 | for _, tt := range tests { 76 | data, err := bertrpc.Encode(tt.n) 77 | if err != nil { 78 | t.Error(err) 79 | } 80 | if !bytes.Equal(data, tt.expected) { 81 | t.Errorf("EncodeMiscInt %d: expected %v, actual %v", tt.n, tt.expected, data) 82 | } 83 | } 84 | } 85 | 86 | func TestEncodeTuple(t *testing.T) { 87 | tuple := bertrpc.T(bertrpc.A("atom"), "string", 42) 88 | 89 | data, err := bertrpc.Encode(tuple) 90 | if err != nil { 91 | t.Error(err) 92 | } 93 | 94 | // In Erlang, generated tuple was using deprecated atom header 100,0,4 instead of 119, 4 (right after 104, 3) 95 | // However, the new version decodes just fine. 96 | // TODO: Deserialization should support deprecated header decoding 97 | expected := []byte{131, 104, 3, 119, 4, 97, 116, 111, 109, 109, 0, 0, 0, 6, 115, 116, 114, 105, 110, 103, 97, 42} 98 | if !bytes.Equal(data, expected) { 99 | t.Errorf("EncodeTuple: expected %v, actual %v", expected, data) 100 | } 101 | } 102 | 103 | func TestEncodeLargeTuple(t *testing.T) { 104 | var els []interface{} 105 | 106 | for el := 0; el < 256; el++ { 107 | els = append(els, el) 108 | } 109 | tuple := bertrpc.Tuple{els} 110 | 111 | data, err := bertrpc.Encode(tuple) 112 | if err != nil { 113 | t.Error(err) 114 | } 115 | 116 | // Inspect header 117 | expected := []byte{131, 105, 0, 0, 1, 0} 118 | header := data[0:6] 119 | if !bytes.Equal(header, expected) { 120 | t.Errorf("EncodeLargeTuple: expected %v, actual %v", expected, header) 121 | } 122 | } 123 | 124 | func TestEncodeList(t *testing.T) { 125 | list := bertrpc.L(bertrpc.A("atom"), "string", 42) 126 | 127 | data, err := bertrpc.Encode(list) 128 | if err != nil { 129 | t.Error(err) 130 | } 131 | 132 | // Use new utf8 atom (119), instead of old deprecated atom (100) 133 | expected := []byte{131, 108, 0, 0, 0, 3, 119, 4, 97, 116, 111, 109, 109, 0, 0, 0, 6, 115, 134 | 116, 114, 105, 110, 103, 97, 42, 106} 135 | if !bytes.Equal(data, expected) { 136 | t.Errorf("EncodeList: expected %v, actual %v", expected, data) 137 | } 138 | } 139 | 140 | func TestEncodeIntSlice(t *testing.T) { 141 | list := []int{1, 2, 3} 142 | 143 | data, err := bertrpc.Encode(list) 144 | if err != nil { 145 | t.Error(err) 146 | } 147 | 148 | // TODO: (most like for decoding). Erlang optimize this as string (list char): 131, 107, 0, 3, 1, 2, 3 149 | expected := []byte{131, 108, 0, 0, 0, 3, 97, 1, 97, 2, 97, 3, 106} 150 | if !bytes.Equal(data, expected) { 151 | t.Errorf("EncodeIntSlice: expected %v, actual %v", expected, data) 152 | } 153 | } 154 | 155 | // Recursive structure: puts a list into a tuple 156 | func TestEncodeTupleList(t *testing.T) { 157 | tuple := bertrpc.T(bertrpc.L(bertrpc.A("atom"), "string", 42)) 158 | data, err := bertrpc.Encode(tuple) 159 | if err != nil { 160 | t.Error(err) 161 | } 162 | // Use new utf8 atom (119), instead of old deprecated atom (100) 163 | expected := []byte{131, 104, 1, 108, 0, 0, 0, 3, 119, 4, 97, 116, 111, 109, 109, 0, 0, 0, 164 | 6, 115, 116, 114, 105, 110, 103, 97, 42, 106} 165 | if !bytes.Equal(data, expected) { 166 | t.Errorf("EncodeTuple: expected %v, actual %v", expected, data) 167 | } 168 | } 169 | 170 | func BenchmarkBufferString(b *testing.B) { 171 | for i := 0; i < b.N; i++ { 172 | _, _ = bertrpc.Encode("test") 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /bertrpc/etf.go: -------------------------------------------------------------------------------- 1 | package bertrpc // import "gosrc.io/erlang/bertrpc" 2 | 3 | // Supported ETF types 4 | const ( 5 | TagSmallInteger = 97 6 | TagInteger = 98 7 | TagDeprecatedAtom = 100 8 | TagSmallTuple = 104 9 | TagLargeTuple = 105 10 | TagNil = 106 11 | TagString = 107 12 | TagList = 108 13 | TagBinary = 109 14 | TagAtomUTF8 = 118 15 | TagSmallAtomUTF8 = 119 16 | TagETFVersion = 131 17 | ) 18 | 19 | // tagName convert a tag ID to its human readable tag name. 20 | func tagName(tag int) string { 21 | switch tag { 22 | case TagSmallInteger: 23 | return "SmallInteger" 24 | case TagInteger: 25 | return "Integer" 26 | case TagDeprecatedAtom: 27 | return "DeprecatedAtom" 28 | case TagSmallTuple: 29 | return "SmallTuple" 30 | case TagLargeTuple: 31 | return "LargeTuple" 32 | case TagNil: 33 | return "Nil" 34 | case TagString: 35 | return "String" 36 | case TagList: 37 | return "List" 38 | case TagBinary: 39 | return "Binary" 40 | case TagAtomUTF8: 41 | return "AtomUTF8" 42 | case TagSmallAtomUTF8: 43 | return "SmallAtomUTF" 44 | case TagETFVersion: 45 | return "VersionTag" 46 | default: 47 | return string(tag) 48 | } 49 | } 50 | 51 | // ============================================================================ 52 | // String / Atom wrapper 53 | 54 | type StringType int 55 | 56 | const ( 57 | StringTypeString = iota 58 | StringTypeAtom 59 | ) 60 | 61 | // String is a wrapper structure to support Erlang atom or string data type. 62 | // This type can be used when you want control / access to the underlying representation, 63 | // for example to make a difference between atoms and binaries. 64 | // If the difference does not matter for your code, you can simply use Go built-in string type. 65 | type String struct { 66 | Value string 67 | ErlangType StringType 68 | } 69 | 70 | func (str String) String() string { 71 | return str.Value 72 | } 73 | 74 | func (str String) IsAtom() bool { 75 | return str.ErlangType == StringTypeAtom 76 | } 77 | 78 | // ============================================================================ 79 | // List / Collection types 80 | 81 | type Tuple struct { 82 | Elems []interface{} 83 | } 84 | 85 | type List []interface{} 86 | 87 | // Charlist is a wrapper structure to support Erlang charlist in encoding. 88 | // Charlist is only used in encoding. On decoding, charlists are always decoded 89 | // as strings. 90 | type CharList struct { 91 | Value string 92 | } 93 | 94 | // ============================================================================ 95 | // Helpers 96 | // Short factory functions to help write short structure generation code. 97 | 98 | // Atom 99 | func A(atom string) String { 100 | return String{Value: atom, ErlangType: StringTypeAtom} 101 | } 102 | 103 | // String 104 | func S(str string) String { 105 | return String{Value: str} 106 | } 107 | 108 | // Tuple 109 | func T(el ...interface{}) Tuple { 110 | return Tuple{el} 111 | } 112 | 113 | // List 114 | func L(el ...interface{}) []interface{} { 115 | return el 116 | } 117 | -------------------------------------------------------------------------------- /bertrpc/rpc.go: -------------------------------------------------------------------------------- 1 | package bertrpc // import "gosrc.io/erlang/bertrpc" 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | ) 7 | 8 | // EncodeCall prepare a BERT-RPC Packet 9 | // See: http://bert-rpc.org/ 10 | func EncodeCall(module string, function string, args ...interface{}) (bytes.Buffer, error) { 11 | var buf bytes.Buffer 12 | 13 | // -- {call, Module, Function, Arguments} 14 | call := T(A("call"), A(module), A(function), args) 15 | data, err := Encode(call) 16 | if err != nil { 17 | return buf, err 18 | } 19 | 20 | // BERP Header = 4-bytes length 21 | // TODO: This should be optional for HTTP as it forces an extra allocation, instead of directly writing to the buffer 22 | // We already have packet framing at the HTTP call level. 23 | if err := binary.Write(&buf, binary.BigEndian, uint32(len(data))); err != nil { 24 | return buf, err 25 | } 26 | 27 | // Finally, write the data, after the length header 28 | buf.Write(data) 29 | return buf, err 30 | } 31 | -------------------------------------------------------------------------------- /examples/clients/ejabberd-register/ejabberd-register.go: -------------------------------------------------------------------------------- 1 | // Example client showing how to use BERT-RPC to create a user in ejabberd. 2 | package main 3 | 4 | import ( 5 | "log" 6 | 7 | "gosrc.io/erlang/bertrpc" 8 | ) 9 | 10 | func main() { 11 | svc := bertrpc.New("http://localhost:5281/rpc/") 12 | c := svc.NewCall("ejabberd_auth", "try_register", "john", "localhost", "password") 13 | var result struct { // ok | {error, atom()} 14 | Tag string `erlang:"tag"` 15 | Reason string `erlang:"tag:error"` 16 | } 17 | err := svc.Exec(c, &result) 18 | if err != nil { 19 | // Protocol or decoding errors 20 | log.Fatal("operation failed: ", err) 21 | } 22 | 23 | switch result.Tag { 24 | case "ok": 25 | log.Println("Successfully created user") 26 | case "error": 27 | log.Fatal("Could not create user: ", result.Reason) 28 | } 29 | } 30 | 31 | /* 32 | This module assumes that ejabberd has been configured with ejabberd_rpc support. This module is available in ejabberd 33 | master repository. 34 | 35 | Example config: 36 | 37 | ``` 38 | # Listener. ejabberd bertrpc module will be available on localhost on port 5281, under /rpc/ http endpoint. 39 | listen: 40 | # ... 41 | - 42 | port: 5281 43 | # For IPv6, use: 44 | # ip: "::FFFF:127.0.0.1" 45 | ip: "127.0.0.1" 46 | module: ejabberd_http 47 | request_handlers: 48 | "rpc": ejabberd_rpc 49 | ``` 50 | */ 51 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module gosrc.io/erlang 2 | 3 | go 1.12 4 | --------------------------------------------------------------------------------