├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── ffjson.go ├── ffjson ├── decoder.go ├── encoder.go ├── marshal.go └── pool.go ├── fflib └── v1 │ ├── buffer.go │ ├── buffer_nopool.go │ ├── buffer_pool.go │ ├── bytenum.go │ ├── decimal.go │ ├── extfloat.go │ ├── fold.go │ ├── ftoa.go │ ├── internal │ ├── atof.go │ ├── atoi.go │ ├── extfloat.go │ └── ftoa.go │ ├── iota.go │ ├── jsonstring.go │ ├── jsonstring_test.go │ ├── lexer.go │ ├── lexer_test.go │ ├── reader.go │ ├── reader_scan_generic.go │ └── reader_test.go ├── generator ├── generator.go ├── inceptionmain.go ├── parser.go ├── tags.go └── tempfile.go ├── inception ├── decoder.go ├── decoder_tpl.go ├── encoder.go ├── encoder_tpl.go ├── inception.go ├── reflect.go ├── tags.go ├── template.go └── writerstack.go ├── shared └── options.go └── tests ├── base.go ├── bench.cmd ├── encode_test.go ├── ff.go ├── ff_float_test.go ├── ff_invalid_test.go ├── ff_obj_test.go ├── ff_string_test.go ├── ff_test.go ├── fuzz ├── fuzzit.sh ├── generator_fuzz.go ├── go.mod ├── go.sum ├── target.go └── target_fuzz.go ├── fuzz_test.go ├── go.stripe ├── base │ └── customer.go ├── ff │ └── customer.go └── stripe_test.go ├── goser ├── base │ └── goser.go ├── ff │ └── goser.go └── goser_test.go ├── number ├── ff │ └── number.go └── number_test.go ├── t.cmd ├── t.sh ├── types ├── ff │ └── everything.go └── types_test.go └── vendor └── github.com └── foo └── vendored └── vendored.go /.gitignore: -------------------------------------------------------------------------------- 1 | tests/go.stripe/ff/customer_ffjson.go 2 | tests/goser/ff/goser_ffjson.go 3 | tests/types/ff/everything_ffjson.go 4 | tests/number/ff/number_ffjson.go 5 | tests/ff_ffjson.go 6 | fuzzing/ 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | services: 4 | - docker 5 | 6 | install: 7 | - A=${PWD#*github.com/};A=${A%/ffjson};cd ../..;mv $A pquerna;cd pquerna/ffjson 8 | - go get -d -v -t ./... 9 | 10 | script: make clean && make lint && make test && make test 11 | 12 | jobs: 13 | include: 14 | - stage: Fuzz regression 15 | go: 1.12.x 16 | dist: bionic 17 | script: 18 | - cd tests/fuzz 19 | - ./fuzzit.sh local-regression 20 | - stage: Fuzz 21 | if: branch = master AND type IN (push) 22 | go: 1.12.x 23 | dist: bionic 24 | script: 25 | - cd tests/fuzz 26 | - ./fuzzit.sh fuzzing 27 | 28 | go: 29 | - "1.10.x" 30 | - "1.11.x" 31 | 32 | env: 33 | GO15VENDOREXPERIMENT=1 34 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: test install 3 | @echo "Done" 4 | 5 | install: 6 | go install github.com/pquerna/ffjson 7 | 8 | deps: 9 | 10 | fmt: 11 | go fmt github.com/pquerna/ffjson/... 12 | 13 | cov: 14 | # TODO: cleanup this make target. 15 | mkdir -p coverage 16 | rm -f coverage/*.html 17 | # gocov test github.com/pquerna/ffjson/generator | gocov-html > coverage/generator.html 18 | # gocov test github.com/pquerna/ffjson/inception | gocov-html > coverage/inception.html 19 | gocov test github.com/pquerna/ffjson/fflib/v1 | gocov-html > coverage/fflib.html 20 | @echo "coverage written" 21 | 22 | test-core: 23 | go test -v github.com/pquerna/ffjson/fflib/v1 github.com/pquerna/ffjson/generator github.com/pquerna/ffjson/inception 24 | 25 | test: ffize test-core 26 | go test -v github.com/pquerna/ffjson/tests/... 27 | 28 | ffize: install 29 | ffjson -force-regenerate tests/ff.go 30 | ffjson -force-regenerate tests/goser/ff/goser.go 31 | ffjson -force-regenerate tests/go.stripe/ff/customer.go 32 | ffjson -force-regenerate -reset-fields tests/types/ff/everything.go 33 | ffjson -force-regenerate tests/number/ff/number.go 34 | 35 | lint: ffize 36 | go get github.com/golang/lint/golint 37 | golint --set_exit_status tests/... 38 | 39 | bench: ffize all 40 | go test -v -benchmem -bench MarshalJSON github.com/pquerna/ffjson/tests 41 | go test -v -benchmem -bench MarshalJSON github.com/pquerna/ffjson/tests/goser github.com/pquerna/ffjson/tests/go.stripe 42 | go test -v -benchmem -bench UnmarshalJSON github.com/pquerna/ffjson/tests/goser github.com/pquerna/ffjson/tests/go.stripe 43 | 44 | clean: 45 | go clean -i github.com/pquerna/ffjson/... 46 | find . -name '*_ffjson.go' -delete 47 | find . -name 'ffjson-inception*' -delete 48 | 49 | .PHONY: deps clean test fmt install all 50 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | ffjson 2 | Copyright (c) 2014, Paul Querna 3 | 4 | This product includes software developed by 5 | Paul Querna (http://paul.querna.org/). 6 | 7 | Portions of this software were developed as 8 | part of Go, Copyright (c) 2012 The Go Authors. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ffjson: faster JSON for Go 2 | 3 | [![Build Status](https://travis-ci.org/pquerna/ffjson.svg?branch=master)](https://travis-ci.org/pquerna/ffjson) 4 | [![Fuzzit Status](https://app.fuzzit.dev/badge?org_id=pquerna)](https://app.fuzzit.dev/orgs/pquerna/dashboard) 5 | 6 | `ffjson` generates static `MarshalJSON` and `UnmarshalJSON` functions for structures in Go. The generated functions reduce the reliance upon runtime reflection to do serialization and are generally 2 to 3 times faster. In cases where `ffjson` doesn't understand a Type involved, it falls back to `encoding/json`, meaning it is a safe drop in replacement. By using `ffjson` your JSON serialization just gets faster with no additional code changes. 7 | 8 | When you change your `struct`, you will need to run `ffjson` again (or make it part of your build tools). 9 | 10 | ## Blog Posts 11 | 12 | * 2014-03-31: [First Release and Background](https://journal.paul.querna.org/articles/2014/03/31/ffjson-faster-json-in-go/) 13 | 14 | ## Getting Started 15 | 16 | If `myfile.go` contains the `struct` types you would like to be faster, and assuming `GOPATH` is set to a reasonable value for an existing project (meaning that in this particular example if `myfile.go` is in the `myproject` directory, the project should be under `$GOPATH/src/myproject`), you can just run: 17 | 18 | go get -u github.com/pquerna/ffjson 19 | ffjson myfile.go 20 | git add myfile_ffjson.go 21 | 22 | 23 | ## Performance Status: 24 | 25 | * `MarshalJSON` is **2x to 3x** faster than `encoding/json`. 26 | * `UnmarshalJSON` is **2x to 3x** faster than `encoding/json`. 27 | 28 | ## Features 29 | 30 | * **Unmarshal Support:** Since v0.9, `ffjson` supports Unmarshaling of structures. 31 | * **Drop in Replacement:** Because `ffjson` implements the interfaces already defined by `encoding/json` the performance enhancements are transparent to users of your structures. 32 | * **Supports all types:** `ffjson` has native support for most of Go's types -- for any type it doesn't support with fast paths, it falls back to using `encoding/json`. This means all structures should work out of the box. If they don't, [open a issue!](https://github.com/pquerna/ffjson/issues) 33 | * **ffjson: skip**: If you have a structure you want `ffjson` to ignore, add `ffjson: skip` to the doc string for this structure. 34 | * **Extensive Tests:** `ffjson` contains an extensive test suite including fuzz'ing against the JSON parser. 35 | 36 | 37 | # Using ffjson 38 | 39 | `ffjson` generates code based upon existing `struct` types. For example, `ffjson foo.go` will by default create a new file `foo_ffjson.go` that contains serialization functions for all structs found in `foo.go`. 40 | 41 | ``` 42 | Usage of ffjson: 43 | 44 | ffjson [options] [input_file] 45 | 46 | ffjson generates Go code for optimized JSON serialization. 47 | 48 | -go-cmd="": Path to go command; Useful for `goapp` support. 49 | -import-name="": Override import name in case it cannot be detected. 50 | -nodecoder: Do not generate decoder functions 51 | -noencoder: Do not generate encoder functions 52 | -w="": Write generate code to this path instead of ${input}_ffjson.go. 53 | ``` 54 | 55 | Your code must be in a compilable state for `ffjson` to work. If you code doesn't compile ffjson will most likely exit with an error. 56 | 57 | ## Disabling code generation for structs 58 | 59 | You might not want all your structs to have JSON code generated. To completely disable generation for a struct, add `ffjson: skip` to the struct comment. For example: 60 | 61 | ```Go 62 | // ffjson: skip 63 | type Foo struct { 64 | Bar string 65 | } 66 | ``` 67 | 68 | You can also choose not to have either the decoder or encoder generated by including `ffjson: nodecoder` or `ffjson: noencoder` in your comment. For instance, this will only generate the encoder (marshal) part for this struct: 69 | 70 | ```Go 71 | // ffjson: nodecoder 72 | type Foo struct { 73 | Bar string 74 | } 75 | ``` 76 | 77 | You can also disable encoders/decoders entirely for a file by using the `-noencoder`/`-nodecoder` commandline flags. 78 | 79 | ## Using ffjson with `go generate` 80 | 81 | `ffjson` is a great fit with `go generate`. It allows you to specify the ffjson command inside your individual go files and run them all at once. This way you don't have to maintain a separate build file with the files you need to generate. 82 | 83 | Add this comment anywhere inside your go files: 84 | 85 | ```Go 86 | //go:generate ffjson $GOFILE 87 | ``` 88 | 89 | To re-generate ffjson for all files with the tag in a folder, simply execute: 90 | 91 | ```sh 92 | go generate 93 | ``` 94 | 95 | To generate for the current package and all sub-packages, use: 96 | 97 | ```sh 98 | go generate ./... 99 | ``` 100 | This is most of what you need to know about go generate, but you can sese more about [go generate on the golang blog](http://blog.golang.org/generate). 101 | 102 | ## Should I include ffjson files in VCS? 103 | 104 | That question is really up to you. If you don't, you will have a more complex build process. If you do, you have to keep the generated files updated if you change the content of your structs. 105 | 106 | That said, ffjson operates deterministically, so it will generate the same code every time it run, so unless your code changes, the generated content should not change. Note however that this is only true if you are using the same ffjson version, so if you have several people working on a project, you might need to synchronize your ffjson version. 107 | 108 | ## Performance pitfalls 109 | 110 | `ffjson` has a few cases where it will fall back to using the runtime encoder/decoder. Notable cases are: 111 | 112 | * Interface struct members. Since it isn't possible to know the type of these types before runtime, ffjson has to use the reflect based coder. 113 | * Structs with custom marshal/unmarshal. 114 | * Map with a complex value. Simple types like `map[string]int` is fine though. 115 | * Inline struct definitions `type A struct{B struct{ X int} }` are handled by the encoder, but currently has fallback in the decoder. 116 | * Slices of slices / slices of maps are currently falling back when generating the decoder. 117 | 118 | ## Reducing Garbage Collection 119 | 120 | `ffjson` already does a lot to help garbage generation. However whenever you go through the json.Marshal you get a new byte slice back. On very high throughput servers this can lead to increased GC pressure. 121 | 122 | ### Tip 1: Use ffjson.Marshal() / ffjson.Unmarshal() 123 | 124 | This is probably the easiest optimization for you. Instead of going through encoding/json, you can call ffjson. This will disable the checks that encoding/json does to the json when it receives it from struct functions. 125 | 126 | ```Go 127 | import "github.com/pquerna/ffjson/ffjson" 128 | 129 | // BEFORE: 130 | buf, err := json.Marshal(&item) 131 | 132 | // AFTER: 133 | buf, err := ffjson.Marshal(&item) 134 | ``` 135 | This simple change is likely to double the speed of your encoding/decoding. 136 | 137 | 138 | [![GoDoc][1]][2] 139 | [1]: https://godoc.org/github.com/pquerna/ffjson/ffjson?status.svg 140 | [2]: https://godoc.org/github.com/pquerna/ffjson/ffjson#Marshal 141 | 142 | ### Tip 2: Pooling the buffer 143 | 144 | On servers where you have a lot of concurrent encoding going on, you can hand back the byte buffer you get from json.Marshal once you are done using it. An example could look like this: 145 | ```Go 146 | import "github.com/pquerna/ffjson/ffjson" 147 | 148 | func Encode(item interface{}, out io.Writer) { 149 | // Encode 150 | buf, err := ffjson.Marshal(&item) 151 | 152 | // Write the buffer 153 | _,_ = out.Write(buf) 154 | 155 | // We are now no longer need the buffer so we pool it. 156 | ffjson.Pool(buf) 157 | } 158 | ``` 159 | Note that the buffers you put back in the pool can still be reclaimed by the garbage collector, so you wont risk your program building up a big memory use by pooling the buffers. 160 | 161 | [![GoDoc][1]][2] 162 | [1]: https://godoc.org/github.com/pquerna/ffjson/ffjson?status.svg 163 | [2]: https://godoc.org/github.com/pquerna/ffjson/ffjson#Pool 164 | 165 | ### Tip 3: Creating an Encoder 166 | 167 | There might be cases where you need to encode many objects at once. This could be a server backing up, writing a lot of entries to files, etc. 168 | 169 | To do this, there is an interface similar to `encoding/json`, that allow you to create a re-usable encoder. Here is an example where we want to encode an array of the `Item` type, with a comma between entries: 170 | ```Go 171 | import "github.com/pquerna/ffjson/ffjson" 172 | 173 | func EncodeItems(items []Item, out io.Writer) { 174 | // We create an encoder. 175 | enc := ffjson.NewEncoder(out) 176 | 177 | for i, item := range items { 178 | // Encode into the buffer 179 | err := enc.Encode(&item) 180 | 181 | // If err is nil, the content is written to out, so we can write to it as well. 182 | if i != len(items) -1 { 183 | _,_ = out.Write([]byte{','}) 184 | } 185 | } 186 | } 187 | ``` 188 | 189 | 190 | Documentation: [![GoDoc][1]][2] 191 | [1]: https://godoc.org/github.com/pquerna/ffjson/ffjson?status.svg 192 | [2]: https://godoc.org/github.com/pquerna/ffjson/ffjson#Encoder 193 | 194 | ## Tip 4: Avoid interfaces 195 | 196 | We don't want to dictate how you structure your data, but having interfaces in your code will make ffjson use the golang encoder for these. When ffjson has to do this, it may even become slower than using `json.Marshal` directly. 197 | 198 | To see where that happens, search the generated `_ffjson.go` file for the text `Falling back`, which will indicate where ffjson is unable to generate code for your data structure. 199 | 200 | ## Tip 5: `ffjson` all the things! 201 | 202 | You should not only create ffjson code for your main struct, but also any structs that is included/used in your json code. 203 | 204 | So if your struct looks like this: 205 | ```Go 206 | type Foo struct { 207 | V Bar 208 | } 209 | ``` 210 | You should also make sure that code is generated for `Bar` if it is placed in another file. Also note that currently it requires you to do this in order, since generating code for `Foo` will check if code for `Bar` exists. This is only an issue if `Foo` and `Bar` are placed in different files. We are currently working on allowing simultaneous generation of an entire package. 211 | 212 | 213 | ## Improvements, bugs, adding features, and taking ffjson new directions! 214 | 215 | Please [open issues in Github](https://github.com/pquerna/ffjson/issues) for ideas, bugs, and general thoughts. Pull requests are of course preferred :) 216 | 217 | ## Similar projects 218 | 219 | * [go-codec](https://github.com/ugorji/go/tree/master/codec#readme). Very good project, that also allows streaming en/decoding, but requires you to call the library to use. 220 | * [megajson](https://github.com/benbjohnson/megajson). This has limited support, and development seems to have almost stopped at the time of writing. 221 | 222 | # Credits 223 | 224 | `ffjson` has recieved significant contributions from: 225 | 226 | * [Klaus Post](https://github.com/klauspost) 227 | * [Paul Querna](https://github.com/pquerna) 228 | * [Erik Dubbelboer](https://github.com/erikdubbelboer) 229 | 230 | ## License 231 | 232 | `ffjson` is licensed under the [Apache License, Version 2.0](./LICENSE) 233 | 234 | -------------------------------------------------------------------------------- /ffjson.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package main 19 | 20 | import ( 21 | _ "github.com/pquerna/ffjson/fflib/v1" 22 | "github.com/pquerna/ffjson/generator" 23 | _ "github.com/pquerna/ffjson/inception" 24 | 25 | "flag" 26 | "fmt" 27 | "os" 28 | "path/filepath" 29 | "regexp" 30 | ) 31 | 32 | var outputPathFlag = flag.String("w", "", "Write generate code to this path instead of ${input}_ffjson.go.") 33 | var goCmdFlag = flag.String("go-cmd", "", "Path to go command; Useful for `goapp` support.") 34 | var importNameFlag = flag.String("import-name", "", "Override import name in case it cannot be detected.") 35 | var forceRegenerateFlag = flag.Bool("force-regenerate", false, "Regenerate every input file, without checking modification date.") 36 | var resetFields = flag.Bool("reset-fields", false, "When unmarshalling reset all fields missing in the JSON") 37 | 38 | func usage() { 39 | fmt.Fprintf(os.Stderr, "Usage of %s:\n\n", os.Args[0]) 40 | fmt.Fprintf(os.Stderr, "\t%s [options] [input_file]\n\n", os.Args[0]) 41 | fmt.Fprintf(os.Stderr, "%s generates Go code for optimized JSON serialization.\n\n", os.Args[0]) 42 | flag.PrintDefaults() 43 | os.Exit(1) 44 | } 45 | 46 | var extRe = regexp.MustCompile(`(.*)(\.go)$`) 47 | 48 | func main() { 49 | flag.Parse() 50 | extra := flag.Args() 51 | 52 | if len(extra) != 1 { 53 | usage() 54 | } 55 | 56 | inputPath := filepath.ToSlash(extra[0]) 57 | 58 | var outputPath string 59 | if outputPathFlag == nil || *outputPathFlag == "" { 60 | outputPath = extRe.ReplaceAllString(inputPath, "${1}_ffjson.go") 61 | } else { 62 | outputPath = *outputPathFlag 63 | } 64 | 65 | var goCmd string 66 | if goCmdFlag == nil || *goCmdFlag == "" { 67 | goCmd = "go" 68 | } else { 69 | goCmd = *goCmdFlag 70 | } 71 | 72 | var importName string 73 | if importNameFlag != nil && *importNameFlag != "" { 74 | importName = *importNameFlag 75 | } 76 | 77 | err := generator.GenerateFiles(goCmd, inputPath, outputPath, importName, *forceRegenerateFlag, *resetFields) 78 | 79 | if err != nil { 80 | fmt.Fprintf(os.Stderr, "Error: %s:\n\n", err) 81 | os.Exit(1) 82 | } 83 | 84 | println(outputPath) 85 | } 86 | -------------------------------------------------------------------------------- /ffjson/decoder.go: -------------------------------------------------------------------------------- 1 | package ffjson 2 | 3 | /** 4 | * Copyright 2015 Paul Querna, Klaus Post 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | */ 19 | 20 | import ( 21 | "encoding/json" 22 | "errors" 23 | fflib "github.com/pquerna/ffjson/fflib/v1" 24 | "io" 25 | "io/ioutil" 26 | "reflect" 27 | ) 28 | 29 | // This is a reusable decoder. 30 | // This should not be used by more than one goroutine at the time. 31 | type Decoder struct { 32 | fs *fflib.FFLexer 33 | } 34 | 35 | // NewDecoder returns a reusable Decoder. 36 | func NewDecoder() *Decoder { 37 | return &Decoder{} 38 | } 39 | 40 | // Decode the data in the supplied data slice. 41 | func (d *Decoder) Decode(data []byte, v interface{}) error { 42 | f, ok := v.(unmarshalFaster) 43 | if ok { 44 | if d.fs == nil { 45 | d.fs = fflib.NewFFLexer(data) 46 | } else { 47 | d.fs.Reset(data) 48 | } 49 | return f.UnmarshalJSONFFLexer(d.fs, fflib.FFParse_map_start) 50 | } 51 | 52 | um, ok := v.(json.Unmarshaler) 53 | if ok { 54 | return um.UnmarshalJSON(data) 55 | } 56 | return json.Unmarshal(data, v) 57 | } 58 | 59 | // Decode the data from the supplied reader. 60 | // You should expect that data is read into memory before it is decoded. 61 | func (d *Decoder) DecodeReader(r io.Reader, v interface{}) error { 62 | _, ok := v.(unmarshalFaster) 63 | _, ok2 := v.(json.Unmarshaler) 64 | if ok || ok2 { 65 | data, err := ioutil.ReadAll(r) 66 | if err != nil { 67 | return err 68 | } 69 | defer fflib.Pool(data) 70 | return d.Decode(data, v) 71 | } 72 | dec := json.NewDecoder(r) 73 | return dec.Decode(v) 74 | } 75 | 76 | // DecodeFast will unmarshal the data if fast unmarshal is available. 77 | // This function can be used if you want to be sure the fast 78 | // unmarshal is used or in testing. 79 | // If you would like to have fallback to encoding/json you can use the 80 | // regular Decode() method. 81 | func (d *Decoder) DecodeFast(data []byte, v interface{}) error { 82 | f, ok := v.(unmarshalFaster) 83 | if !ok { 84 | return errors.New("ffjson unmarshal not available for type " + reflect.TypeOf(v).String()) 85 | } 86 | if d.fs == nil { 87 | d.fs = fflib.NewFFLexer(data) 88 | } else { 89 | d.fs.Reset(data) 90 | } 91 | return f.UnmarshalJSONFFLexer(d.fs, fflib.FFParse_map_start) 92 | } 93 | -------------------------------------------------------------------------------- /ffjson/encoder.go: -------------------------------------------------------------------------------- 1 | package ffjson 2 | 3 | /** 4 | * Copyright 2015 Paul Querna, Klaus Post 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | */ 19 | 20 | import ( 21 | "encoding/json" 22 | "errors" 23 | fflib "github.com/pquerna/ffjson/fflib/v1" 24 | "io" 25 | "reflect" 26 | ) 27 | 28 | // This is a reusable encoder. 29 | // It allows to encode many objects to a single writer. 30 | // This should not be used by more than one goroutine at the time. 31 | type Encoder struct { 32 | buf fflib.Buffer 33 | w io.Writer 34 | enc *json.Encoder 35 | } 36 | 37 | // SetEscapeHTML specifies whether problematic HTML characters 38 | // should be escaped inside JSON quoted strings. 39 | // The default behavior is to escape &, <, and > to \u0026, \u003c, and \u003e 40 | // to avoid certain safety problems that can arise when embedding JSON in HTML. 41 | // 42 | // In non-HTML settings where the escaping interferes with the readability 43 | // of the output, SetEscapeHTML(false) disables this behavior. 44 | func (enc *Encoder) SetEscapeHTML(on bool) { 45 | enc.enc.SetEscapeHTML(on) 46 | } 47 | 48 | // NewEncoder returns a reusable Encoder. 49 | // Output will be written to the supplied writer. 50 | func NewEncoder(w io.Writer) *Encoder { 51 | return &Encoder{w: w, enc: json.NewEncoder(w)} 52 | } 53 | 54 | // Encode the data in the supplied value to the stream 55 | // given on creation. 56 | // When the function returns the output has been 57 | // written to the stream. 58 | func (e *Encoder) Encode(v interface{}) error { 59 | f, ok := v.(marshalerFaster) 60 | if ok { 61 | e.buf.Reset() 62 | err := f.MarshalJSONBuf(&e.buf) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | _, err = io.Copy(e.w, &e.buf) 68 | return err 69 | } 70 | 71 | return e.enc.Encode(v) 72 | } 73 | 74 | // EncodeFast will unmarshal the data if fast marshall is available. 75 | // This function can be used if you want to be sure the fast 76 | // marshal is used or in testing. 77 | // If you would like to have fallback to encoding/json you can use the 78 | // regular Encode() method. 79 | func (e *Encoder) EncodeFast(v interface{}) error { 80 | _, ok := v.(marshalerFaster) 81 | if !ok { 82 | return errors.New("ffjson marshal not available for type " + reflect.TypeOf(v).String()) 83 | } 84 | return e.Encode(v) 85 | } 86 | -------------------------------------------------------------------------------- /ffjson/marshal.go: -------------------------------------------------------------------------------- 1 | package ffjson 2 | 3 | /** 4 | * Copyright 2015 Paul Querna, Klaus Post 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | */ 19 | 20 | import ( 21 | "encoding/json" 22 | "errors" 23 | fflib "github.com/pquerna/ffjson/fflib/v1" 24 | "reflect" 25 | ) 26 | 27 | type marshalerFaster interface { 28 | MarshalJSONBuf(buf fflib.EncodingBuffer) error 29 | } 30 | 31 | type unmarshalFaster interface { 32 | UnmarshalJSONFFLexer(l *fflib.FFLexer, state fflib.FFParseState) error 33 | } 34 | 35 | // Marshal will act the same way as json.Marshal, except 36 | // it will choose the ffjson marshal function before falling 37 | // back to using json.Marshal. 38 | // Using this function will bypass the internal copying and parsing 39 | // the json library normally does, which greatly speeds up encoding time. 40 | // It is ok to call this function even if no ffjson code has been 41 | // generated for the data type you pass in the interface. 42 | func Marshal(v interface{}) ([]byte, error) { 43 | f, ok := v.(marshalerFaster) 44 | if ok { 45 | buf := fflib.Buffer{} 46 | err := f.MarshalJSONBuf(&buf) 47 | b := buf.Bytes() 48 | if err != nil { 49 | if len(b) > 0 { 50 | Pool(b) 51 | } 52 | return nil, err 53 | } 54 | return b, nil 55 | } 56 | 57 | j, ok := v.(json.Marshaler) 58 | if ok { 59 | return j.MarshalJSON() 60 | } 61 | return json.Marshal(v) 62 | } 63 | 64 | // MarshalFast will marshal the data if fast marshal is available. 65 | // This function can be used if you want to be sure the fast 66 | // marshal is used or in testing. 67 | // If you would like to have fallback to encoding/json you can use the 68 | // Marshal() method. 69 | func MarshalFast(v interface{}) ([]byte, error) { 70 | _, ok := v.(marshalerFaster) 71 | if !ok { 72 | return nil, errors.New("ffjson marshal not available for type " + reflect.TypeOf(v).String()) 73 | } 74 | return Marshal(v) 75 | } 76 | 77 | // Unmarshal will act the same way as json.Unmarshal, except 78 | // it will choose the ffjson unmarshal function before falling 79 | // back to using json.Unmarshal. 80 | // The overhead of unmarshal is lower than on Marshal, 81 | // however this should still provide a speedup for your encoding. 82 | // It is ok to call this function even if no ffjson code has been 83 | // generated for the data type you pass in the interface. 84 | func Unmarshal(data []byte, v interface{}) error { 85 | f, ok := v.(unmarshalFaster) 86 | if ok { 87 | fs := fflib.NewFFLexer(data) 88 | return f.UnmarshalJSONFFLexer(fs, fflib.FFParse_map_start) 89 | } 90 | 91 | j, ok := v.(json.Unmarshaler) 92 | if ok { 93 | return j.UnmarshalJSON(data) 94 | } 95 | return json.Unmarshal(data, v) 96 | } 97 | 98 | // UnmarshalFast will unmarshal the data if fast marshall is available. 99 | // This function can be used if you want to be sure the fast 100 | // unmarshal is used or in testing. 101 | // If you would like to have fallback to encoding/json you can use the 102 | // Unmarshal() method. 103 | func UnmarshalFast(data []byte, v interface{}) error { 104 | _, ok := v.(unmarshalFaster) 105 | if !ok { 106 | return errors.New("ffjson unmarshal not available for type " + reflect.TypeOf(v).String()) 107 | } 108 | return Unmarshal(data, v) 109 | } 110 | -------------------------------------------------------------------------------- /ffjson/pool.go: -------------------------------------------------------------------------------- 1 | package ffjson 2 | 3 | /** 4 | * Copyright 2015 Paul Querna, Klaus Post 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | */ 19 | 20 | import ( 21 | fflib "github.com/pquerna/ffjson/fflib/v1" 22 | ) 23 | 24 | // Send a buffer to the Pool to reuse for other instances. 25 | // 26 | // On servers where you have a lot of concurrent encoding going on, 27 | // you can hand back the byte buffer you get marshalling once you are done using it. 28 | // 29 | // You may no longer utilize the content of the buffer, since it may be used 30 | // by other goroutines. 31 | func Pool(b []byte) { 32 | fflib.Pool(b) 33 | } 34 | -------------------------------------------------------------------------------- /fflib/v1/buffer_nopool.go: -------------------------------------------------------------------------------- 1 | // +build !go1.3 2 | 3 | package v1 4 | 5 | // Stub version of buffer_pool.go for Go 1.2, which doesn't have sync.Pool. 6 | 7 | func Pool(b []byte) {} 8 | 9 | func makeSlice(n int) []byte { 10 | return make([]byte, n) 11 | } 12 | -------------------------------------------------------------------------------- /fflib/v1/buffer_pool.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build go1.3 6 | 7 | package v1 8 | 9 | // Allocation pools for Buffers. 10 | 11 | import "sync" 12 | 13 | var pools [14]sync.Pool 14 | var pool64 *sync.Pool 15 | 16 | func init() { 17 | var i uint 18 | // TODO(pquerna): add science here around actual pool sizes. 19 | for i = 6; i < 20; i++ { 20 | n := 1 << i 21 | pools[poolNum(n)].New = func() interface{} { return make([]byte, 0, n) } 22 | } 23 | pool64 = &pools[0] 24 | } 25 | 26 | // This returns the pool number that will give a buffer of 27 | // at least 'i' bytes. 28 | func poolNum(i int) int { 29 | // TODO(pquerna): convert to log2 w/ bsr asm instruction: 30 | // 31 | if i <= 64 { 32 | return 0 33 | } else if i <= 128 { 34 | return 1 35 | } else if i <= 256 { 36 | return 2 37 | } else if i <= 512 { 38 | return 3 39 | } else if i <= 1024 { 40 | return 4 41 | } else if i <= 2048 { 42 | return 5 43 | } else if i <= 4096 { 44 | return 6 45 | } else if i <= 8192 { 46 | return 7 47 | } else if i <= 16384 { 48 | return 8 49 | } else if i <= 32768 { 50 | return 9 51 | } else if i <= 65536 { 52 | return 10 53 | } else if i <= 131072 { 54 | return 11 55 | } else if i <= 262144 { 56 | return 12 57 | } else if i <= 524288 { 58 | return 13 59 | } else { 60 | return -1 61 | } 62 | } 63 | 64 | // Send a buffer to the Pool to reuse for other instances. 65 | // You may no longer utilize the content of the buffer, since it may be used 66 | // by other goroutines. 67 | func Pool(b []byte) { 68 | if b == nil { 69 | return 70 | } 71 | c := cap(b) 72 | 73 | // Our smallest buffer is 64 bytes, so we discard smaller buffers. 74 | if c < 64 { 75 | return 76 | } 77 | 78 | // We need to put the incoming buffer into the NEXT buffer, 79 | // since a buffer guarantees AT LEAST the number of bytes available 80 | // that is the top of this buffer. 81 | // That is the reason for dividing the cap by 2, so it gets into the NEXT bucket. 82 | // We add 2 to avoid rounding down if size is exactly power of 2. 83 | pn := poolNum((c + 2) >> 1) 84 | if pn != -1 { 85 | pools[pn].Put(b[0:0]) 86 | } 87 | // if we didn't have a slot for this []byte, we just drop it and let the GC 88 | // take care of it. 89 | } 90 | 91 | // makeSlice allocates a slice of size n -- it will attempt to use a pool'ed 92 | // instance whenever possible. 93 | func makeSlice(n int) []byte { 94 | if n <= 64 { 95 | return pool64.Get().([]byte)[0:n] 96 | } 97 | 98 | pn := poolNum(n) 99 | 100 | if pn != -1 { 101 | return pools[pn].Get().([]byte)[0:n] 102 | } else { 103 | return make([]byte, n) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /fflib/v1/bytenum.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | /* Portions of this file are on Go stdlib's strconv/iota.go */ 19 | // Copyright 2009 The Go Authors. All rights reserved. 20 | // Use of this source code is governed by a BSD-style 21 | // license that can be found in the LICENSE file. 22 | 23 | package v1 24 | 25 | import ( 26 | "github.com/pquerna/ffjson/fflib/v1/internal" 27 | ) 28 | 29 | func ParseFloat(s []byte, bitSize int) (f float64, err error) { 30 | return internal.ParseFloat(s, bitSize) 31 | } 32 | 33 | // ParseUint is like ParseInt but for unsigned numbers, and oeprating on []byte 34 | func ParseUint(s []byte, base int, bitSize int) (n uint64, err error) { 35 | if len(s) == 1 { 36 | switch s[0] { 37 | case '0': 38 | return 0, nil 39 | case '1': 40 | return 1, nil 41 | case '2': 42 | return 2, nil 43 | case '3': 44 | return 3, nil 45 | case '4': 46 | return 4, nil 47 | case '5': 48 | return 5, nil 49 | case '6': 50 | return 6, nil 51 | case '7': 52 | return 7, nil 53 | case '8': 54 | return 8, nil 55 | case '9': 56 | return 9, nil 57 | } 58 | } 59 | return internal.ParseUint(s, base, bitSize) 60 | } 61 | 62 | func ParseInt(s []byte, base int, bitSize int) (i int64, err error) { 63 | if len(s) == 1 { 64 | switch s[0] { 65 | case '0': 66 | return 0, nil 67 | case '1': 68 | return 1, nil 69 | case '2': 70 | return 2, nil 71 | case '3': 72 | return 3, nil 73 | case '4': 74 | return 4, nil 75 | case '5': 76 | return 5, nil 77 | case '6': 78 | return 6, nil 79 | case '7': 80 | return 7, nil 81 | case '8': 82 | return 8, nil 83 | case '9': 84 | return 9, nil 85 | } 86 | } 87 | return internal.ParseInt(s, base, bitSize) 88 | } 89 | -------------------------------------------------------------------------------- /fflib/v1/decimal.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Multiprecision decimal numbers. 6 | // For floating-point formatting only; not general purpose. 7 | // Only operations are assign and (binary) left/right shift. 8 | // Can do binary floating point in multiprecision decimal precisely 9 | // because 2 divides 10; cannot do decimal floating point 10 | // in multiprecision binary precisely. 11 | 12 | package v1 13 | 14 | type decimal struct { 15 | d [800]byte // digits 16 | nd int // number of digits used 17 | dp int // decimal point 18 | neg bool 19 | trunc bool // discarded nonzero digits beyond d[:nd] 20 | } 21 | 22 | func (a *decimal) String() string { 23 | n := 10 + a.nd 24 | if a.dp > 0 { 25 | n += a.dp 26 | } 27 | if a.dp < 0 { 28 | n += -a.dp 29 | } 30 | 31 | buf := make([]byte, n) 32 | w := 0 33 | switch { 34 | case a.nd == 0: 35 | return "0" 36 | 37 | case a.dp <= 0: 38 | // zeros fill space between decimal point and digits 39 | buf[w] = '0' 40 | w++ 41 | buf[w] = '.' 42 | w++ 43 | w += digitZero(buf[w : w+-a.dp]) 44 | w += copy(buf[w:], a.d[0:a.nd]) 45 | 46 | case a.dp < a.nd: 47 | // decimal point in middle of digits 48 | w += copy(buf[w:], a.d[0:a.dp]) 49 | buf[w] = '.' 50 | w++ 51 | w += copy(buf[w:], a.d[a.dp:a.nd]) 52 | 53 | default: 54 | // zeros fill space between digits and decimal point 55 | w += copy(buf[w:], a.d[0:a.nd]) 56 | w += digitZero(buf[w : w+a.dp-a.nd]) 57 | } 58 | return string(buf[0:w]) 59 | } 60 | 61 | func digitZero(dst []byte) int { 62 | for i := range dst { 63 | dst[i] = '0' 64 | } 65 | return len(dst) 66 | } 67 | 68 | // trim trailing zeros from number. 69 | // (They are meaningless; the decimal point is tracked 70 | // independent of the number of digits.) 71 | func trim(a *decimal) { 72 | for a.nd > 0 && a.d[a.nd-1] == '0' { 73 | a.nd-- 74 | } 75 | if a.nd == 0 { 76 | a.dp = 0 77 | } 78 | } 79 | 80 | // Assign v to a. 81 | func (a *decimal) Assign(v uint64) { 82 | var buf [24]byte 83 | 84 | // Write reversed decimal in buf. 85 | n := 0 86 | for v > 0 { 87 | v1 := v / 10 88 | v -= 10 * v1 89 | buf[n] = byte(v + '0') 90 | n++ 91 | v = v1 92 | } 93 | 94 | // Reverse again to produce forward decimal in a.d. 95 | a.nd = 0 96 | for n--; n >= 0; n-- { 97 | a.d[a.nd] = buf[n] 98 | a.nd++ 99 | } 100 | a.dp = a.nd 101 | trim(a) 102 | } 103 | 104 | // Maximum shift that we can do in one pass without overflow. 105 | // Signed int has 31 bits, and we have to be able to accommodate 9<>k == 0; r++ { 116 | if r >= a.nd { 117 | if n == 0 { 118 | // a == 0; shouldn't get here, but handle anyway. 119 | a.nd = 0 120 | return 121 | } 122 | for n>>k == 0 { 123 | n = n * 10 124 | r++ 125 | } 126 | break 127 | } 128 | c := int(a.d[r]) 129 | n = n*10 + c - '0' 130 | } 131 | a.dp -= r - 1 132 | 133 | // Pick up a digit, put down a digit. 134 | for ; r < a.nd; r++ { 135 | c := int(a.d[r]) 136 | dig := n >> k 137 | n -= dig << k 138 | a.d[w] = byte(dig + '0') 139 | w++ 140 | n = n*10 + c - '0' 141 | } 142 | 143 | // Put down extra digits. 144 | for n > 0 { 145 | dig := n >> k 146 | n -= dig << k 147 | if w < len(a.d) { 148 | a.d[w] = byte(dig + '0') 149 | w++ 150 | } else if dig > 0 { 151 | a.trunc = true 152 | } 153 | n = n * 10 154 | } 155 | 156 | a.nd = w 157 | trim(a) 158 | } 159 | 160 | // Cheat sheet for left shift: table indexed by shift count giving 161 | // number of new digits that will be introduced by that shift. 162 | // 163 | // For example, leftcheats[4] = {2, "625"}. That means that 164 | // if we are shifting by 4 (multiplying by 16), it will add 2 digits 165 | // when the string prefix is "625" through "999", and one fewer digit 166 | // if the string prefix is "000" through "624". 167 | // 168 | // Credit for this trick goes to Ken. 169 | 170 | type leftCheat struct { 171 | delta int // number of new digits 172 | cutoff string // minus one digit if original < a. 173 | } 174 | 175 | var leftcheats = []leftCheat{ 176 | // Leading digits of 1/2^i = 5^i. 177 | // 5^23 is not an exact 64-bit floating point number, 178 | // so have to use bc for the math. 179 | /* 180 | seq 27 | sed 's/^/5^/' | bc | 181 | awk 'BEGIN{ print "\tleftCheat{ 0, \"\" }," } 182 | { 183 | log2 = log(2)/log(10) 184 | printf("\tleftCheat{ %d, \"%s\" },\t// * %d\n", 185 | int(log2*NR+1), $0, 2**NR) 186 | }' 187 | */ 188 | {0, ""}, 189 | {1, "5"}, // * 2 190 | {1, "25"}, // * 4 191 | {1, "125"}, // * 8 192 | {2, "625"}, // * 16 193 | {2, "3125"}, // * 32 194 | {2, "15625"}, // * 64 195 | {3, "78125"}, // * 128 196 | {3, "390625"}, // * 256 197 | {3, "1953125"}, // * 512 198 | {4, "9765625"}, // * 1024 199 | {4, "48828125"}, // * 2048 200 | {4, "244140625"}, // * 4096 201 | {4, "1220703125"}, // * 8192 202 | {5, "6103515625"}, // * 16384 203 | {5, "30517578125"}, // * 32768 204 | {5, "152587890625"}, // * 65536 205 | {6, "762939453125"}, // * 131072 206 | {6, "3814697265625"}, // * 262144 207 | {6, "19073486328125"}, // * 524288 208 | {7, "95367431640625"}, // * 1048576 209 | {7, "476837158203125"}, // * 2097152 210 | {7, "2384185791015625"}, // * 4194304 211 | {7, "11920928955078125"}, // * 8388608 212 | {8, "59604644775390625"}, // * 16777216 213 | {8, "298023223876953125"}, // * 33554432 214 | {8, "1490116119384765625"}, // * 67108864 215 | {9, "7450580596923828125"}, // * 134217728 216 | } 217 | 218 | // Is the leading prefix of b lexicographically less than s? 219 | func prefixIsLessThan(b []byte, s string) bool { 220 | for i := 0; i < len(s); i++ { 221 | if i >= len(b) { 222 | return true 223 | } 224 | if b[i] != s[i] { 225 | return b[i] < s[i] 226 | } 227 | } 228 | return false 229 | } 230 | 231 | // Binary shift left (/ 2) by k bits. k <= maxShift to avoid overflow. 232 | func leftShift(a *decimal, k uint) { 233 | delta := leftcheats[k].delta 234 | if prefixIsLessThan(a.d[0:a.nd], leftcheats[k].cutoff) { 235 | delta-- 236 | } 237 | 238 | r := a.nd // read index 239 | w := a.nd + delta // write index 240 | n := 0 241 | 242 | // Pick up a digit, put down a digit. 243 | for r--; r >= 0; r-- { 244 | n += (int(a.d[r]) - '0') << k 245 | quo := n / 10 246 | rem := n - 10*quo 247 | w-- 248 | if w < len(a.d) { 249 | a.d[w] = byte(rem + '0') 250 | } else if rem != 0 { 251 | a.trunc = true 252 | } 253 | n = quo 254 | } 255 | 256 | // Put down extra digits. 257 | for n > 0 { 258 | quo := n / 10 259 | rem := n - 10*quo 260 | w-- 261 | if w < len(a.d) { 262 | a.d[w] = byte(rem + '0') 263 | } else if rem != 0 { 264 | a.trunc = true 265 | } 266 | n = quo 267 | } 268 | 269 | a.nd += delta 270 | if a.nd >= len(a.d) { 271 | a.nd = len(a.d) 272 | } 273 | a.dp += delta 274 | trim(a) 275 | } 276 | 277 | // Binary shift left (k > 0) or right (k < 0). 278 | func (a *decimal) Shift(k int) { 279 | switch { 280 | case a.nd == 0: 281 | // nothing to do: a == 0 282 | case k > 0: 283 | for k > maxShift { 284 | leftShift(a, maxShift) 285 | k -= maxShift 286 | } 287 | leftShift(a, uint(k)) 288 | case k < 0: 289 | for k < -maxShift { 290 | rightShift(a, maxShift) 291 | k += maxShift 292 | } 293 | rightShift(a, uint(-k)) 294 | } 295 | } 296 | 297 | // If we chop a at nd digits, should we round up? 298 | func shouldRoundUp(a *decimal, nd int) bool { 299 | if nd < 0 || nd >= a.nd { 300 | return false 301 | } 302 | if a.d[nd] == '5' && nd+1 == a.nd { // exactly halfway - round to even 303 | // if we truncated, a little higher than what's recorded - always round up 304 | if a.trunc { 305 | return true 306 | } 307 | return nd > 0 && (a.d[nd-1]-'0')%2 != 0 308 | } 309 | // not halfway - digit tells all 310 | return a.d[nd] >= '5' 311 | } 312 | 313 | // Round a to nd digits (or fewer). 314 | // If nd is zero, it means we're rounding 315 | // just to the left of the digits, as in 316 | // 0.09 -> 0.1. 317 | func (a *decimal) Round(nd int) { 318 | if nd < 0 || nd >= a.nd { 319 | return 320 | } 321 | if shouldRoundUp(a, nd) { 322 | a.RoundUp(nd) 323 | } else { 324 | a.RoundDown(nd) 325 | } 326 | } 327 | 328 | // Round a down to nd digits (or fewer). 329 | func (a *decimal) RoundDown(nd int) { 330 | if nd < 0 || nd >= a.nd { 331 | return 332 | } 333 | a.nd = nd 334 | trim(a) 335 | } 336 | 337 | // Round a up to nd digits (or fewer). 338 | func (a *decimal) RoundUp(nd int) { 339 | if nd < 0 || nd >= a.nd { 340 | return 341 | } 342 | 343 | // round up 344 | for i := nd - 1; i >= 0; i-- { 345 | c := a.d[i] 346 | if c < '9' { // can stop after this digit 347 | a.d[i]++ 348 | a.nd = i + 1 349 | return 350 | } 351 | } 352 | 353 | // Number is all 9s. 354 | // Change to single 1 with adjusted decimal point. 355 | a.d[0] = '1' 356 | a.nd = 1 357 | a.dp++ 358 | } 359 | 360 | // Extract integer part, rounded appropriately. 361 | // No guarantees about overflow. 362 | func (a *decimal) RoundedInteger() uint64 { 363 | if a.dp > 20 { 364 | return 0xFFFFFFFFFFFFFFFF 365 | } 366 | var i int 367 | n := uint64(0) 368 | for i = 0; i < a.dp && i < a.nd; i++ { 369 | n = n*10 + uint64(a.d[i]-'0') 370 | } 371 | for ; i < a.dp; i++ { 372 | n *= 10 373 | } 374 | if shouldRoundUp(a, a.dp) { 375 | n++ 376 | } 377 | return n 378 | } 379 | -------------------------------------------------------------------------------- /fflib/v1/fold.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | /* Portions of this file are on Go stdlib's encoding/json/fold.go */ 19 | // Copyright 2009 The Go Authors. All rights reserved. 20 | // Use of this source code is governed by a BSD-style 21 | // license that can be found in the LICENSE file. 22 | 23 | package v1 24 | 25 | import ( 26 | "unicode/utf8" 27 | ) 28 | 29 | const ( 30 | caseMask = ^byte(0x20) // Mask to ignore case in ASCII. 31 | kelvin = '\u212a' 32 | smallLongEss = '\u017f' 33 | ) 34 | 35 | // equalFoldRight is a specialization of bytes.EqualFold when s is 36 | // known to be all ASCII (including punctuation), but contains an 's', 37 | // 'S', 'k', or 'K', requiring a Unicode fold on the bytes in t. 38 | // See comments on foldFunc. 39 | func EqualFoldRight(s, t []byte) bool { 40 | for _, sb := range s { 41 | if len(t) == 0 { 42 | return false 43 | } 44 | tb := t[0] 45 | if tb < utf8.RuneSelf { 46 | if sb != tb { 47 | sbUpper := sb & caseMask 48 | if 'A' <= sbUpper && sbUpper <= 'Z' { 49 | if sbUpper != tb&caseMask { 50 | return false 51 | } 52 | } else { 53 | return false 54 | } 55 | } 56 | t = t[1:] 57 | continue 58 | } 59 | // sb is ASCII and t is not. t must be either kelvin 60 | // sign or long s; sb must be s, S, k, or K. 61 | tr, size := utf8.DecodeRune(t) 62 | switch sb { 63 | case 's', 'S': 64 | if tr != smallLongEss { 65 | return false 66 | } 67 | case 'k', 'K': 68 | if tr != kelvin { 69 | return false 70 | } 71 | default: 72 | return false 73 | } 74 | t = t[size:] 75 | 76 | } 77 | if len(t) > 0 { 78 | return false 79 | } 80 | return true 81 | } 82 | 83 | // asciiEqualFold is a specialization of bytes.EqualFold for use when 84 | // s is all ASCII (but may contain non-letters) and contains no 85 | // special-folding letters. 86 | // See comments on foldFunc. 87 | func AsciiEqualFold(s, t []byte) bool { 88 | if len(s) != len(t) { 89 | return false 90 | } 91 | for i, sb := range s { 92 | tb := t[i] 93 | if sb == tb { 94 | continue 95 | } 96 | if ('a' <= sb && sb <= 'z') || ('A' <= sb && sb <= 'Z') { 97 | if sb&caseMask != tb&caseMask { 98 | return false 99 | } 100 | } else { 101 | return false 102 | } 103 | } 104 | return true 105 | } 106 | 107 | // simpleLetterEqualFold is a specialization of bytes.EqualFold for 108 | // use when s is all ASCII letters (no underscores, etc) and also 109 | // doesn't contain 'k', 'K', 's', or 'S'. 110 | // See comments on foldFunc. 111 | func SimpleLetterEqualFold(s, t []byte) bool { 112 | if len(s) != len(t) { 113 | return false 114 | } 115 | for i, b := range s { 116 | if b&caseMask != t[i]&caseMask { 117 | return false 118 | } 119 | } 120 | return true 121 | } 122 | -------------------------------------------------------------------------------- /fflib/v1/internal/atoi.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | /* Portions of this file are on Go stdlib's strconv/atoi.go */ 19 | // Copyright 2009 The Go Authors. All rights reserved. 20 | // Use of this source code is governed by a BSD-style 21 | // license that can be found in the LICENSE file. 22 | 23 | package internal 24 | 25 | import ( 26 | "errors" 27 | "strconv" 28 | ) 29 | 30 | // ErrRange indicates that a value is out of range for the target type. 31 | var ErrRange = errors.New("value out of range") 32 | 33 | // ErrSyntax indicates that a value does not have the right syntax for the target type. 34 | var ErrSyntax = errors.New("invalid syntax") 35 | 36 | // A NumError records a failed conversion. 37 | type NumError struct { 38 | Func string // the failing function (ParseBool, ParseInt, ParseUint, ParseFloat) 39 | Num string // the input 40 | Err error // the reason the conversion failed (ErrRange, ErrSyntax) 41 | } 42 | 43 | func (e *NumError) Error() string { 44 | return "strconv." + e.Func + ": " + "parsing " + strconv.Quote(e.Num) + ": " + e.Err.Error() 45 | } 46 | 47 | func syntaxError(fn, str string) *NumError { 48 | return &NumError{fn, str, ErrSyntax} 49 | } 50 | 51 | func rangeError(fn, str string) *NumError { 52 | return &NumError{fn, str, ErrRange} 53 | } 54 | 55 | const intSize = 32 << uint(^uint(0)>>63) 56 | 57 | // IntSize is the size in bits of an int or uint value. 58 | const IntSize = intSize 59 | 60 | // Return the first number n such that n*base >= 1<<64. 61 | func cutoff64(base int) uint64 { 62 | if base < 2 { 63 | return 0 64 | } 65 | return (1<<64-1)/uint64(base) + 1 66 | } 67 | 68 | // ParseUint is like ParseInt but for unsigned numbers, and oeprating on []byte 69 | func ParseUint(s []byte, base int, bitSize int) (n uint64, err error) { 70 | var cutoff, maxVal uint64 71 | 72 | if bitSize == 0 { 73 | bitSize = int(IntSize) 74 | } 75 | 76 | s0 := s 77 | switch { 78 | case len(s) < 1: 79 | err = ErrSyntax 80 | goto Error 81 | 82 | case 2 <= base && base <= 36: 83 | // valid base; nothing to do 84 | 85 | case base == 0: 86 | // Look for octal, hex prefix. 87 | switch { 88 | case s[0] == '0' && len(s) > 1 && (s[1] == 'x' || s[1] == 'X'): 89 | base = 16 90 | s = s[2:] 91 | if len(s) < 1 { 92 | err = ErrSyntax 93 | goto Error 94 | } 95 | case s[0] == '0': 96 | base = 8 97 | default: 98 | base = 10 99 | } 100 | 101 | default: 102 | err = errors.New("invalid base " + strconv.Itoa(base)) 103 | goto Error 104 | } 105 | 106 | n = 0 107 | cutoff = cutoff64(base) 108 | maxVal = 1<= base { 126 | n = 0 127 | err = ErrSyntax 128 | goto Error 129 | } 130 | 131 | if n >= cutoff { 132 | // n*base overflows 133 | n = 1<<64 - 1 134 | err = ErrRange 135 | goto Error 136 | } 137 | n *= uint64(base) 138 | 139 | n1 := n + uint64(v) 140 | if n1 < n || n1 > maxVal { 141 | // n+v overflows 142 | n = 1<<64 - 1 143 | err = ErrRange 144 | goto Error 145 | } 146 | n = n1 147 | } 148 | 149 | return n, nil 150 | 151 | Error: 152 | return n, &NumError{"ParseUint", string(s0), err} 153 | } 154 | 155 | // ParseInt interprets a string s in the given base (2 to 36) and 156 | // returns the corresponding value i. If base == 0, the base is 157 | // implied by the string's prefix: base 16 for "0x", base 8 for 158 | // "0", and base 10 otherwise. 159 | // 160 | // The bitSize argument specifies the integer type 161 | // that the result must fit into. Bit sizes 0, 8, 16, 32, and 64 162 | // correspond to int, int8, int16, int32, and int64. 163 | // 164 | // The errors that ParseInt returns have concrete type *NumError 165 | // and include err.Num = s. If s is empty or contains invalid 166 | // digits, err.Err = ErrSyntax and the returned value is 0; 167 | // if the value corresponding to s cannot be represented by a 168 | // signed integer of the given size, err.Err = ErrRange and the 169 | // returned value is the maximum magnitude integer of the 170 | // appropriate bitSize and sign. 171 | func ParseInt(s []byte, base int, bitSize int) (i int64, err error) { 172 | const fnParseInt = "ParseInt" 173 | 174 | if bitSize == 0 { 175 | bitSize = int(IntSize) 176 | } 177 | 178 | // Empty string bad. 179 | if len(s) == 0 { 180 | return 0, syntaxError(fnParseInt, string(s)) 181 | } 182 | 183 | // Pick off leading sign. 184 | s0 := s 185 | neg := false 186 | if s[0] == '+' { 187 | s = s[1:] 188 | } else if s[0] == '-' { 189 | neg = true 190 | s = s[1:] 191 | } 192 | 193 | // Convert unsigned and check range. 194 | var un uint64 195 | un, err = ParseUint(s, base, bitSize) 196 | if err != nil && err.(*NumError).Err != ErrRange { 197 | err.(*NumError).Func = fnParseInt 198 | err.(*NumError).Num = string(s0) 199 | return 0, err 200 | } 201 | cutoff := uint64(1 << uint(bitSize-1)) 202 | if !neg && un >= cutoff { 203 | return int64(cutoff - 1), rangeError(fnParseInt, string(s0)) 204 | } 205 | if neg && un > cutoff { 206 | return -int64(cutoff), rangeError(fnParseInt, string(s0)) 207 | } 208 | n := int64(un) 209 | if neg { 210 | n = -n 211 | } 212 | return n, nil 213 | } 214 | -------------------------------------------------------------------------------- /fflib/v1/iota.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | /* Portions of this file are on Go stdlib's strconv/iota.go */ 19 | // Copyright 2009 The Go Authors. All rights reserved. 20 | // Use of this source code is governed by a BSD-style 21 | // license that can be found in the LICENSE file. 22 | 23 | package v1 24 | 25 | import ( 26 | "io" 27 | ) 28 | 29 | const ( 30 | digits = "0123456789abcdefghijklmnopqrstuvwxyz" 31 | digits01 = "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789" 32 | digits10 = "0000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999" 33 | ) 34 | 35 | var shifts = [len(digits) + 1]uint{ 36 | 1 << 1: 1, 37 | 1 << 2: 2, 38 | 1 << 3: 3, 39 | 1 << 4: 4, 40 | 1 << 5: 5, 41 | } 42 | 43 | var smallNumbers = [][]byte{ 44 | []byte("0"), 45 | []byte("1"), 46 | []byte("2"), 47 | []byte("3"), 48 | []byte("4"), 49 | []byte("5"), 50 | []byte("6"), 51 | []byte("7"), 52 | []byte("8"), 53 | []byte("9"), 54 | []byte("10"), 55 | } 56 | 57 | type FormatBitsWriter interface { 58 | io.Writer 59 | io.ByteWriter 60 | } 61 | 62 | type FormatBitsScratch struct{} 63 | 64 | // 65 | // DEPRECIATED: `scratch` is no longer used, FormatBits2 is available. 66 | // 67 | // FormatBits computes the string representation of u in the given base. 68 | // If neg is set, u is treated as negative int64 value. If append_ is 69 | // set, the string is appended to dst and the resulting byte slice is 70 | // returned as the first result value; otherwise the string is returned 71 | // as the second result value. 72 | // 73 | func FormatBits(scratch *FormatBitsScratch, dst FormatBitsWriter, u uint64, base int, neg bool) { 74 | FormatBits2(dst, u, base, neg) 75 | } 76 | 77 | // FormatBits2 computes the string representation of u in the given base. 78 | // If neg is set, u is treated as negative int64 value. If append_ is 79 | // set, the string is appended to dst and the resulting byte slice is 80 | // returned as the first result value; otherwise the string is returned 81 | // as the second result value. 82 | // 83 | func FormatBits2(dst FormatBitsWriter, u uint64, base int, neg bool) { 84 | if base < 2 || base > len(digits) { 85 | panic("strconv: illegal AppendInt/FormatInt base") 86 | } 87 | // fast path for small common numbers 88 | if u <= 10 { 89 | if neg { 90 | dst.WriteByte('-') 91 | } 92 | dst.Write(smallNumbers[u]) 93 | return 94 | } 95 | 96 | // 2 <= base && base <= len(digits) 97 | 98 | var a = makeSlice(65) 99 | // var a [64 + 1]byte // +1 for sign of 64bit value in base 2 100 | i := len(a) 101 | 102 | if neg { 103 | u = -u 104 | } 105 | 106 | // convert bits 107 | if base == 10 { 108 | // common case: use constants for / and % because 109 | // the compiler can optimize it into a multiply+shift, 110 | // and unroll loop 111 | for u >= 100 { 112 | i -= 2 113 | q := u / 100 114 | j := uintptr(u - q*100) 115 | a[i+1] = digits01[j] 116 | a[i+0] = digits10[j] 117 | u = q 118 | } 119 | if u >= 10 { 120 | i-- 121 | q := u / 10 122 | a[i] = digits[uintptr(u-q*10)] 123 | u = q 124 | } 125 | 126 | } else if s := shifts[base]; s > 0 { 127 | // base is power of 2: use shifts and masks instead of / and % 128 | b := uint64(base) 129 | m := uintptr(b) - 1 // == 1<= b { 131 | i-- 132 | a[i] = digits[uintptr(u)&m] 133 | u >>= s 134 | } 135 | 136 | } else { 137 | // general case 138 | b := uint64(base) 139 | for u >= b { 140 | i-- 141 | a[i] = digits[uintptr(u%b)] 142 | u /= b 143 | } 144 | } 145 | 146 | // u < base 147 | i-- 148 | a[i] = digits[uintptr(u)] 149 | 150 | // add sign, if any 151 | if neg { 152 | i-- 153 | a[i] = '-' 154 | } 155 | 156 | dst.Write(a[i:]) 157 | 158 | Pool(a) 159 | 160 | return 161 | } 162 | -------------------------------------------------------------------------------- /fflib/v1/jsonstring_test.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package v1 19 | 20 | import ( 21 | "bytes" 22 | "testing" 23 | ) 24 | 25 | func TestWriteJsonString(t *testing.T) { 26 | var buf bytes.Buffer 27 | WriteJsonString(&buf, "foo") 28 | if string(buf.Bytes()) != `"foo"` { 29 | t.Fatalf("Expected: %v\nGot: %v", `"foo"`, string(buf.Bytes())) 30 | } 31 | 32 | buf.Reset() 33 | WriteJsonString(&buf, `f"oo`) 34 | if string(buf.Bytes()) != `"f\"oo"` { 35 | t.Fatalf("Expected: %v\nGot: %v", `"f\"oo"`, string(buf.Bytes())) 36 | } 37 | // TODO(pquerna): all them important tests. 38 | } 39 | -------------------------------------------------------------------------------- /fflib/v1/lexer_test.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package v1 19 | 20 | import ( 21 | "bytes" 22 | "errors" 23 | "strconv" 24 | "testing" 25 | ) 26 | 27 | func scanAll(ffl *FFLexer) []FFTok { 28 | rv := make([]FFTok, 0, 0) 29 | for { 30 | tok := ffl.Scan() 31 | rv = append(rv, tok) 32 | if tok == FFTok_eof || tok == FFTok_error { 33 | break 34 | } 35 | } 36 | 37 | return rv 38 | } 39 | 40 | func assertTokensEqual(t *testing.T, a []FFTok, b []FFTok) { 41 | 42 | if len(a) != len(b) { 43 | t.Fatalf("Token lists of mixed length: expected=%v found=%v", a, b) 44 | return 45 | } 46 | 47 | for i, v := range a { 48 | if b[i] != v { 49 | t.Fatalf("Invalid Token: expected=%d found=%d token=%d", 50 | v, b, i) 51 | return 52 | } 53 | } 54 | } 55 | 56 | func scanToTok(ffl *FFLexer, targetTok FFTok) error { 57 | _, err := scanToTokCount(ffl, targetTok) 58 | return err 59 | } 60 | 61 | func scanToTokCount(ffl *FFLexer, targetTok FFTok) (int, error) { 62 | c := 0 63 | for { 64 | tok := ffl.Scan() 65 | c++ 66 | 67 | if tok == targetTok { 68 | return c, nil 69 | } 70 | 71 | if tok == FFTok_error { 72 | return c, errors.New("Hit error before target token") 73 | } 74 | if tok == FFTok_eof { 75 | return c, errors.New("Hit EOF before target token") 76 | } 77 | } 78 | } 79 | 80 | func TestBasicLexing(t *testing.T) { 81 | ffl := NewFFLexer([]byte(`{}`)) 82 | toks := scanAll(ffl) 83 | assertTokensEqual(t, []FFTok{ 84 | FFTok_left_bracket, 85 | FFTok_right_bracket, 86 | FFTok_eof, 87 | }, toks) 88 | } 89 | 90 | func TestHelloWorld(t *testing.T) { 91 | ffl := NewFFLexer([]byte(`{"hello":"world"}`)) 92 | toks := scanAll(ffl) 93 | assertTokensEqual(t, []FFTok{ 94 | FFTok_left_bracket, 95 | FFTok_string, 96 | FFTok_colon, 97 | FFTok_string, 98 | FFTok_right_bracket, 99 | FFTok_eof, 100 | }, toks) 101 | 102 | ffl = NewFFLexer([]byte(`{"hello": 1}`)) 103 | toks = scanAll(ffl) 104 | assertTokensEqual(t, []FFTok{ 105 | FFTok_left_bracket, 106 | FFTok_string, 107 | FFTok_colon, 108 | FFTok_integer, 109 | FFTok_right_bracket, 110 | FFTok_eof, 111 | }, toks) 112 | 113 | ffl = NewFFLexer([]byte(`{"hello": 1.0}`)) 114 | toks = scanAll(ffl) 115 | assertTokensEqual(t, []FFTok{ 116 | FFTok_left_bracket, 117 | FFTok_string, 118 | FFTok_colon, 119 | FFTok_double, 120 | FFTok_right_bracket, 121 | FFTok_eof, 122 | }, toks) 123 | 124 | ffl = NewFFLexer([]byte(`{"hello": 1e2}`)) 125 | toks = scanAll(ffl) 126 | assertTokensEqual(t, []FFTok{ 127 | FFTok_left_bracket, 128 | FFTok_string, 129 | FFTok_colon, 130 | FFTok_double, 131 | FFTok_right_bracket, 132 | FFTok_eof, 133 | }, toks) 134 | 135 | ffl = NewFFLexer([]byte(`{"hello": {}}`)) 136 | toks = scanAll(ffl) 137 | assertTokensEqual(t, []FFTok{ 138 | FFTok_left_bracket, 139 | FFTok_string, 140 | FFTok_colon, 141 | FFTok_left_bracket, 142 | FFTok_right_bracket, 143 | FFTok_right_bracket, 144 | FFTok_eof, 145 | }, toks) 146 | 147 | ffl = NewFFLexer([]byte(`{"hello": {"blah": null}}`)) 148 | toks = scanAll(ffl) 149 | assertTokensEqual(t, []FFTok{ 150 | FFTok_left_bracket, 151 | FFTok_string, 152 | FFTok_colon, 153 | FFTok_left_bracket, 154 | FFTok_string, 155 | FFTok_colon, 156 | FFTok_null, 157 | FFTok_right_bracket, 158 | FFTok_right_bracket, 159 | FFTok_eof, 160 | }, toks) 161 | 162 | ffl = NewFFLexer([]byte(`{"hello": /* comment */ 0}`)) 163 | toks = scanAll(ffl) 164 | assertTokensEqual(t, []FFTok{ 165 | FFTok_left_bracket, 166 | FFTok_string, 167 | FFTok_colon, 168 | FFTok_comment, 169 | FFTok_integer, 170 | FFTok_right_bracket, 171 | FFTok_eof, 172 | }, toks) 173 | 174 | ffl = NewFFLexer([]byte(`{"hello": / comment`)) 175 | toks = scanAll(ffl) 176 | assertTokensEqual(t, []FFTok{ 177 | FFTok_left_bracket, 178 | FFTok_string, 179 | FFTok_colon, 180 | FFTok_error, 181 | }, toks) 182 | 183 | ffl = NewFFLexer([]byte(`{"陫ʋsş\")珷\u003cºɖgȏ哙ȍ":"2ħ籦ö嗏ʑ\u003e季"}`)) 184 | toks = scanAll(ffl) 185 | assertTokensEqual(t, []FFTok{ 186 | FFTok_left_bracket, 187 | FFTok_string, 188 | FFTok_colon, 189 | FFTok_string, 190 | FFTok_right_bracket, 191 | FFTok_eof, 192 | }, toks) 193 | 194 | ffl = NewFFLexer([]byte(`{"X":{"陫ʋsş\")珷\u003cºɖgȏ哙ȍ":"2ħ籦ö嗏ʑ\u003e季"}}`)) 195 | toks = scanAll(ffl) 196 | assertTokensEqual(t, []FFTok{ 197 | FFTok_left_bracket, 198 | FFTok_string, 199 | FFTok_colon, 200 | FFTok_left_bracket, 201 | FFTok_string, 202 | FFTok_colon, 203 | FFTok_string, 204 | FFTok_right_bracket, 205 | FFTok_right_bracket, 206 | FFTok_eof, 207 | }, toks) 208 | } 209 | 210 | func tDouble(t *testing.T, input string, target float64) { 211 | ffl := NewFFLexer([]byte(input)) 212 | err := scanToTok(ffl, FFTok_double) 213 | if err != nil { 214 | t.Fatalf("scanToTok failed, couldnt find double: %v input: %v", err, input) 215 | } 216 | 217 | f64, err := strconv.ParseFloat(ffl.Output.String(), 64) 218 | if err != nil { 219 | t.Fatalf("ParseFloat failed, shouldnt of: %v input: %v", err, input) 220 | } 221 | 222 | if int64(f64*1000) != int64(target*1000) { 223 | t.Fatalf("ffl.Output: expected f64 '%v', got: %v from: %v input: %v", 224 | target, f64, ffl.Output.String(), input) 225 | } 226 | 227 | err = scanToTok(ffl, FFTok_eof) 228 | if err != nil { 229 | t.Fatalf("Failed to find EOF after double. input: %v", input) 230 | } 231 | } 232 | 233 | func TestDouble(t *testing.T) { 234 | tDouble(t, `{"a": 1.2}`, 1.2) 235 | tDouble(t, `{"a": 1.2e2}`, 1.2e2) 236 | tDouble(t, `{"a": -1.2e2}`, -1.2e2) 237 | tDouble(t, `{"a": 1.2e-2}`, 1.2e-2) 238 | tDouble(t, `{"a": -1.2e-2}`, -1.2e-2) 239 | } 240 | 241 | func tInt(t *testing.T, input string, target int64) { 242 | ffl := NewFFLexer([]byte(input)) 243 | err := scanToTok(ffl, FFTok_integer) 244 | if err != nil { 245 | t.Fatalf("scanToTok failed, couldnt find int: %v input: %v", err, input) 246 | } 247 | 248 | // Bit sizes 0, 8, 16, 32, and 64 correspond to int, int8, int16, int32, and int64. 249 | i64, err := strconv.ParseInt(ffl.Output.String(), 10, 64) 250 | if err != nil { 251 | t.Fatalf("ParseInt failed, shouldnt of: %v input: %v", err, input) 252 | } 253 | 254 | if i64 != target { 255 | t.Fatalf("ffl.Output: expected i64 '%v', got: %v from: %v", target, i64, ffl.Output.String()) 256 | } 257 | 258 | err = scanToTok(ffl, FFTok_eof) 259 | if err != nil { 260 | t.Fatalf("Failed to find EOF after int. input: %v", input) 261 | } 262 | } 263 | 264 | func TestInt(t *testing.T) { 265 | tInt(t, `{"a": 2000}`, 2000) 266 | tInt(t, `{"a": -2000}`, -2000) 267 | tInt(t, `{"a": 0}`, 0) 268 | tInt(t, `{"a": -0}`, -0) 269 | } 270 | 271 | func tError(t *testing.T, input string, targetCount int, targetError FFErr) { 272 | ffl := NewFFLexer([]byte(input)) 273 | count, err := scanToTokCount(ffl, FFTok_error) 274 | if err != nil { 275 | t.Fatalf("scanToTok failed, couldnt find error token: %v input: %v", err, input) 276 | } 277 | 278 | if count != targetCount { 279 | t.Fatalf("Expected error token at offset %v, but found it at %v input: %v", 280 | count, targetCount, input) 281 | } 282 | 283 | if ffl.Error != targetError { 284 | t.Fatalf("Expected error token %v, but got %v input: %v", 285 | targetError, ffl.Error, input) 286 | } 287 | 288 | line, char := ffl.reader.PosWithLine() 289 | if line == 0 || char == 0 { 290 | t.Fatalf("ffl.PosWithLine(): expected >=0 values. line=%v char=%v", 291 | line, char) 292 | } 293 | 294 | berr := ffl.WrapErr(ffl.Error.ToError()) 295 | if berr == nil { 296 | t.Fatalf("expected error") 297 | } 298 | 299 | } 300 | 301 | func TestInvalid(t *testing.T) { 302 | tError(t, `{"a": nul}`, 4, FFErr_invalid_string) 303 | tError(t, `{"a": 1.a}`, 4, FFErr_missing_integer_after_decimal) 304 | } 305 | 306 | func TestCapture(t *testing.T) { 307 | ffl := NewFFLexer([]byte(`{"hello": {"blah": [null, 1]}}`)) 308 | 309 | err := scanToTok(ffl, FFTok_left_bracket) 310 | if err != nil { 311 | t.Fatalf("scanToTok failed: %v", err) 312 | } 313 | 314 | err = scanToTok(ffl, FFTok_left_bracket) 315 | if err != nil { 316 | t.Fatalf("scanToTok failed: %v", err) 317 | } 318 | 319 | buf, err := ffl.CaptureField(FFTok_left_bracket) 320 | if err != nil { 321 | t.Fatalf("CaptureField failed: %v", err) 322 | } 323 | 324 | if bytes.Compare(buf, []byte(`{"blah": [null, 1]}`)) != 0 { 325 | t.Fatalf("didnt capture subfield: buf: %v", string(buf)) 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /fflib/v1/reader.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package v1 19 | 20 | import ( 21 | "fmt" 22 | "io" 23 | "unicode" 24 | "unicode/utf16" 25 | ) 26 | 27 | const sliceStringMask = cIJC | cNFP 28 | 29 | type ffReader struct { 30 | s []byte 31 | i int 32 | l int 33 | } 34 | 35 | func newffReader(d []byte) *ffReader { 36 | return &ffReader{ 37 | s: d, 38 | i: 0, 39 | l: len(d), 40 | } 41 | } 42 | 43 | func (r *ffReader) Slice(start, stop int) []byte { 44 | return r.s[start:stop] 45 | } 46 | 47 | func (r *ffReader) Pos() int { 48 | return r.i 49 | } 50 | 51 | // Reset the reader, and add new input. 52 | func (r *ffReader) Reset(d []byte) { 53 | r.s = d 54 | r.i = 0 55 | r.l = len(d) 56 | } 57 | 58 | // Calculates the Position with line and line offset, 59 | // because this isn't counted for performance reasons, 60 | // it will iterate the buffer from the beginning, and should 61 | // only be used in error-paths. 62 | func (r *ffReader) PosWithLine() (int, int) { 63 | currentLine := 1 64 | currentChar := 0 65 | 66 | for i := 0; i < r.i; i++ { 67 | c := r.s[i] 68 | currentChar++ 69 | if c == '\n' { 70 | currentLine++ 71 | currentChar = 0 72 | } 73 | } 74 | 75 | return currentLine, currentChar 76 | } 77 | 78 | func (r *ffReader) ReadByteNoWS() (byte, error) { 79 | if r.i >= r.l { 80 | return 0, io.EOF 81 | } 82 | 83 | j := r.i 84 | 85 | for { 86 | c := r.s[j] 87 | j++ 88 | 89 | // inline whitespace parsing gives another ~8% performance boost 90 | // for many kinds of nicely indented JSON. 91 | // ... and using a [255]bool instead of multiple ifs, gives another 2% 92 | /* 93 | if c != '\t' && 94 | c != '\n' && 95 | c != '\v' && 96 | c != '\f' && 97 | c != '\r' && 98 | c != ' ' { 99 | r.i = j 100 | return c, nil 101 | } 102 | */ 103 | if whitespaceLookupTable[c] == false { 104 | r.i = j 105 | return c, nil 106 | } 107 | 108 | if j >= r.l { 109 | return 0, io.EOF 110 | } 111 | } 112 | } 113 | 114 | func (r *ffReader) ReadByte() (byte, error) { 115 | if r.i >= r.l { 116 | return 0, io.EOF 117 | } 118 | 119 | r.i++ 120 | 121 | return r.s[r.i-1], nil 122 | } 123 | 124 | func (r *ffReader) UnreadByte() error { 125 | if r.i <= 0 { 126 | panic("ffReader.UnreadByte: at beginning of slice") 127 | } 128 | r.i-- 129 | return nil 130 | } 131 | 132 | func (r *ffReader) readU4(j int) (rune, error) { 133 | 134 | var u4 [4]byte 135 | for i := 0; i < 4; i++ { 136 | if j >= r.l { 137 | return -1, io.EOF 138 | } 139 | c := r.s[j] 140 | if byteLookupTable[c]&cVHC != 0 { 141 | u4[i] = c 142 | j++ 143 | continue 144 | } else { 145 | // TODO(pquerna): handle errors better. layering violation. 146 | return -1, fmt.Errorf("lex_string_invalid_hex_char: %v %v", c, string(u4[:])) 147 | } 148 | } 149 | 150 | // TODO(pquerna): utf16.IsSurrogate 151 | rr, err := ParseUint(u4[:], 16, 64) 152 | if err != nil { 153 | return -1, err 154 | } 155 | return rune(rr), nil 156 | } 157 | 158 | func (r *ffReader) handleEscaped(c byte, j int, out DecodingBuffer) (int, error) { 159 | if j >= r.l { 160 | return 0, io.EOF 161 | } 162 | 163 | c = r.s[j] 164 | j++ 165 | 166 | if c == 'u' { 167 | ru, err := r.readU4(j) 168 | if err != nil { 169 | return 0, err 170 | } 171 | 172 | if utf16.IsSurrogate(ru) { 173 | ru2, err := r.readU4(j + 6) 174 | if err != nil { 175 | return 0, err 176 | } 177 | out.Write(r.s[r.i : j-2]) 178 | r.i = j + 10 179 | j = r.i 180 | rval := utf16.DecodeRune(ru, ru2) 181 | if rval != unicode.ReplacementChar { 182 | out.WriteRune(rval) 183 | } else { 184 | return 0, fmt.Errorf("lex_string_invalid_unicode_surrogate: %v %v", ru, ru2) 185 | } 186 | } else { 187 | out.Write(r.s[r.i : j-2]) 188 | r.i = j + 4 189 | j = r.i 190 | out.WriteRune(ru) 191 | } 192 | return j, nil 193 | } else if byteLookupTable[c]&cVEC == 0 { 194 | return 0, fmt.Errorf("lex_string_invalid_escaped_char: %v", c) 195 | } else { 196 | out.Write(r.s[r.i : j-2]) 197 | r.i = j 198 | j = r.i 199 | 200 | switch c { 201 | case '"': 202 | out.WriteByte('"') 203 | case '\\': 204 | out.WriteByte('\\') 205 | case '/': 206 | out.WriteByte('/') 207 | case 'b': 208 | out.WriteByte('\b') 209 | case 'f': 210 | out.WriteByte('\f') 211 | case 'n': 212 | out.WriteByte('\n') 213 | case 'r': 214 | out.WriteByte('\r') 215 | case 't': 216 | out.WriteByte('\t') 217 | } 218 | } 219 | 220 | return j, nil 221 | } 222 | 223 | func (r *ffReader) SliceString(out DecodingBuffer) error { 224 | var c byte 225 | // TODO(pquerna): string_with_escapes? de-escape here? 226 | j := r.i 227 | 228 | for { 229 | if j >= r.l { 230 | return io.EOF 231 | } 232 | 233 | j, c = scanString(r.s, j) 234 | 235 | if c == '"' { 236 | if j != r.i { 237 | out.Write(r.s[r.i : j-1]) 238 | r.i = j 239 | } 240 | return nil 241 | } else if c == '\\' { 242 | var err error 243 | j, err = r.handleEscaped(c, j, out) 244 | if err != nil { 245 | return err 246 | } 247 | } else if byteLookupTable[c]&cIJC != 0 { 248 | return fmt.Errorf("lex_string_invalid_json_char: %v", c) 249 | } 250 | continue 251 | } 252 | } 253 | 254 | // TODO(pquerna): consider combining wibth the normal byte mask. 255 | var whitespaceLookupTable [256]bool = [256]bool{ 256 | false, /* 0 */ 257 | false, /* 1 */ 258 | false, /* 2 */ 259 | false, /* 3 */ 260 | false, /* 4 */ 261 | false, /* 5 */ 262 | false, /* 6 */ 263 | false, /* 7 */ 264 | false, /* 8 */ 265 | true, /* 9 */ 266 | true, /* 10 */ 267 | true, /* 11 */ 268 | true, /* 12 */ 269 | true, /* 13 */ 270 | false, /* 14 */ 271 | false, /* 15 */ 272 | false, /* 16 */ 273 | false, /* 17 */ 274 | false, /* 18 */ 275 | false, /* 19 */ 276 | false, /* 20 */ 277 | false, /* 21 */ 278 | false, /* 22 */ 279 | false, /* 23 */ 280 | false, /* 24 */ 281 | false, /* 25 */ 282 | false, /* 26 */ 283 | false, /* 27 */ 284 | false, /* 28 */ 285 | false, /* 29 */ 286 | false, /* 30 */ 287 | false, /* 31 */ 288 | true, /* 32 */ 289 | false, /* 33 */ 290 | false, /* 34 */ 291 | false, /* 35 */ 292 | false, /* 36 */ 293 | false, /* 37 */ 294 | false, /* 38 */ 295 | false, /* 39 */ 296 | false, /* 40 */ 297 | false, /* 41 */ 298 | false, /* 42 */ 299 | false, /* 43 */ 300 | false, /* 44 */ 301 | false, /* 45 */ 302 | false, /* 46 */ 303 | false, /* 47 */ 304 | false, /* 48 */ 305 | false, /* 49 */ 306 | false, /* 50 */ 307 | false, /* 51 */ 308 | false, /* 52 */ 309 | false, /* 53 */ 310 | false, /* 54 */ 311 | false, /* 55 */ 312 | false, /* 56 */ 313 | false, /* 57 */ 314 | false, /* 58 */ 315 | false, /* 59 */ 316 | false, /* 60 */ 317 | false, /* 61 */ 318 | false, /* 62 */ 319 | false, /* 63 */ 320 | false, /* 64 */ 321 | false, /* 65 */ 322 | false, /* 66 */ 323 | false, /* 67 */ 324 | false, /* 68 */ 325 | false, /* 69 */ 326 | false, /* 70 */ 327 | false, /* 71 */ 328 | false, /* 72 */ 329 | false, /* 73 */ 330 | false, /* 74 */ 331 | false, /* 75 */ 332 | false, /* 76 */ 333 | false, /* 77 */ 334 | false, /* 78 */ 335 | false, /* 79 */ 336 | false, /* 80 */ 337 | false, /* 81 */ 338 | false, /* 82 */ 339 | false, /* 83 */ 340 | false, /* 84 */ 341 | false, /* 85 */ 342 | false, /* 86 */ 343 | false, /* 87 */ 344 | false, /* 88 */ 345 | false, /* 89 */ 346 | false, /* 90 */ 347 | false, /* 91 */ 348 | false, /* 92 */ 349 | false, /* 93 */ 350 | false, /* 94 */ 351 | false, /* 95 */ 352 | false, /* 96 */ 353 | false, /* 97 */ 354 | false, /* 98 */ 355 | false, /* 99 */ 356 | false, /* 100 */ 357 | false, /* 101 */ 358 | false, /* 102 */ 359 | false, /* 103 */ 360 | false, /* 104 */ 361 | false, /* 105 */ 362 | false, /* 106 */ 363 | false, /* 107 */ 364 | false, /* 108 */ 365 | false, /* 109 */ 366 | false, /* 110 */ 367 | false, /* 111 */ 368 | false, /* 112 */ 369 | false, /* 113 */ 370 | false, /* 114 */ 371 | false, /* 115 */ 372 | false, /* 116 */ 373 | false, /* 117 */ 374 | false, /* 118 */ 375 | false, /* 119 */ 376 | false, /* 120 */ 377 | false, /* 121 */ 378 | false, /* 122 */ 379 | false, /* 123 */ 380 | false, /* 124 */ 381 | false, /* 125 */ 382 | false, /* 126 */ 383 | false, /* 127 */ 384 | false, /* 128 */ 385 | false, /* 129 */ 386 | false, /* 130 */ 387 | false, /* 131 */ 388 | false, /* 132 */ 389 | false, /* 133 */ 390 | false, /* 134 */ 391 | false, /* 135 */ 392 | false, /* 136 */ 393 | false, /* 137 */ 394 | false, /* 138 */ 395 | false, /* 139 */ 396 | false, /* 140 */ 397 | false, /* 141 */ 398 | false, /* 142 */ 399 | false, /* 143 */ 400 | false, /* 144 */ 401 | false, /* 145 */ 402 | false, /* 146 */ 403 | false, /* 147 */ 404 | false, /* 148 */ 405 | false, /* 149 */ 406 | false, /* 150 */ 407 | false, /* 151 */ 408 | false, /* 152 */ 409 | false, /* 153 */ 410 | false, /* 154 */ 411 | false, /* 155 */ 412 | false, /* 156 */ 413 | false, /* 157 */ 414 | false, /* 158 */ 415 | false, /* 159 */ 416 | false, /* 160 */ 417 | false, /* 161 */ 418 | false, /* 162 */ 419 | false, /* 163 */ 420 | false, /* 164 */ 421 | false, /* 165 */ 422 | false, /* 166 */ 423 | false, /* 167 */ 424 | false, /* 168 */ 425 | false, /* 169 */ 426 | false, /* 170 */ 427 | false, /* 171 */ 428 | false, /* 172 */ 429 | false, /* 173 */ 430 | false, /* 174 */ 431 | false, /* 175 */ 432 | false, /* 176 */ 433 | false, /* 177 */ 434 | false, /* 178 */ 435 | false, /* 179 */ 436 | false, /* 180 */ 437 | false, /* 181 */ 438 | false, /* 182 */ 439 | false, /* 183 */ 440 | false, /* 184 */ 441 | false, /* 185 */ 442 | false, /* 186 */ 443 | false, /* 187 */ 444 | false, /* 188 */ 445 | false, /* 189 */ 446 | false, /* 190 */ 447 | false, /* 191 */ 448 | false, /* 192 */ 449 | false, /* 193 */ 450 | false, /* 194 */ 451 | false, /* 195 */ 452 | false, /* 196 */ 453 | false, /* 197 */ 454 | false, /* 198 */ 455 | false, /* 199 */ 456 | false, /* 200 */ 457 | false, /* 201 */ 458 | false, /* 202 */ 459 | false, /* 203 */ 460 | false, /* 204 */ 461 | false, /* 205 */ 462 | false, /* 206 */ 463 | false, /* 207 */ 464 | false, /* 208 */ 465 | false, /* 209 */ 466 | false, /* 210 */ 467 | false, /* 211 */ 468 | false, /* 212 */ 469 | false, /* 213 */ 470 | false, /* 214 */ 471 | false, /* 215 */ 472 | false, /* 216 */ 473 | false, /* 217 */ 474 | false, /* 218 */ 475 | false, /* 219 */ 476 | false, /* 220 */ 477 | false, /* 221 */ 478 | false, /* 222 */ 479 | false, /* 223 */ 480 | false, /* 224 */ 481 | false, /* 225 */ 482 | false, /* 226 */ 483 | false, /* 227 */ 484 | false, /* 228 */ 485 | false, /* 229 */ 486 | false, /* 230 */ 487 | false, /* 231 */ 488 | false, /* 232 */ 489 | false, /* 233 */ 490 | false, /* 234 */ 491 | false, /* 235 */ 492 | false, /* 236 */ 493 | false, /* 237 */ 494 | false, /* 238 */ 495 | false, /* 239 */ 496 | false, /* 240 */ 497 | false, /* 241 */ 498 | false, /* 242 */ 499 | false, /* 243 */ 500 | false, /* 244 */ 501 | false, /* 245 */ 502 | false, /* 246 */ 503 | false, /* 247 */ 504 | false, /* 248 */ 505 | false, /* 249 */ 506 | false, /* 250 */ 507 | false, /* 251 */ 508 | false, /* 252 */ 509 | false, /* 253 */ 510 | false, /* 254 */ 511 | false, /* 255 */ 512 | } 513 | -------------------------------------------------------------------------------- /fflib/v1/reader_scan_generic.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package v1 19 | 20 | func scanString(s []byte, j int) (int, byte) { 21 | for { 22 | if j >= len(s) { 23 | return j, 0 24 | } 25 | 26 | c := s[j] 27 | j++ 28 | if byteLookupTable[c]&sliceStringMask == 0 { 29 | continue 30 | } 31 | 32 | return j, c 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /fflib/v1/reader_test.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package v1 19 | 20 | import ( 21 | "testing" 22 | ) 23 | 24 | func tsliceString(t *testing.T, expected string, enc string) { 25 | var out Buffer 26 | ffr := newffReader([]byte(enc + `"`)) 27 | err := ffr.SliceString(&out) 28 | if err != nil { 29 | t.Fatalf("unexpect SliceString error: %v from %v", err, enc) 30 | } 31 | 32 | if out.String() != expected { 33 | t.Fatalf(`failed to decode %v into %v, got: %v`, enc, expected, out.String()) 34 | } 35 | } 36 | 37 | func TestUnicode(t *testing.T) { 38 | var testvecs = map[string]string{ 39 | "€": `\u20AC`, 40 | "𐐷": `\uD801\uDC37`, 41 | } 42 | 43 | for k, v := range testvecs { 44 | tsliceString(t, k, v) 45 | } 46 | } 47 | 48 | func TestBadUnicode(t *testing.T) { 49 | var out Buffer 50 | ffr := newffReader([]byte(`\u20--"`)) 51 | err := ffr.SliceString(&out) 52 | if err == nil { 53 | t.Fatalf("expected SliceString hex decode error") 54 | } 55 | } 56 | 57 | func TestNonUnicodeEscape(t *testing.T) { 58 | var out Buffer 59 | ffr := newffReader([]byte(`\t\n\r"`)) 60 | err := ffr.SliceString(&out) 61 | if err != nil { 62 | t.Fatalf("unexpected SliceString error: %v", err) 63 | } 64 | } 65 | 66 | func TestInvalidEscape(t *testing.T) { 67 | var out Buffer 68 | ffr := newffReader([]byte(`\x134"`)) 69 | err := ffr.SliceString(&out) 70 | if err == nil { 71 | t.Fatalf("expected SliceString escape decode error") 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /generator/generator.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package generator 19 | 20 | import ( 21 | "errors" 22 | "fmt" 23 | "os" 24 | ) 25 | 26 | func GenerateFiles(goCmd string, inputPath string, outputPath string, importName string, forceRegenerate bool, resetFields bool) error { 27 | 28 | if _, StatErr := os.Stat(outputPath); !os.IsNotExist(StatErr) { 29 | inputFileInfo, inputFileErr := os.Stat(inputPath) 30 | outputFileInfo, outputFileErr := os.Stat(outputPath) 31 | 32 | if nil == outputFileErr && nil == inputFileErr { 33 | if !forceRegenerate && inputFileInfo.ModTime().Before(outputFileInfo.ModTime()) { 34 | fmt.Println("File " + outputPath + " already exists.") 35 | 36 | return nil 37 | } 38 | } 39 | } 40 | 41 | packageName, structs, err := ExtractStructs(inputPath) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | im := NewInceptionMain(goCmd, inputPath, outputPath, resetFields) 47 | 48 | err = im.Generate(packageName, structs, importName) 49 | if err != nil { 50 | return errors.New(fmt.Sprintf("error=%v path=%q", err, im.TempMainPath)) 51 | } 52 | 53 | err = im.Run() 54 | if err != nil { 55 | return err 56 | } 57 | 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /generator/inceptionmain.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package generator 19 | 20 | import ( 21 | "bytes" 22 | "errors" 23 | "fmt" 24 | "go/format" 25 | "io/ioutil" 26 | "os" 27 | "os/exec" 28 | "path/filepath" 29 | "strings" 30 | "text/template" 31 | 32 | "github.com/pquerna/ffjson/shared" 33 | ) 34 | 35 | const inceptionMainTemplate = ` 36 | // DO NOT EDIT! 37 | // Code generated by ffjson 38 | // DO NOT EDIT! 39 | 40 | package main 41 | 42 | import ( 43 | "github.com/pquerna/ffjson/inception" 44 | importedinceptionpackage "{{.ImportName}}" 45 | ) 46 | 47 | func main() { 48 | i := ffjsoninception.NewInception("{{.InputPath}}", "{{.PackageName}}", "{{.OutputPath}}", {{.ResetFields}}) 49 | i.AddMany(importedinceptionpackage.FFJSONExpose()) 50 | i.Execute() 51 | } 52 | ` 53 | 54 | const ffjsonExposeTemplate = ` 55 | // Code generated by ffjson 56 | // 57 | // This should be automatically deleted by running 'ffjson', 58 | // if leftover, please delete it. 59 | 60 | package {{.PackageName}} 61 | 62 | import ( 63 | ffjsonshared "github.com/pquerna/ffjson/shared" 64 | ) 65 | 66 | func FFJSONExpose() []ffjsonshared.InceptionType { 67 | rv := make([]ffjsonshared.InceptionType, 0) 68 | {{range .StructNames}} 69 | rv = append(rv, ffjsonshared.InceptionType{Obj: {{.Name}}{}, Options: ffjson{{printf "%#v" .Options}} } ) 70 | {{end}} 71 | return rv 72 | } 73 | ` 74 | 75 | type structName struct { 76 | Name string 77 | Options shared.StructOptions 78 | } 79 | 80 | type templateCtx struct { 81 | StructNames []structName 82 | ImportName string 83 | PackageName string 84 | InputPath string 85 | OutputPath string 86 | ResetFields bool 87 | } 88 | 89 | type InceptionMain struct { 90 | goCmd string 91 | inputPath string 92 | exposePath string 93 | outputPath string 94 | TempMainPath string 95 | tempDir string 96 | tempMain *os.File 97 | tempExpose *os.File 98 | resetFields bool 99 | } 100 | 101 | func NewInceptionMain(goCmd string, inputPath string, outputPath string, resetFields bool) *InceptionMain { 102 | exposePath := getExposePath(inputPath) 103 | return &InceptionMain{ 104 | goCmd: goCmd, 105 | inputPath: inputPath, 106 | outputPath: outputPath, 107 | exposePath: exposePath, 108 | resetFields: resetFields, 109 | } 110 | } 111 | 112 | func getImportName(goCmd, inputPath string) (string, error) { 113 | p, err := filepath.Abs(inputPath) 114 | if err != nil { 115 | return "", err 116 | } 117 | dir := filepath.Dir(p) 118 | 119 | // `go list dir` gives back the module name 120 | // Should work for GOPATH as well as with modules 121 | // Errors if no go files are found 122 | cmd := exec.Command(goCmd, "list", dir) 123 | b, err := cmd.Output() 124 | if err == nil { 125 | return string(b[:len(b)-1]), nil 126 | } 127 | 128 | gopaths := strings.Split(os.Getenv("GOPATH"), string(os.PathListSeparator)) 129 | 130 | for _, path := range gopaths { 131 | gpath, err := filepath.Abs(path) 132 | if err != nil { 133 | continue 134 | } 135 | rel, err := filepath.Rel(filepath.ToSlash(gpath), dir) 136 | if err != nil { 137 | return "", err 138 | } 139 | 140 | if len(rel) < 4 || rel[:4] != "src"+string(os.PathSeparator) { 141 | continue 142 | } 143 | return rel[4:], nil 144 | } 145 | return "", errors.New(fmt.Sprintf("Could not find source directory: GOPATH=%q REL=%q", gopaths, dir)) 146 | 147 | } 148 | 149 | func getExposePath(inputPath string) string { 150 | return inputPath[0:len(inputPath)-3] + "_ffjson_expose.go" 151 | } 152 | 153 | func (im *InceptionMain) renderTpl(f *os.File, t *template.Template, tc *templateCtx) error { 154 | buf := new(bytes.Buffer) 155 | err := t.Execute(buf, tc) 156 | if err != nil { 157 | return err 158 | } 159 | formatted, err := format.Source(buf.Bytes()) 160 | if err != nil { 161 | return err 162 | } 163 | _, err = f.Write(formatted) 164 | return err 165 | } 166 | 167 | func (im *InceptionMain) Generate(packageName string, si []*StructInfo, importName string) error { 168 | var err error 169 | if importName == "" { 170 | importName, err = getImportName(im.goCmd, im.inputPath) 171 | if err != nil { 172 | return err 173 | } 174 | } 175 | 176 | im.tempDir, err = ioutil.TempDir(filepath.Dir(im.inputPath), "ffjson-inception") 177 | if err != nil { 178 | return err 179 | } 180 | 181 | importName = filepath.ToSlash(importName) 182 | // for `go run` to work, we must have a file ending in ".go". 183 | im.tempMain, err = TempFileWithPostfix(im.tempDir, "ffjson-inception", ".go") 184 | if err != nil { 185 | return err 186 | } 187 | 188 | im.TempMainPath = im.tempMain.Name() 189 | sn := make([]structName, len(si)) 190 | for i, st := range si { 191 | sn[i].Name = st.Name 192 | sn[i].Options = st.Options 193 | } 194 | 195 | tc := &templateCtx{ 196 | ImportName: importName, 197 | PackageName: packageName, 198 | StructNames: sn, 199 | InputPath: im.inputPath, 200 | OutputPath: im.outputPath, 201 | ResetFields: im.resetFields, 202 | } 203 | 204 | t := template.Must(template.New("inception.go").Parse(inceptionMainTemplate)) 205 | 206 | err = im.renderTpl(im.tempMain, t, tc) 207 | if err != nil { 208 | return err 209 | } 210 | 211 | im.tempExpose, err = os.Create(im.exposePath) 212 | if err != nil { 213 | return err 214 | } 215 | 216 | t = template.Must(template.New("ffjson_expose.go").Parse(ffjsonExposeTemplate)) 217 | 218 | err = im.renderTpl(im.tempExpose, t, tc) 219 | if err != nil { 220 | return err 221 | } 222 | 223 | return nil 224 | } 225 | 226 | func (im *InceptionMain) Run() error { 227 | var out bytes.Buffer 228 | var errOut bytes.Buffer 229 | 230 | cmd := exec.Command(im.goCmd, "run", "-a", im.TempMainPath) 231 | cmd.Stdout = &out 232 | cmd.Stderr = &errOut 233 | 234 | err := cmd.Run() 235 | 236 | if err != nil { 237 | return errors.New( 238 | fmt.Sprintf("Go Run Failed for: %s\nSTDOUT:\n%s\nSTDERR:\n%s\n", 239 | im.TempMainPath, 240 | string(out.Bytes()), 241 | string(errOut.Bytes()))) 242 | } 243 | 244 | defer func() { 245 | if im.tempExpose != nil { 246 | im.tempExpose.Close() 247 | } 248 | 249 | if im.tempMain != nil { 250 | im.tempMain.Close() 251 | } 252 | 253 | os.Remove(im.TempMainPath) 254 | os.Remove(im.exposePath) 255 | os.Remove(im.tempDir) 256 | }() 257 | 258 | return nil 259 | } 260 | -------------------------------------------------------------------------------- /generator/parser.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package generator 19 | 20 | import ( 21 | "flag" 22 | "fmt" 23 | "github.com/pquerna/ffjson/shared" 24 | "go/ast" 25 | "go/doc" 26 | "go/parser" 27 | "go/token" 28 | "regexp" 29 | "strings" 30 | ) 31 | 32 | var noEncoder = flag.Bool("noencoder", false, "Do not generate encoder functions") 33 | var noDecoder = flag.Bool("nodecoder", false, "Do not generate decoder functions") 34 | 35 | type StructField struct { 36 | Name string 37 | } 38 | 39 | type StructInfo struct { 40 | Name string 41 | Options shared.StructOptions 42 | } 43 | 44 | func NewStructInfo(name string) *StructInfo { 45 | return &StructInfo{ 46 | Name: name, 47 | Options: shared.StructOptions{ 48 | SkipDecoder: *noDecoder, 49 | SkipEncoder: *noEncoder, 50 | }, 51 | } 52 | } 53 | 54 | var skipre = regexp.MustCompile("(.*)ffjson:(\\s*)((skip)|(ignore))(.*)") 55 | var skipdec = regexp.MustCompile("(.*)ffjson:(\\s*)((skipdecoder)|(nodecoder))(.*)") 56 | var skipenc = regexp.MustCompile("(.*)ffjson:(\\s*)((skipencoder)|(noencoder))(.*)") 57 | 58 | func shouldInclude(d *ast.Object) (bool, error) { 59 | ts, ok := d.Decl.(*ast.TypeSpec) 60 | if !ok { 61 | return false, fmt.Errorf("Unknown type without TypeSec: %v", d) 62 | } 63 | 64 | _, ok = ts.Type.(*ast.StructType) 65 | if !ok { 66 | ident, ok := ts.Type.(*ast.Ident) 67 | if !ok || ident.Name == "" { 68 | return false, nil 69 | } 70 | 71 | // It must be in this package, and not a pointer alias 72 | if strings.Contains(ident.Name, ".") || strings.Contains(ident.Name, "*") { 73 | return false, nil 74 | } 75 | 76 | // if Obj is nil, we have an external type or built-in. 77 | if ident.Obj == nil || ident.Obj.Decl == nil { 78 | return false, nil 79 | } 80 | return shouldInclude(ident.Obj) 81 | } 82 | return true, nil 83 | } 84 | 85 | func ExtractStructs(inputPath string) (string, []*StructInfo, error) { 86 | fset := token.NewFileSet() 87 | 88 | f, err := parser.ParseFile(fset, inputPath, nil, parser.ParseComments) 89 | 90 | if err != nil { 91 | return "", nil, err 92 | } 93 | 94 | packageName := f.Name.String() 95 | structs := make(map[string]*StructInfo) 96 | 97 | for k, d := range f.Scope.Objects { 98 | if d.Kind == ast.Typ { 99 | incl, err := shouldInclude(d) 100 | if err != nil { 101 | return "", nil, err 102 | } 103 | if incl { 104 | stobj := NewStructInfo(k) 105 | 106 | structs[k] = stobj 107 | } 108 | } 109 | } 110 | 111 | files := map[string]*ast.File{ 112 | inputPath: f, 113 | } 114 | 115 | pkg, _ := ast.NewPackage(fset, files, nil, nil) 116 | 117 | d := doc.New(pkg, f.Name.String(), doc.AllDecls) 118 | for _, t := range d.Types { 119 | if skipre.MatchString(t.Doc) { 120 | delete(structs, t.Name) 121 | } else { 122 | if skipdec.MatchString(t.Doc) { 123 | s, ok := structs[t.Name] 124 | if ok { 125 | s.Options.SkipDecoder = true 126 | } 127 | } 128 | if skipenc.MatchString(t.Doc) { 129 | s, ok := structs[t.Name] 130 | if ok { 131 | s.Options.SkipEncoder = true 132 | } 133 | } 134 | } 135 | } 136 | 137 | rv := make([]*StructInfo, 0) 138 | for _, v := range structs { 139 | rv = append(rv, v) 140 | } 141 | return packageName, rv, nil 142 | } 143 | -------------------------------------------------------------------------------- /generator/tags.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package generator 19 | 20 | import ( 21 | "strings" 22 | ) 23 | 24 | // from: http://golang.org/src/pkg/encoding/json/tags.go 25 | 26 | // tagOptions is the string following a comma in a struct field's "json" 27 | // tag, or the empty string. It does not include the leading comma. 28 | type tagOptions string 29 | 30 | // parseTag splits a struct field's json tag into its name and 31 | // comma-separated options. 32 | func parseTag(tag string) (string, tagOptions) { 33 | if idx := strings.Index(tag, ","); idx != -1 { 34 | return tag[:idx], tagOptions(tag[idx+1:]) 35 | } 36 | return tag, tagOptions("") 37 | } 38 | 39 | // Contains reports whether a comma-separated list of options 40 | // contains a particular substr flag. substr must be surrounded by a 41 | // string boundary or commas. 42 | func (o tagOptions) Contains(optionName string) bool { 43 | if len(o) == 0 { 44 | return false 45 | } 46 | s := string(o) 47 | for s != "" { 48 | var next string 49 | i := strings.Index(s, ",") 50 | if i >= 0 { 51 | s, next = s[:i], s[i+1:] 52 | } 53 | if s == optionName { 54 | return true 55 | } 56 | s = next 57 | } 58 | return false 59 | } 60 | -------------------------------------------------------------------------------- /generator/tempfile.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package generator 6 | 7 | import ( 8 | "os" 9 | "path/filepath" 10 | "strconv" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | // Random number state. 16 | // We generate random temporary file names so that there's a good 17 | // chance the file doesn't exist yet - keeps the number of tries in 18 | // TempFile to a minimum. 19 | var rand uint32 20 | var randmu sync.Mutex 21 | 22 | func reseed() uint32 { 23 | return uint32(time.Now().UnixNano() + int64(os.Getpid())) 24 | } 25 | 26 | func nextSuffix() string { 27 | randmu.Lock() 28 | r := rand 29 | if r == 0 { 30 | r = reseed() 31 | } 32 | r = r*1664525 + 1013904223 // constants from Numerical Recipes 33 | rand = r 34 | randmu.Unlock() 35 | return strconv.Itoa(int(1e9 + r%1e9))[1:] 36 | } 37 | 38 | // TempFile creates a new temporary file in the directory dir 39 | // with a name beginning with prefix, opens the file for reading 40 | // and writing, and returns the resulting *os.File. 41 | // If dir is the empty string, TempFile uses the default directory 42 | // for temporary files (see os.TempDir). 43 | // Multiple programs calling TempFile simultaneously 44 | // will not choose the same file. The caller can use f.Name() 45 | // to find the pathname of the file. It is the caller's responsibility 46 | // to remove the file when no longer needed. 47 | func TempFileWithPostfix(dir, prefix string, postfix string) (f *os.File, err error) { 48 | if dir == "" { 49 | dir = os.TempDir() 50 | } 51 | 52 | nconflict := 0 53 | for i := 0; i < 10000; i++ { 54 | name := filepath.Join(dir, prefix+nextSuffix()+postfix) 55 | f, err = os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) 56 | if os.IsExist(err) { 57 | if nconflict++; nconflict > 10 { 58 | rand = reseed() 59 | } 60 | continue 61 | } 62 | break 63 | } 64 | return 65 | } 66 | -------------------------------------------------------------------------------- /inception/decoder.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package ffjsoninception 19 | 20 | import ( 21 | "fmt" 22 | "reflect" 23 | "strings" 24 | 25 | "github.com/pquerna/ffjson/shared" 26 | ) 27 | 28 | var validValues []string = []string{ 29 | "FFTok_left_brace", 30 | "FFTok_left_bracket", 31 | "FFTok_integer", 32 | "FFTok_double", 33 | "FFTok_string", 34 | "FFTok_bool", 35 | "FFTok_null", 36 | } 37 | 38 | func CreateUnmarshalJSON(ic *Inception, si *StructInfo) error { 39 | out := "" 40 | ic.OutputImports[`fflib "github.com/pquerna/ffjson/fflib/v1"`] = true 41 | if len(si.Fields) > 0 { 42 | ic.OutputImports[`"bytes"`] = true 43 | } 44 | ic.OutputImports[`"fmt"`] = true 45 | 46 | out += tplStr(decodeTpl["header"], header{ 47 | IC: ic, 48 | SI: si, 49 | }) 50 | 51 | out += tplStr(decodeTpl["ujFunc"], ujFunc{ 52 | SI: si, 53 | IC: ic, 54 | ValidValues: validValues, 55 | ResetFields: ic.ResetFields, 56 | }) 57 | 58 | ic.OutputFuncs = append(ic.OutputFuncs, out) 59 | 60 | return nil 61 | } 62 | 63 | func handleField(ic *Inception, name string, typ reflect.Type, ptr bool, quoted bool) string { 64 | return handleFieldAddr(ic, name, false, typ, ptr, quoted) 65 | } 66 | 67 | func handleFieldAddr(ic *Inception, name string, takeAddr bool, typ reflect.Type, ptr bool, quoted bool) string { 68 | out := fmt.Sprintf("/* handler: %s type=%v kind=%v quoted=%t*/\n", name, typ, typ.Kind(), quoted) 69 | 70 | umlx := typ.Implements(unmarshalFasterType) || typeInInception(ic, typ, shared.MustDecoder) 71 | umlx = umlx || reflect.PtrTo(typ).Implements(unmarshalFasterType) 72 | 73 | umlstd := typ.Implements(unmarshalerType) || reflect.PtrTo(typ).Implements(unmarshalerType) 74 | 75 | out += tplStr(decodeTpl["handleUnmarshaler"], handleUnmarshaler{ 76 | IC: ic, 77 | Name: name, 78 | Typ: typ, 79 | Ptr: reflect.Ptr, 80 | TakeAddr: takeAddr || ptr, 81 | UnmarshalJSONFFLexer: umlx, 82 | Unmarshaler: umlstd, 83 | }) 84 | 85 | if umlx || umlstd { 86 | return out 87 | } 88 | 89 | // TODO(pquerna): generic handling of token type mismatching struct type 90 | switch typ.Kind() { 91 | case reflect.Int, 92 | reflect.Int8, 93 | reflect.Int16, 94 | reflect.Int32, 95 | reflect.Int64: 96 | 97 | allowed := buildTokens(quoted, "FFTok_string", "FFTok_integer", "FFTok_null") 98 | out += getAllowTokens(typ.Name(), allowed...) 99 | 100 | out += getNumberHandler(ic, name, takeAddr || ptr, typ, "ParseInt") 101 | 102 | case reflect.Uint, 103 | reflect.Uint8, 104 | reflect.Uint16, 105 | reflect.Uint32, 106 | reflect.Uint64: 107 | 108 | allowed := buildTokens(quoted, "FFTok_string", "FFTok_integer", "FFTok_null") 109 | out += getAllowTokens(typ.Name(), allowed...) 110 | 111 | out += getNumberHandler(ic, name, takeAddr || ptr, typ, "ParseUint") 112 | 113 | case reflect.Float32, 114 | reflect.Float64: 115 | 116 | allowed := buildTokens(quoted, "FFTok_string", "FFTok_double", "FFTok_integer", "FFTok_null") 117 | out += getAllowTokens(typ.Name(), allowed...) 118 | 119 | out += getNumberHandler(ic, name, takeAddr || ptr, typ, "ParseFloat") 120 | 121 | case reflect.Bool: 122 | ic.OutputImports[`"bytes"`] = true 123 | ic.OutputImports[`"errors"`] = true 124 | 125 | allowed := buildTokens(quoted, "FFTok_string", "FFTok_bool", "FFTok_null") 126 | out += getAllowTokens(typ.Name(), allowed...) 127 | 128 | out += tplStr(decodeTpl["handleBool"], handleBool{ 129 | Name: name, 130 | Typ: typ, 131 | TakeAddr: takeAddr || ptr, 132 | }) 133 | 134 | case reflect.Ptr: 135 | out += tplStr(decodeTpl["handlePtr"], handlePtr{ 136 | IC: ic, 137 | Name: name, 138 | Typ: typ, 139 | Quoted: quoted, 140 | }) 141 | 142 | case reflect.Array, 143 | reflect.Slice: 144 | out += getArrayHandler(ic, name, typ, ptr) 145 | 146 | case reflect.String: 147 | // Is it a json.Number? 148 | if typ.PkgPath() == "encoding/json" && typ.Name() == "Number" { 149 | // Fall back to json package to rely on the valid number check. 150 | // See: https://github.com/golang/go/blob/f05c3aa24d815cd3869153750c9875e35fc48a6e/src/encoding/json/decode.go#L897 151 | ic.OutputImports[`"encoding/json"`] = true 152 | out += tplStr(decodeTpl["handleFallback"], handleFallback{ 153 | Name: name, 154 | Typ: typ, 155 | Kind: typ.Kind(), 156 | }) 157 | } else { 158 | out += tplStr(decodeTpl["handleString"], handleString{ 159 | IC: ic, 160 | Name: name, 161 | Typ: typ, 162 | TakeAddr: takeAddr || ptr, 163 | Quoted: quoted, 164 | }) 165 | } 166 | case reflect.Interface: 167 | ic.OutputImports[`"encoding/json"`] = true 168 | out += tplStr(decodeTpl["handleFallback"], handleFallback{ 169 | Name: name, 170 | Typ: typ, 171 | Kind: typ.Kind(), 172 | }) 173 | case reflect.Map: 174 | out += tplStr(decodeTpl["handleObject"], handleObject{ 175 | IC: ic, 176 | Name: name, 177 | Typ: typ, 178 | Ptr: reflect.Ptr, 179 | TakeAddr: takeAddr || ptr, 180 | }) 181 | default: 182 | ic.OutputImports[`"encoding/json"`] = true 183 | out += tplStr(decodeTpl["handleFallback"], handleFallback{ 184 | Name: name, 185 | Typ: typ, 186 | Kind: typ.Kind(), 187 | }) 188 | } 189 | 190 | return out 191 | } 192 | 193 | func getArrayHandler(ic *Inception, name string, typ reflect.Type, ptr bool) string { 194 | if typ.Kind() == reflect.Slice && typ.Elem().Kind() == reflect.Uint8 { 195 | ic.OutputImports[`"encoding/base64"`] = true 196 | useReflectToSet := false 197 | if typ.Elem().Name() != "byte" { 198 | ic.OutputImports[`"reflect"`] = true 199 | useReflectToSet = true 200 | } 201 | 202 | return tplStr(decodeTpl["handleByteSlice"], handleArray{ 203 | IC: ic, 204 | Name: name, 205 | Typ: typ, 206 | Ptr: reflect.Ptr, 207 | UseReflectToSet: useReflectToSet, 208 | }) 209 | } 210 | 211 | if typ.Elem().Kind() == reflect.Struct && typ.Elem().Name() != "" { 212 | goto sliceOrArray 213 | } 214 | 215 | if (typ.Elem().Kind() == reflect.Struct || typ.Elem().Kind() == reflect.Map) || 216 | typ.Elem().Kind() == reflect.Array || typ.Elem().Kind() == reflect.Slice && 217 | typ.Elem().Name() == "" { 218 | ic.OutputImports[`"encoding/json"`] = true 219 | 220 | return tplStr(decodeTpl["handleFallback"], handleFallback{ 221 | Name: name, 222 | Typ: typ, 223 | Kind: typ.Kind(), 224 | }) 225 | } 226 | 227 | sliceOrArray: 228 | 229 | if typ.Kind() == reflect.Array { 230 | return tplStr(decodeTpl["handleArray"], handleArray{ 231 | IC: ic, 232 | Name: name, 233 | Typ: typ, 234 | IsPtr: ptr, 235 | Ptr: reflect.Ptr, 236 | }) 237 | } 238 | 239 | return tplStr(decodeTpl["handleSlice"], handleArray{ 240 | IC: ic, 241 | Name: name, 242 | Typ: typ, 243 | IsPtr: ptr, 244 | Ptr: reflect.Ptr, 245 | }) 246 | } 247 | 248 | func getAllowTokens(name string, tokens ...string) string { 249 | return tplStr(decodeTpl["allowTokens"], allowTokens{ 250 | Name: name, 251 | Tokens: tokens, 252 | }) 253 | } 254 | 255 | func getNumberHandler(ic *Inception, name string, takeAddr bool, typ reflect.Type, parsefunc string) string { 256 | return tplStr(decodeTpl["handlerNumeric"], handlerNumeric{ 257 | IC: ic, 258 | Name: name, 259 | ParseFunc: parsefunc, 260 | TakeAddr: takeAddr, 261 | Typ: typ, 262 | }) 263 | } 264 | 265 | func getNumberSize(typ reflect.Type) string { 266 | return fmt.Sprintf("%d", typ.Bits()) 267 | } 268 | 269 | func getType(ic *Inception, name string, typ reflect.Type) string { 270 | s := typ.Name() 271 | 272 | if typ.PkgPath() != "" && typ.PkgPath() != ic.PackagePath { 273 | path := removeVendor(typ.PkgPath()) 274 | ic.OutputImports[`"`+path+`"`] = true 275 | s = typ.String() 276 | } 277 | 278 | if s == "" { 279 | return typ.String() 280 | } 281 | 282 | return s 283 | } 284 | 285 | // removeVendor removes everything before and including a '/vendor/' 286 | // substring in the package path. 287 | // This is needed becuase that full path can't be used in the 288 | // import statement. 289 | func removeVendor(path string) string { 290 | i := strings.Index(path, "/vendor/") 291 | if i == -1 { 292 | return path 293 | } 294 | return path[i+8:] 295 | } 296 | 297 | func buildTokens(containsOptional bool, optional string, required ...string) []string { 298 | if containsOptional { 299 | return append(required, optional) 300 | } 301 | 302 | return required 303 | } 304 | 305 | func unquoteField(quoted bool) string { 306 | // The outer quote of a string is already stripped out by 307 | // the lexer. We need to check if the inner string is also 308 | // quoted. If so, we will decode it as json string. If decoding 309 | // fails, we will use the original string 310 | if quoted { 311 | return ` 312 | unquoted, ok := fflib.UnquoteBytes(outBuf) 313 | if ok { 314 | outBuf = unquoted 315 | } 316 | ` 317 | } 318 | return "" 319 | } 320 | 321 | func getTmpVarFor(name string) string { 322 | return "tmp" + strings.Replace(strings.Title(name), ".", "", -1) 323 | } 324 | -------------------------------------------------------------------------------- /inception/encoder_tpl.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package ffjsoninception 19 | 20 | import ( 21 | "reflect" 22 | "text/template" 23 | ) 24 | 25 | var encodeTpl map[string]*template.Template 26 | 27 | func init() { 28 | encodeTpl = make(map[string]*template.Template) 29 | 30 | funcs := map[string]string{ 31 | "handleMarshaler": handleMarshalerTxt, 32 | } 33 | tplFuncs := template.FuncMap{} 34 | 35 | for k, v := range funcs { 36 | encodeTpl[k] = template.Must(template.New(k).Funcs(tplFuncs).Parse(v)) 37 | } 38 | } 39 | 40 | type handleMarshaler struct { 41 | IC *Inception 42 | Name string 43 | Typ reflect.Type 44 | Ptr reflect.Kind 45 | MarshalJSONBuf bool 46 | Marshaler bool 47 | } 48 | 49 | var handleMarshalerTxt = ` 50 | { 51 | {{if eq .Typ.Kind .Ptr}} 52 | if {{.Name}} == nil { 53 | buf.WriteString("null") 54 | } else { 55 | {{end}} 56 | 57 | {{if eq .MarshalJSONBuf true}} 58 | err = {{.Name}}.MarshalJSONBuf(buf) 59 | if err != nil { 60 | return err 61 | } 62 | {{else if eq .Marshaler true}} 63 | obj, err = {{.Name}}.MarshalJSON() 64 | if err != nil { 65 | return err 66 | } 67 | buf.Write(obj) 68 | {{end}} 69 | {{if eq .Typ.Kind .Ptr}} 70 | } 71 | {{end}} 72 | } 73 | ` 74 | -------------------------------------------------------------------------------- /inception/inception.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package ffjsoninception 19 | 20 | import ( 21 | "errors" 22 | "fmt" 23 | "github.com/pquerna/ffjson/shared" 24 | "io/ioutil" 25 | "os" 26 | "reflect" 27 | "sort" 28 | ) 29 | 30 | type Inception struct { 31 | objs []*StructInfo 32 | InputPath string 33 | OutputPath string 34 | PackageName string 35 | PackagePath string 36 | OutputImports map[string]bool 37 | OutputFuncs []string 38 | q ConditionalWrite 39 | ResetFields bool 40 | } 41 | 42 | func NewInception(inputPath string, packageName string, outputPath string, resetFields bool) *Inception { 43 | return &Inception{ 44 | objs: make([]*StructInfo, 0), 45 | InputPath: inputPath, 46 | OutputPath: outputPath, 47 | PackageName: packageName, 48 | OutputFuncs: make([]string, 0), 49 | OutputImports: make(map[string]bool), 50 | ResetFields: resetFields, 51 | } 52 | } 53 | 54 | func (i *Inception) AddMany(objs []shared.InceptionType) { 55 | for _, obj := range objs { 56 | i.Add(obj) 57 | } 58 | } 59 | 60 | func (i *Inception) Add(obj shared.InceptionType) { 61 | i.objs = append(i.objs, NewStructInfo(obj)) 62 | i.PackagePath = i.objs[0].Typ.PkgPath() 63 | } 64 | 65 | func (i *Inception) wantUnmarshal(si *StructInfo) bool { 66 | if si.Options.SkipDecoder { 67 | return false 68 | } 69 | typ := si.Typ 70 | umlx := typ.Implements(unmarshalFasterType) || reflect.PtrTo(typ).Implements(unmarshalFasterType) 71 | umlstd := typ.Implements(unmarshalerType) || reflect.PtrTo(typ).Implements(unmarshalerType) 72 | if umlstd && !umlx { 73 | // structure has UnmarshalJSON, but not our faster version -- skip it. 74 | return false 75 | } 76 | return true 77 | } 78 | 79 | func (i *Inception) wantMarshal(si *StructInfo) bool { 80 | if si.Options.SkipEncoder { 81 | return false 82 | } 83 | typ := si.Typ 84 | mlx := typ.Implements(marshalerFasterType) || reflect.PtrTo(typ).Implements(marshalerFasterType) 85 | mlstd := typ.Implements(marshalerType) || reflect.PtrTo(typ).Implements(marshalerType) 86 | if mlstd && !mlx { 87 | // structure has MarshalJSON, but not our faster version -- skip it. 88 | return false 89 | } 90 | return true 91 | } 92 | 93 | type sortedStructs []*StructInfo 94 | 95 | func (p sortedStructs) Len() int { return len(p) } 96 | func (p sortedStructs) Less(i, j int) bool { return p[i].Name < p[j].Name } 97 | func (p sortedStructs) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 98 | func (p sortedStructs) Sort() { sort.Sort(p) } 99 | 100 | func (i *Inception) generateCode() error { 101 | // We sort the structs by name, so output if predictable. 102 | sorted := sortedStructs(i.objs) 103 | sorted.Sort() 104 | 105 | for _, si := range sorted { 106 | if i.wantMarshal(si) { 107 | err := CreateMarshalJSON(i, si) 108 | if err != nil { 109 | return err 110 | } 111 | } 112 | 113 | if i.wantUnmarshal(si) { 114 | err := CreateUnmarshalJSON(i, si) 115 | if err != nil { 116 | return err 117 | } 118 | } 119 | } 120 | return nil 121 | } 122 | 123 | func (i *Inception) handleError(err error) { 124 | fmt.Fprintf(os.Stderr, "Error: %s:\n\n", err) 125 | os.Exit(1) 126 | } 127 | 128 | func (i *Inception) Execute() { 129 | if len(os.Args) != 1 { 130 | i.handleError(errors.New(fmt.Sprintf("Internal ffjson error: inception executable takes no args: %v", os.Args))) 131 | return 132 | } 133 | 134 | err := i.generateCode() 135 | if err != nil { 136 | i.handleError(err) 137 | return 138 | } 139 | 140 | data, err := RenderTemplate(i) 141 | if err != nil { 142 | i.handleError(err) 143 | return 144 | } 145 | 146 | stat, err := os.Stat(i.InputPath) 147 | 148 | if err != nil { 149 | i.handleError(err) 150 | return 151 | } 152 | 153 | err = ioutil.WriteFile(i.OutputPath, data, stat.Mode()) 154 | 155 | if err != nil { 156 | i.handleError(err) 157 | return 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /inception/reflect.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package ffjsoninception 19 | 20 | import ( 21 | fflib "github.com/pquerna/ffjson/fflib/v1" 22 | "github.com/pquerna/ffjson/shared" 23 | 24 | "bytes" 25 | "encoding/json" 26 | "reflect" 27 | "unicode/utf8" 28 | ) 29 | 30 | type StructField struct { 31 | Name string 32 | JsonName string 33 | FoldFuncName string 34 | Typ reflect.Type 35 | OmitEmpty bool 36 | ForceString bool 37 | HasMarshalJSON bool 38 | HasUnmarshalJSON bool 39 | Pointer bool 40 | Tagged bool 41 | } 42 | 43 | type FieldByJsonName []*StructField 44 | 45 | func (a FieldByJsonName) Len() int { return len(a) } 46 | func (a FieldByJsonName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 47 | func (a FieldByJsonName) Less(i, j int) bool { return a[i].JsonName < a[j].JsonName } 48 | 49 | type StructInfo struct { 50 | Name string 51 | Obj interface{} 52 | Typ reflect.Type 53 | Fields []*StructField 54 | Options shared.StructOptions 55 | } 56 | 57 | func NewStructInfo(obj shared.InceptionType) *StructInfo { 58 | t := reflect.TypeOf(obj.Obj) 59 | return &StructInfo{ 60 | Obj: obj.Obj, 61 | Name: t.Name(), 62 | Typ: t, 63 | Fields: extractFields(obj.Obj), 64 | Options: obj.Options, 65 | } 66 | } 67 | 68 | func (si *StructInfo) FieldsByFirstByte() map[string][]*StructField { 69 | rv := make(map[string][]*StructField) 70 | for _, f := range si.Fields { 71 | b := string(f.JsonName[1]) 72 | rv[b] = append(rv[b], f) 73 | } 74 | return rv 75 | } 76 | 77 | func (si *StructInfo) ReverseFields() []*StructField { 78 | var i int 79 | rv := make([]*StructField, 0) 80 | for i = len(si.Fields) - 1; i >= 0; i-- { 81 | rv = append(rv, si.Fields[i]) 82 | } 83 | return rv 84 | } 85 | 86 | const ( 87 | caseMask = ^byte(0x20) // Mask to ignore case in ASCII. 88 | ) 89 | 90 | func foldFunc(key []byte) string { 91 | nonLetter := false 92 | special := false // special letter 93 | for _, b := range key { 94 | if b >= utf8.RuneSelf { 95 | return "bytes.EqualFold" 96 | } 97 | upper := b & caseMask 98 | if upper < 'A' || upper > 'Z' { 99 | nonLetter = true 100 | } else if upper == 'K' || upper == 'S' { 101 | // See above for why these letters are special. 102 | special = true 103 | } 104 | } 105 | if special { 106 | return "fflib.EqualFoldRight" 107 | } 108 | if nonLetter { 109 | return "fflib.AsciiEqualFold" 110 | } 111 | return "fflib.SimpleLetterEqualFold" 112 | } 113 | 114 | type MarshalerFaster interface { 115 | MarshalJSONBuf(buf fflib.EncodingBuffer) error 116 | } 117 | 118 | type UnmarshalFaster interface { 119 | UnmarshalJSONFFLexer(l *fflib.FFLexer, state fflib.FFParseState) error 120 | } 121 | 122 | var marshalerType = reflect.TypeOf(new(json.Marshaler)).Elem() 123 | var marshalerFasterType = reflect.TypeOf(new(MarshalerFaster)).Elem() 124 | var unmarshalerType = reflect.TypeOf(new(json.Unmarshaler)).Elem() 125 | var unmarshalFasterType = reflect.TypeOf(new(UnmarshalFaster)).Elem() 126 | 127 | // extractFields returns a list of fields that JSON should recognize for the given type. 128 | // The algorithm is breadth-first search over the set of structs to include - the top struct 129 | // and then any reachable anonymous structs. 130 | func extractFields(obj interface{}) []*StructField { 131 | t := reflect.TypeOf(obj) 132 | // Anonymous fields to explore at the current level and the next. 133 | current := []StructField{} 134 | next := []StructField{{Typ: t}} 135 | 136 | // Count of queued names for current level and the next. 137 | count := map[reflect.Type]int{} 138 | nextCount := map[reflect.Type]int{} 139 | 140 | // Types already visited at an earlier level. 141 | visited := map[reflect.Type]bool{} 142 | 143 | // Fields found. 144 | var fields []*StructField 145 | 146 | for len(next) > 0 { 147 | current, next = next, current[:0] 148 | count, nextCount = nextCount, map[reflect.Type]int{} 149 | 150 | for _, f := range current { 151 | if visited[f.Typ] { 152 | continue 153 | } 154 | visited[f.Typ] = true 155 | 156 | // Scan f.typ for fields to include. 157 | for i := 0; i < f.Typ.NumField(); i++ { 158 | sf := f.Typ.Field(i) 159 | if sf.PkgPath != "" { // unexported 160 | continue 161 | } 162 | tag := sf.Tag.Get("json") 163 | if tag == "-" { 164 | continue 165 | } 166 | name, opts := parseTag(tag) 167 | if !isValidTag(name) { 168 | name = "" 169 | } 170 | 171 | ft := sf.Type 172 | ptr := false 173 | if ft.Kind() == reflect.Ptr { 174 | ptr = true 175 | } 176 | 177 | if ft.Name() == "" && ft.Kind() == reflect.Ptr { 178 | // Follow pointer. 179 | ft = ft.Elem() 180 | } 181 | 182 | // Record found field and index sequence. 183 | if name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct { 184 | tagged := name != "" 185 | if name == "" { 186 | name = sf.Name 187 | } 188 | 189 | var buf bytes.Buffer 190 | fflib.WriteJsonString(&buf, name) 191 | 192 | field := &StructField{ 193 | Name: sf.Name, 194 | JsonName: string(buf.Bytes()), 195 | FoldFuncName: foldFunc([]byte(name)), 196 | Typ: ft, 197 | HasMarshalJSON: ft.Implements(marshalerType), 198 | HasUnmarshalJSON: ft.Implements(unmarshalerType), 199 | OmitEmpty: opts.Contains("omitempty"), 200 | ForceString: opts.Contains("string"), 201 | Pointer: ptr, 202 | Tagged: tagged, 203 | } 204 | 205 | fields = append(fields, field) 206 | 207 | if count[f.Typ] > 1 { 208 | // If there were multiple instances, add a second, 209 | // so that the annihilation code will see a duplicate. 210 | // It only cares about the distinction between 1 or 2, 211 | // so don't bother generating any more copies. 212 | fields = append(fields, fields[len(fields)-1]) 213 | } 214 | continue 215 | } 216 | 217 | // Record new anonymous struct to explore in next round. 218 | nextCount[ft]++ 219 | if nextCount[ft] == 1 { 220 | next = append(next, StructField{ 221 | Name: ft.Name(), 222 | Typ: ft, 223 | }) 224 | } 225 | } 226 | } 227 | } 228 | 229 | // Delete all fields that are hidden by the Go rules for embedded fields, 230 | // except that fields with JSON tags are promoted. 231 | 232 | // The fields are sorted in primary order of name, secondary order 233 | // of field index length. Loop over names; for each name, delete 234 | // hidden fields by choosing the one dominant field that survives. 235 | out := fields[:0] 236 | for advance, i := 0, 0; i < len(fields); i += advance { 237 | // One iteration per name. 238 | // Find the sequence of fields with the name of this first field. 239 | fi := fields[i] 240 | name := fi.JsonName 241 | for advance = 1; i+advance < len(fields); advance++ { 242 | fj := fields[i+advance] 243 | if fj.JsonName != name { 244 | break 245 | } 246 | } 247 | if advance == 1 { // Only one field with this name 248 | out = append(out, fi) 249 | continue 250 | } 251 | dominant, ok := dominantField(fields[i : i+advance]) 252 | if ok { 253 | out = append(out, dominant) 254 | } 255 | } 256 | 257 | fields = out 258 | 259 | return fields 260 | } 261 | 262 | // dominantField looks through the fields, all of which are known to 263 | // have the same name, to find the single field that dominates the 264 | // others using Go's embedding rules, modified by the presence of 265 | // JSON tags. If there are multiple top-level fields, the boolean 266 | // will be false: This condition is an error in Go and we skip all 267 | // the fields. 268 | func dominantField(fields []*StructField) (*StructField, bool) { 269 | tagged := -1 // Index of first tagged field. 270 | for i, f := range fields { 271 | if f.Tagged { 272 | if tagged >= 0 { 273 | // Multiple tagged fields at the same level: conflict. 274 | // Return no field. 275 | return nil, false 276 | } 277 | tagged = i 278 | } 279 | } 280 | if tagged >= 0 { 281 | return fields[tagged], true 282 | } 283 | // All remaining fields have the same length. If there's more than one, 284 | // we have a conflict (two fields named "X" at the same level) and we 285 | // return no field. 286 | if len(fields) > 1 { 287 | return nil, false 288 | } 289 | return fields[0], true 290 | } 291 | -------------------------------------------------------------------------------- /inception/tags.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package ffjsoninception 19 | 20 | import ( 21 | "strings" 22 | "unicode" 23 | ) 24 | 25 | // from: http://golang.org/src/pkg/encoding/json/tags.go 26 | 27 | // tagOptions is the string following a comma in a struct field's "json" 28 | // tag, or the empty string. It does not include the leading comma. 29 | type tagOptions string 30 | 31 | // parseTag splits a struct field's json tag into its name and 32 | // comma-separated options. 33 | func parseTag(tag string) (string, tagOptions) { 34 | if idx := strings.Index(tag, ","); idx != -1 { 35 | return tag[:idx], tagOptions(tag[idx+1:]) 36 | } 37 | return tag, tagOptions("") 38 | } 39 | 40 | // Contains reports whether a comma-separated list of options 41 | // contains a particular substr flag. substr must be surrounded by a 42 | // string boundary or commas. 43 | func (o tagOptions) Contains(optionName string) bool { 44 | if len(o) == 0 { 45 | return false 46 | } 47 | s := string(o) 48 | for s != "" { 49 | var next string 50 | i := strings.Index(s, ",") 51 | if i >= 0 { 52 | s, next = s[:i], s[i+1:] 53 | } 54 | if s == optionName { 55 | return true 56 | } 57 | s = next 58 | } 59 | return false 60 | } 61 | 62 | func isValidTag(s string) bool { 63 | if s == "" { 64 | return false 65 | } 66 | for _, c := range s { 67 | switch { 68 | case strings.ContainsRune("!#$%&()*+-./:<=>?@[]^_{|}~ ", c): 69 | // Backslash and quote chars are reserved, but 70 | // otherwise any punctuation chars are allowed 71 | // in a tag name. 72 | default: 73 | if !unicode.IsLetter(c) && !unicode.IsDigit(c) { 74 | return false 75 | } 76 | } 77 | } 78 | return true 79 | } 80 | -------------------------------------------------------------------------------- /inception/template.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package ffjsoninception 19 | 20 | import ( 21 | "bytes" 22 | "go/format" 23 | "text/template" 24 | ) 25 | 26 | const ffjsonTemplate = ` 27 | // Code generated by ffjson . DO NOT EDIT. 28 | // source: {{.InputPath}} 29 | 30 | package {{.PackageName}} 31 | 32 | import ( 33 | {{range $k, $v := .OutputImports}}{{$k}} 34 | {{end}} 35 | ) 36 | 37 | {{range .OutputFuncs}} 38 | {{.}} 39 | {{end}} 40 | 41 | ` 42 | 43 | func RenderTemplate(ic *Inception) ([]byte, error) { 44 | t := template.Must(template.New("ffjson.go").Parse(ffjsonTemplate)) 45 | buf := new(bytes.Buffer) 46 | err := t.Execute(buf, ic) 47 | if err != nil { 48 | return nil, err 49 | } 50 | return format.Source(buf.Bytes()) 51 | } 52 | 53 | func tplStr(t *template.Template, data interface{}) string { 54 | buf := bytes.Buffer{} 55 | err := t.Execute(&buf, data) 56 | if err != nil { 57 | panic(err) 58 | } 59 | return buf.String() 60 | } 61 | -------------------------------------------------------------------------------- /inception/writerstack.go: -------------------------------------------------------------------------------- 1 | package ffjsoninception 2 | 3 | import "strings" 4 | 5 | // ConditionalWrite is a stack containing a number of pending writes 6 | type ConditionalWrite struct { 7 | Queued []string 8 | } 9 | 10 | // Write will add a string to be written 11 | func (w *ConditionalWrite) Write(s string) { 12 | w.Queued = append(w.Queued, s) 13 | } 14 | 15 | // DeleteLast will delete the last added write 16 | func (w *ConditionalWrite) DeleteLast() { 17 | if len(w.Queued) == 0 { 18 | return 19 | } 20 | w.Queued = w.Queued[:len(w.Queued)-1] 21 | } 22 | 23 | // Last will return the last added write 24 | func (w *ConditionalWrite) Last() string { 25 | if len(w.Queued) == 0 { 26 | return "" 27 | } 28 | return w.Queued[len(w.Queued)-1] 29 | } 30 | 31 | // Flush will return all queued writes, and return 32 | // "" (empty string) in nothing has been queued 33 | // "buf.WriteByte('" + byte + "')" + '\n' if one bute has been queued. 34 | // "buf.WriteString(`" + string + "`)" + "\n" if more than one byte has been queued. 35 | func (w *ConditionalWrite) Flush() string { 36 | combined := strings.Join(w.Queued, "") 37 | if len(combined) == 0 { 38 | return "" 39 | } 40 | 41 | w.Queued = nil 42 | if len(combined) == 1 { 43 | return "buf.WriteByte('" + combined + "')" + "\n" 44 | } 45 | return "buf.WriteString(`" + combined + "`)" + "\n" 46 | } 47 | 48 | func (w *ConditionalWrite) FlushTo(out string) string { 49 | out += w.Flush() 50 | return out 51 | } 52 | 53 | // WriteFlush will add a string and return the Flush result for the queue 54 | func (w *ConditionalWrite) WriteFlush(s string) string { 55 | w.Write(s) 56 | return w.Flush() 57 | } 58 | 59 | // GetQueued will return the current queued content without flushing. 60 | func (w *ConditionalWrite) GetQueued() string { 61 | t := w.Queued 62 | s := w.Flush() 63 | w.Queued = t 64 | return s 65 | } 66 | -------------------------------------------------------------------------------- /shared/options.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna, Klaus Post 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 | */ 17 | 18 | package shared 19 | 20 | type StructOptions struct { 21 | SkipDecoder bool 22 | SkipEncoder bool 23 | } 24 | 25 | type InceptionType struct { 26 | Obj interface{} 27 | Options StructOptions 28 | } 29 | type Feature int 30 | 31 | const ( 32 | Nothing Feature = 0 33 | MustDecoder = 1 << 1 34 | MustEncoder = 1 << 2 35 | MustEncDec = MustDecoder | MustEncoder 36 | ) 37 | 38 | func (i InceptionType) HasFeature(f Feature) bool { 39 | return i.HasFeature(f) 40 | } 41 | 42 | func (s StructOptions) HasFeature(f Feature) bool { 43 | hasNeeded := true 44 | if f&MustDecoder != 0 && s.SkipDecoder { 45 | hasNeeded = false 46 | } 47 | if f&MustEncoder != 0 && s.SkipEncoder { 48 | hasNeeded = false 49 | } 50 | return hasNeeded 51 | } 52 | -------------------------------------------------------------------------------- /tests/base.go: -------------------------------------------------------------------------------- 1 | package tff 2 | 3 | // Foo struct 4 | type Foo struct { 5 | Blah int 6 | } 7 | 8 | // Record struct 9 | type Record struct { 10 | Timestamp int64 `json:"id,omitempty"` 11 | OriginID uint32 12 | Bar Foo 13 | Method string `json:"meth"` 14 | ReqID string 15 | ServerIP string 16 | RemoteIP string 17 | BytesSent uint64 18 | } 19 | -------------------------------------------------------------------------------- /tests/bench.cmd: -------------------------------------------------------------------------------- 1 | del ff_ffjson.go 2 | ffjson ff.go 3 | 4 | go test -benchmem -bench MarshalJSON 5 | 6 | REM ### Bench CPU ### 7 | rem go test -benchmem -test.run=none -bench MarshalJSONNative -cpuprofile="cpu.dat" -benchtime 10s &&go tool pprof -gif tests.test.exe cpu.dat >out.gif 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/encode_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tff 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | "testing" 11 | ) 12 | 13 | func TestOmitEmpty(t *testing.T) { 14 | var o Optionals 15 | o.Sw = "something" 16 | o.Mr = map[string]interface{}{} 17 | o.Mo = map[string]interface{}{} 18 | 19 | got, err := json.MarshalIndent(&o, "", " ") 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | if got := string(got); got != optionalsExpected { 24 | t.Errorf(" got: %s\nwant: %s\n", got, optionalsExpected) 25 | } 26 | } 27 | 28 | func TestOmitEmptyAll(t *testing.T) { 29 | var o OmitAll 30 | 31 | got, err := json.MarshalIndent(&o, "", " ") 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | if got := string(got); got != omitAllExpected { 36 | t.Errorf(" got: %s\nwant: %s\n", got, omitAllExpected) 37 | } 38 | } 39 | 40 | func TestNoExported(t *testing.T) { 41 | var o NoExported 42 | 43 | got, err := json.MarshalIndent(&o, "", " ") 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | if got := string(got); got != noExportedExpected { 48 | t.Errorf(" got: %s\nwant: %s\n", got, noExportedExpected) 49 | } 50 | } 51 | 52 | func TestOmitFirst(t *testing.T) { 53 | var o OmitFirst 54 | 55 | got, err := json.MarshalIndent(&o, "", " ") 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | if got := string(got); got != omitFirstExpected { 60 | t.Errorf(" got: %s\nwant: %s\n", got, omitFirstExpected) 61 | } 62 | } 63 | 64 | func TestOmitLast(t *testing.T) { 65 | var o OmitLast 66 | 67 | got, err := json.MarshalIndent(&o, "", " ") 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | if got := string(got); got != omitLastExpected { 72 | t.Errorf(" got: %s\nwant: %s\n", got, omitLastExpected) 73 | } 74 | } 75 | 76 | func TestStringTag(t *testing.T) { 77 | var s StringTag 78 | s.BoolStr = true 79 | s.IntStr = 42 80 | s.StrStr = "xzbit" 81 | got, err := json.MarshalIndent(&s, "", " ") 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | if got := string(got); got != stringTagExpected { 86 | t.Fatalf(" got: %s\nwant: %s\n", got, stringTagExpected) 87 | } 88 | } 89 | 90 | func TestUnsupportedValues(t *testing.T) { 91 | for _, v := range unsupportedValues { 92 | if _, err := json.Marshal(v); err != nil { 93 | if _, ok := err.(*json.UnsupportedValueError); !ok { 94 | t.Errorf("for %v, got %T want UnsupportedValueError", v, err) 95 | } 96 | } else { 97 | t.Errorf("for %v, expected error", v) 98 | } 99 | } 100 | } 101 | 102 | func TestRefValMarshal(t *testing.T) { 103 | var s = struct { 104 | R0 Ref 105 | R1 *Ref 106 | R2 RefText 107 | R3 *RefText 108 | V0 Val 109 | V1 *Val 110 | V2 ValText 111 | V3 *ValText 112 | }{ 113 | R0: 12, 114 | R1: new(Ref), 115 | R2: 14, 116 | R3: new(RefText), 117 | V0: 13, 118 | V1: new(Val), 119 | V2: 15, 120 | V3: new(ValText), 121 | } 122 | const want = `{"R0":"ref","R1":"ref","R2":"\"ref\"","R3":"\"ref\"","V0":"val","V1":"val","V2":"\"val\"","V3":"\"val\""}` 123 | b, err := json.Marshal(&s) 124 | if err != nil { 125 | t.Fatalf("Marshal: %v", err) 126 | } 127 | if got := string(b); got != want { 128 | t.Errorf("got %q, want %q", got, want) 129 | } 130 | } 131 | 132 | func TestMarshalerEscaping(t *testing.T) { 133 | var c C 134 | want := `"\u003c\u0026\u003e"` 135 | b, err := json.Marshal(c) 136 | if err != nil { 137 | t.Fatalf("Marshal(c): %v", err) 138 | } 139 | if got := string(b); got != want { 140 | t.Errorf("Marshal(c) = %#q, want %#q", got, want) 141 | } 142 | 143 | var ct CText 144 | want = `"\"\u003c\u0026\u003e\""` 145 | b, err = json.Marshal(ct) 146 | if err != nil { 147 | t.Fatalf("Marshal(ct): %v", err) 148 | } 149 | if got := string(b); got != want { 150 | t.Errorf("Marshal(ct) = %#q, want %#q", got, want) 151 | } 152 | } 153 | 154 | func TestAnonymousNonstruct(t *testing.T) { 155 | var i IntType = 11 156 | a := MyStruct{i} 157 | const want = `{"IntType":11}` 158 | 159 | b, err := json.Marshal(a) 160 | if err != nil { 161 | t.Fatalf("Marshal: %v", err) 162 | } 163 | if got := string(b); got != want { 164 | t.Errorf("got %q, want %q", got, want) 165 | } 166 | } 167 | 168 | // Issue 5245. 169 | func TestEmbeddedBug(t *testing.T) { 170 | v := BugB{ 171 | BugA{"A"}, 172 | "B", 173 | } 174 | b, err := json.Marshal(v) 175 | if err != nil { 176 | t.Fatal("Marshal:", err) 177 | } 178 | want := `{"S":"B"}` 179 | got := string(b) 180 | if got != want { 181 | t.Fatalf("Marshal: got %s want %s", got, want) 182 | } 183 | // Now check that the duplicate field, S, does not appear. 184 | x := BugX{ 185 | A: 23, 186 | } 187 | b, err = json.Marshal(x) 188 | if err != nil { 189 | t.Fatal("Marshal:", err) 190 | } 191 | want = `{"A":23}` 192 | got = string(b) 193 | if got != want { 194 | t.Fatalf("Marshal: got %s want %s", got, want) 195 | } 196 | } 197 | 198 | // Test that a field with a tag dominates untagged fields. 199 | func TestTaggedFieldDominates(t *testing.T) { 200 | v := BugY{ 201 | BugA{"BugA"}, 202 | BugD{"BugD"}, 203 | } 204 | b, err := json.Marshal(v) 205 | if err != nil { 206 | t.Fatal("Marshal:", err) 207 | } 208 | want := `{"S":"BugD"}` 209 | got := string(b) 210 | if got != want { 211 | t.Fatalf("Marshal: got %s want %s", got, want) 212 | } 213 | } 214 | 215 | func TestDuplicatedFieldDisappears(t *testing.T) { 216 | v := BugZ{ 217 | BugA{"BugA"}, 218 | BugC{"BugC"}, 219 | BugY{ 220 | BugA{"nested BugA"}, 221 | BugD{"nested BugD"}, 222 | }, 223 | } 224 | b, err := json.Marshal(v) 225 | if err != nil { 226 | t.Fatal("Marshal:", err) 227 | } 228 | want := `{}` 229 | got := string(b) 230 | if got != want { 231 | t.Fatalf("Marshal: got %s want %s", got, want) 232 | } 233 | } 234 | 235 | func TestIssue6458(t *testing.T) { 236 | type Foo struct { 237 | M json.RawMessage 238 | } 239 | x := Foo{json.RawMessage(`"foo"`)} 240 | 241 | b, err := json.Marshal(&x) 242 | if err != nil { 243 | t.Fatal(err) 244 | } 245 | if want := `{"M":"foo"}`; string(b) != want { 246 | t.Errorf("Marshal(&x) = %#q; want %#q", b, want) 247 | } 248 | 249 | b, err = json.Marshal(x) 250 | if err != nil { 251 | t.Fatal(err) 252 | } 253 | 254 | if want := `{"M":"foo"}`; string(b) != want { 255 | t.Errorf("Marshal(x) = %#q; want %#q", b, want) 256 | } 257 | } 258 | 259 | func TestHTMLEscape(t *testing.T) { 260 | var b, want bytes.Buffer 261 | m := `{"M":"foo &` + "\xe2\x80\xa8 \xe2\x80\xa9" + `"}` 262 | want.Write([]byte(`{"M":"\u003chtml\u003efoo \u0026\u2028 \u2029\u003c/html\u003e"}`)) 263 | json.HTMLEscape(&b, []byte(m)) 264 | if !bytes.Equal(b.Bytes(), want.Bytes()) { 265 | t.Errorf("HTMLEscape(&b, []byte(m)) = %s; want %s", b.Bytes(), want.Bytes()) 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /tests/ff_float_test.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package tff 19 | 20 | import ( 21 | "testing" 22 | ) 23 | 24 | // Test data from https://github.com/akheron/jansson/tree/master/test/suites/valid 25 | // jansson, Copyright (c) 2009-2014 Petri Lehtinen 26 | // (MIT Licensed) 27 | 28 | func TestFloatRealCapitalENegativeExponent(t *testing.T) { 29 | testExpectedXValBare(t, 30 | 0.01, 31 | `1E-2`, 32 | &Xfloat64{}) 33 | } 34 | 35 | func TestFloatRealCapitalEPositiveExponent(t *testing.T) { 36 | testExpectedXValBare(t, 37 | 100.0, 38 | `1E+2`, 39 | &Xfloat64{}) 40 | } 41 | 42 | func TestFloatRealCapital(t *testing.T) { 43 | testExpectedXValBare(t, 44 | 1e22, 45 | `1E22`, 46 | &Xfloat64{}) 47 | } 48 | 49 | func TestFloatRealExponent(t *testing.T) { 50 | testExpectedXValBare(t, 51 | 1.2299999999999999e47, 52 | `123e45`, 53 | &Xfloat64{}) 54 | } 55 | 56 | func TestFloatRealFractionExponent(t *testing.T) { 57 | testExpectedXValBare(t, 58 | 1.23456e80, 59 | `123.456e78`, 60 | &Xfloat64{}) 61 | } 62 | 63 | func TestFloatRealNegativeExponent(t *testing.T) { 64 | testExpectedXValBare(t, 65 | 0.01, 66 | `1e-2`, 67 | &Xfloat64{}) 68 | } 69 | 70 | func TestFloatRealPositiveExponent(t *testing.T) { 71 | testExpectedXValBare(t, 72 | 100.0, 73 | `1e2`, 74 | &Xfloat64{}) 75 | } 76 | 77 | func TestFloatRealSubnormalNumber(t *testing.T) { 78 | testExpectedXValBare(t, 79 | 1.8011670033376514e-308, 80 | `1.8011670033376514e-308`, 81 | &Xfloat64{}) 82 | } 83 | 84 | func TestFloatRealUnderflow(t *testing.T) { 85 | testExpectedXValBare(t, 86 | 0.0, 87 | `123e-10000000`, 88 | &Xfloat64{}) 89 | } 90 | 91 | func TestFloatNull(t *testing.T) { 92 | testExpectedXValBare(t, 93 | 0.0, 94 | `null`, 95 | &Xfloat64{}) 96 | } 97 | 98 | func TestFloatInt(t *testing.T) { 99 | testExpectedXValBare(t, 100 | 1.0, 101 | `1`, 102 | &Xfloat64{}) 103 | } 104 | -------------------------------------------------------------------------------- /tests/ff_invalid_test.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package tff 19 | 20 | import ( 21 | fflib "github.com/pquerna/ffjson/fflib/v1" 22 | 23 | _ "encoding/json" 24 | "testing" 25 | ) 26 | 27 | // Test data from https://github.com/akheron/jansson/tree/master/test/suites/invalid 28 | // jansson, Copyright (c) 2009-2014 Petri Lehtinen 29 | // (MIT Licensed) 30 | 31 | func TestInvalidApostrophe(t *testing.T) { 32 | testExpectedError(t, 33 | &fflib.LexerError{}, 34 | `'`, 35 | &Xstring{}) 36 | } 37 | 38 | func TestInvalidASCIIUnicodeIdentifier(t *testing.T) { 39 | testExpectedError(t, 40 | &fflib.LexerError{}, 41 | `aå`, 42 | &Xstring{}) 43 | } 44 | 45 | func TestInvalidBraceComma(t *testing.T) { 46 | testExpectedError(t, 47 | &fflib.LexerError{}, 48 | `{,}`, 49 | &Xstring{}) 50 | } 51 | 52 | func TestInvalidBracketComma(t *testing.T) { 53 | testExpectedError(t, 54 | &fflib.LexerError{}, 55 | `[,]`, 56 | &Xarray{}) 57 | } 58 | 59 | func TestInvalidBracketValueComma(t *testing.T) { 60 | testExpectedError(t, 61 | &fflib.LexerError{}, 62 | `[1,`, 63 | &Xarray{}) 64 | } 65 | 66 | func TestInvalidEmptyValue(t *testing.T) { 67 | testExpectedError(t, 68 | &fflib.LexerError{}, 69 | ``, 70 | &Xarray{}) 71 | } 72 | 73 | func TestInvalidGarbageAfterNewline(t *testing.T) { 74 | testExpectedError(t, 75 | &fflib.LexerError{}, 76 | "[1,2,3]\nfoo", 77 | &Xarray{}) 78 | } 79 | 80 | func TestInvalidGarbageAtEnd(t *testing.T) { 81 | testExpectedError(t, 82 | &fflib.LexerError{}, 83 | "[1,2,3]foo", 84 | &Xarray{}) 85 | } 86 | 87 | func TestInvalidIntStartingWithZero(t *testing.T) { 88 | testExpectedError(t, 89 | &fflib.LexerError{}, 90 | "012", 91 | &Xint64{}) 92 | } 93 | 94 | func TestInvalidEscape(t *testing.T) { 95 | testExpectedError(t, 96 | &fflib.LexerError{}, 97 | `"\a <-- invalid escape"`, 98 | &Xstring{}) 99 | } 100 | 101 | func TestInvalidIdentifier(t *testing.T) { 102 | testExpectedError(t, 103 | &fflib.LexerError{}, 104 | `troo`, 105 | &Xbool{}) 106 | } 107 | 108 | func TestInvalidNegativeInt(t *testing.T) { 109 | testExpectedError(t, 110 | &fflib.LexerError{}, 111 | `-123foo`, 112 | &Xint{}) 113 | } 114 | 115 | func TestInvalidNegativeFloat(t *testing.T) { 116 | testExpectedError(t, 117 | &fflib.LexerError{}, 118 | `-124.123foo`, 119 | &Xfloat64{}) 120 | } 121 | 122 | func TestInvalidSecondSurrogate(t *testing.T) { 123 | testExpectedError(t, 124 | &fflib.LexerError{}, 125 | `"\uD888\u3210 (first surrogate and invalid second surrogate)"`, 126 | &Xstring{}) 127 | } 128 | 129 | func TestInvalidLoneOpenBrace(t *testing.T) { 130 | testExpectedError(t, 131 | &fflib.LexerError{}, 132 | `{`, 133 | &Xstring{}) 134 | } 135 | 136 | func TestInvalidLoneOpenBracket(t *testing.T) { 137 | testExpectedError(t, 138 | &fflib.LexerError{}, 139 | `[`, 140 | &Xarray{}) 141 | } 142 | 143 | func TestInvalidLoneCloseBrace(t *testing.T) { 144 | testExpectedError(t, 145 | &fflib.LexerError{}, 146 | `}`, 147 | &Xstring{}) 148 | } 149 | 150 | func TestInvalidHighBytes(t *testing.T) { 151 | testExpectedError(t, 152 | &fflib.LexerError{}, 153 | string('\xFF'), 154 | &Xstring{}) 155 | } 156 | 157 | func TestInvalidLoneCloseBracket(t *testing.T) { 158 | testExpectedError(t, 159 | &fflib.LexerError{}, 160 | `]`, 161 | &Xarray{}) 162 | } 163 | 164 | func TestInvalidMinusSignWithoutNumber(t *testing.T) { 165 | testExpectedError(t, 166 | &fflib.LexerError{}, 167 | `-`, 168 | &Xint{}) 169 | } 170 | 171 | func TestInvalidNullByte(t *testing.T) { 172 | testExpectedError(t, 173 | &fflib.LexerError{}, 174 | "\u0000", 175 | &Xstring{}) 176 | } 177 | 178 | func TestInvalidNullByteInString(t *testing.T) { 179 | testExpectedError(t, 180 | &fflib.LexerError{}, 181 | "\"\u0000 <- null byte\"", 182 | &Xstring{}) 183 | } 184 | 185 | func TestInvalidFloatGarbageAfterE(t *testing.T) { 186 | testExpectedError(t, 187 | &fflib.LexerError{}, 188 | `1ea`, 189 | &Xfloat64{}) 190 | } 191 | -------------------------------------------------------------------------------- /tests/ff_obj_test.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package tff 19 | 20 | import ( 21 | fflib "github.com/pquerna/ffjson/fflib/v1" 22 | 23 | "testing" 24 | ) 25 | 26 | // Test data from https://github.com/akheron/jansson/tree/master/test/suites 27 | // jansson, Copyright (c) 2009-2014 Petri Lehtinen 28 | // (MIT Licensed) 29 | 30 | func TestInvalidBareKey(t *testing.T) { 31 | testExpectedError(t, 32 | &fflib.LexerError{}, 33 | `{X:"foo"}`, 34 | &Xobj{}) 35 | } 36 | 37 | func TestInvalidNoValue(t *testing.T) { 38 | testExpectedError(t, 39 | &fflib.LexerError{}, 40 | `{"X":}`, 41 | &Xobj{}) 42 | } 43 | 44 | func TestInvalidTrailingComma(t *testing.T) { 45 | testExpectedError(t, 46 | &fflib.LexerError{}, 47 | `{"X":"foo",}`, 48 | &Xobj{}) 49 | } 50 | 51 | func TestInvalidRougeComma(t *testing.T) { 52 | testExpectedError(t, 53 | &fflib.LexerError{}, 54 | `{,}`, 55 | &Xobj{}) 56 | } 57 | 58 | func TestInvalidRougeColon(t *testing.T) { 59 | testExpectedError(t, 60 | &fflib.LexerError{}, 61 | `{:}`, 62 | &Xobj{}) 63 | } 64 | 65 | func TestInvalidMissingColon(t *testing.T) { 66 | testExpectedError(t, 67 | &fflib.LexerError{}, 68 | `{"X""foo"}`, 69 | &Xobj{}) 70 | testExpectedError(t, 71 | &fflib.LexerError{}, 72 | `{"X" "foo"}`, 73 | &Xobj{}) 74 | testExpectedError(t, 75 | &fflib.LexerError{}, 76 | `{"X","foo"}`, 77 | &Xobj{}) 78 | } 79 | 80 | func TestInvalidUnmatchedBrace(t *testing.T) { 81 | testExpectedError(t, 82 | &fflib.LexerError{}, 83 | `[`, 84 | &Xobj{}) 85 | } 86 | 87 | func TestInvalidUnmatchedBracket(t *testing.T) { 88 | testExpectedError(t, 89 | &fflib.LexerError{}, 90 | `{`, 91 | &Xobj{}) 92 | } 93 | 94 | func TestInvalidExpectedObjGotArray(t *testing.T) { 95 | testExpectedError(t, 96 | &fflib.LexerError{}, 97 | `[]`, 98 | &Xobj{}) 99 | } 100 | 101 | func TestInvalidUnterminatedValue(t *testing.T) { 102 | testExpectedError(t, 103 | &fflib.LexerError{}, 104 | `{"X": "foo`, 105 | &Xobj{}) 106 | } 107 | -------------------------------------------------------------------------------- /tests/ff_string_test.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package tff 19 | 20 | import ( 21 | "testing" 22 | "strings" 23 | "runtime" 24 | ) 25 | 26 | // Test data from https://github.com/akheron/jansson/tree/master/test/suites/valid 27 | // jansson, Copyright (c) 2009-2014 Petri Lehtinen 28 | // (MIT Licensed) 29 | 30 | func TestString(t *testing.T) { 31 | testType(t, &Tstring{}, &Xstring{}) 32 | testType(t, &Tmystring{}, &Xmystring{}) 33 | testType(t, &TmystringPtr{}, &XmystringPtr{}) 34 | } 35 | 36 | func TestMapStringString(t *testing.T) { 37 | m := map[string]string{"陫ʋsş\")珷<ºɖgȏ哙ȍ": "2ħ籦ö嗏ʑ>季"} 38 | testCycle(t, &TMapStringString{X: m}, &XMapStringString{X: m}) 39 | } 40 | 41 | func TestMapStringStringLong(t *testing.T) { 42 | m := map[string]string{"ɥ³ƞsɁ8^ʥǔTĪȸŹă": "ɩÅ議Ǹ轺@)蓳嗘TʡȂ", "丯Ƙ枛牐ɺ皚|": "\\p[", "ȉ": "ģ毋Ó6dz娝嘚", "ʒUɦOŖ": "斎AO6ĴC浔Ű壝ž", "/C龷ȪÆl殛瓷雼浢Ü礽绅": "D¡", "Lɋ聻鎥ʟ<$洅ɹ7\\弌Þ帺萸Do©": "A", "yǠ/淹\\韲翁&ʢsɜ": "`诫z徃鷢6ȥ啕禗Ǐ2啗塧ȱ蓿彭聡A", "瓧嫭塓烀罁胾^拜": "ǒɿʒ刽ʼn掏1ſ盷褎weLJ", "姥呄鐊唊飙Ş-U圴÷a/ɔ}摁(": "瓘ǓvjĜ蛶78Ȋ²@H", "IJ斬³;": "鯿r", "勽Ƙq/Ź u衲": "ŭDz鯰硰{舁", "枊a8衍`Ĩɘ.蘯6ċV夸eɑeʤ脽ě": "6/ʕVŚ(ĿȊ甞谐颋DžSǡƏS$+", "1ØœȠƬQg鄠": "军g>郵[+扴ȨŮ+朷Ǝ膯lj", "礶惇¸t颟.鵫ǚ灄鸫rʤî萨z": "", "ȶ网棊ʢ=wǕɳɷ9Ì": "'WKw(ğ儴Ůĺ}潷ʒ胵輓Ɔ", "}ȧ外ĺ稥氹Ç|¶鎚¡ Ɠ(嘒ėf倐": "窮秳ķ蟒苾h^", "?瞲Ť倱<įXŋ朘瑥A徙": "nh0åȂ町恰nj揠8lj黳鈫ʕ禒", "丩ŽoǠŻʘY賃ɪ鐊": "ľǎɳ,ǿ飏騀呣ǎ", "ȇe媹Hǝ呮}臷Ľð»ųKĵ": "踪鄌eÞȦY籎顒ǥŴ唼Ģ猇õǶț", "偐ę腬瓷碑=ɉ鎷卩蝾H韹寬娬ï瓼猀2": "ǰ溟ɴ扵閝ȝ鐵儣廡ɑ龫`劳", "ʮ馜ü": "", "șƶ4ĩĉş蝿ɖȃ賲鐅臬dH巧": "_瀹鞎sn芞QÄȻȊ+?", "E@Ȗs«ö": "蚛隖<ǶĬ4y£軶ǃ*ʙ嫙&蒒5靇C'", "忄*齧獚敆Ȏ": "螩B", "圠=l畣潁谯耨V6&]鴍Ɋ恧ȭ%ƎÜ": "涽託仭w-檮", "ʌ鴜": "琔n宂¬轚9Ȏ瀮昃2Ō¾\\", "ƅTG": "ǺƶȤ^}穠C]躢|)黰eȪ嵛4$%Q", "ǹ_Áȉ彂Ŵ廷s": "", "t莭琽§ć\\ ïì": "", "擓ƖHVe熼'FD剂讼ɓȌʟni酛": "/ɸɎ R§耶FfBls3!", "狞夌碕ʂɭ": "Ƽ@hDrȮO励鹗塢", "ʁgɸ=ǤÆ": "?讦ĭÐ", "陫ʋsş\")珷<ºɖgȏ哙ȍ": "2ħ籦ö嗏ʑ>季", "": "昕Ĭ", "Ⱦdz@ùƸʋŀ": "ǐƲE'iþŹʣy豎@ɀ羭,铻OŤǢʭ", ">犵殇ŕ-Ɂ圯W:ĸ輦唊#v铿ʩȂ4": "屡ʁ", "1Rƥ贫d飼$俊跾|@?鷅bȻN": "H炮掊°nʮ閼咎櫸eʔŊƞ究:ho", "ƻ悖ȩ0Ƹ[": "Ndǂ>5姣>懔%熷谟þ蛯ɰ", "ŵw^Ü郀叚Fi皬择": ":5塋訩塶\"=y钡n)İ笓", "'容": "誒j剐", "猤痈C*ĕ": "鴈o_鹈ɹ坼É/pȿŘ阌"} 43 | testCycle(t, &TMapStringString{X: m}, &XMapStringString{X: m}) 44 | } 45 | 46 | func TestStringEscapedControlCharacter(t *testing.T) { 47 | testExpectedXVal(t, 48 | "\x12 escaped control character", 49 | `\u0012 escaped control character`, 50 | &Xstring{}) 51 | } 52 | 53 | func TestStringOneByteUTF8(t *testing.T) { 54 | testExpectedXVal(t, 55 | ", one-byte UTF-8", 56 | `\u002c one-byte UTF-8`, 57 | &Xstring{}) 58 | } 59 | 60 | func TestStringUtf8Escape(t *testing.T) { 61 | testExpectedXVal(t, 62 | "2ħ籦ö嗏ʑ>嫀", 63 | `2ħ籦ö嗏ʑ\u003e嫀`, 64 | &Xstring{}) 65 | } 66 | 67 | func TestStringTwoByteUTF8(t *testing.T) { 68 | testExpectedXVal(t, 69 | "ģ two-byte UTF-8", 70 | `\u0123 two-byte UTF-8`, 71 | &Xstring{}) 72 | } 73 | 74 | func TestStringThreeByteUTF8(t *testing.T) { 75 | testExpectedXVal(t, 76 | "ࠡ three-byte UTF-8", 77 | `\u0821 three-byte UTF-8`, 78 | &Xstring{}) 79 | } 80 | 81 | func TestStringEsccapes(t *testing.T) { 82 | testExpectedXVal(t, 83 | `"\`+"\b\f\n\r\t", 84 | `\"\\\b\f\n\r\t`, 85 | &Xstring{}) 86 | 87 | testExpectedXVal(t, 88 | `/`, 89 | `\/`, 90 | &Xstring{}) 91 | } 92 | 93 | func TestStringSomeUTF8(t *testing.T) { 94 | testExpectedXVal(t, 95 | `€þıœəßð some utf-8 ĸʒ×ŋµåäö𝄞`, 96 | `€þıœəßð some utf-8 ĸʒ×ŋµåäö𝄞`, 97 | &Xstring{}) 98 | } 99 | 100 | func TestBytesInString(t *testing.T) { 101 | testExpectedXVal(t, 102 | string('\xff')+` <- xFF byte`, 103 | string('\xff')+` <- xFF byte`, 104 | &Xstring{}) 105 | } 106 | 107 | func TestString4ByteSurrogate(t *testing.T) { 108 | testExpectedXVal(t, 109 | "𝄞 surrogate, four-byte UTF-8", 110 | `\uD834\uDD1E surrogate, four-byte UTF-8`, 111 | &Xstring{}) 112 | } 113 | 114 | func TestStringNull(t *testing.T) { 115 | testExpectedXValBare(t, 116 | "foobar", 117 | `null`, 118 | &Xstring{X: "foobar"}) 119 | } 120 | 121 | func TestStringQuoted(t *testing.T) { 122 | ver := runtime.Version() 123 | if strings.Contains(ver, "go1.3") || strings.Contains(ver, "go1.2") { 124 | t.Skipf("Test requires go v1.4 or later, this is %s", ver) 125 | } 126 | 127 | testStrQuoted(t, "\x12 escaped control character") 128 | testStrQuoted(t, `\u0012 escaped control character`) 129 | testStrQuoted(t, ", one-byte UTF-8") 130 | testStrQuoted(t, `\u002c one-byte UTF-8`) 131 | testStrQuoted(t, "2ħ籦ö嗏ʑ>嫀") 132 | testStrQuoted(t, `2ħ籦ö嗏ʑ\u003e嫀`) 133 | testStrQuoted(t, "ģ two-byte UTF-8") 134 | testStrQuoted(t, `\u0123 two-byte UTF-8`) 135 | testStrQuoted(t, "ࠡ three-byte UTF-8") 136 | testStrQuoted(t, `\u0821 three-byte UTF-8`) 137 | testStrQuoted(t, `"\`+"\b\f\n\r\t") 138 | testStrQuoted(t, "𝄞 surrogate, four-byte UTF-8") 139 | testStrQuoted(t, string('\xff')+` <- xFF byte`) 140 | testStrQuoted(t, `€þıœəßð some utf-8 ĸʒ×ŋµåäö𝄞`) 141 | testStrQuoted(t, `\/`) 142 | testStrQuoted(t, `/`) 143 | testStrQuoted(t, `\"\\\b\f\n\r\t`) 144 | testStrQuoted(t, `\uD834\uDD1E surrogate, four-byte UTF-8`) 145 | testStrQuoted(t, `null`) 146 | } 147 | 148 | func testStrQuoted(t *testing.T, str string) { 149 | testCycle(t, &TstringTagged{X: str}, &XstringTagged{X: str}) 150 | testCycle(t, &TstringTaggedPtr{X: &str}, &XstringTaggedPtr{X: &str}) 151 | } -------------------------------------------------------------------------------- /tests/fuzz/fuzzit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xe 3 | 4 | # Validate arguments 5 | if [ "$#" -ne 1 ]; then 6 | echo "Usage: $0 " 7 | exit 1 8 | fi 9 | 10 | # Configure 11 | NAME=ffjson 12 | TYPE=$1 13 | FUZZIT_VERSION=2.4.61 14 | GO_FUZZ_VERSION=1810d380ab9c2786af00db592f86d83063216ed0 15 | 16 | # Setup 17 | (cd ../.. && make) 18 | rm -f target_ffjson.go 19 | ffjson target.go 20 | export GO111MODULE=on 21 | go get -u -v \ 22 | github.com/dvyukov/go-fuzz/go-fuzz@$GO_FUZZ_VERSION \ 23 | github.com/dvyukov/go-fuzz/go-fuzz-build@$GO_FUZZ_VERSION 24 | go mod vendor -v 25 | rm -rf gopath 26 | mkdir -p gopath/src 27 | mv vendor/* gopath/src 28 | rm -rf vendor 29 | export GOPATH=$PWD/gopath 30 | export GO111MODULE=off 31 | if [[ ! -f fuzzit || ! `./fuzzit --version` =~ $FUZZIT_VERSION$ ]]; then 32 | wget -q -O fuzzit https://github.com/fuzzitdev/fuzzit/releases/download/v$FUZZIT_VERSION/fuzzit_Linux_x86_64 33 | chmod a+x fuzzit 34 | fi 35 | ./fuzzit --version 36 | 37 | # Fuzz 38 | function fuzz { 39 | FUNC=Fuzz$1 40 | TARGET=$2 41 | go-fuzz-build -libfuzzer -func $FUNC -o fuzzer.a . 42 | clang -fsanitize=fuzzer fuzzer.a -o fuzzer 43 | ./fuzzit create job --type $TYPE $NAME/$TARGET fuzzer 44 | } 45 | fuzz Generate generate 46 | fuzz Unmarshal unmarshal 47 | -------------------------------------------------------------------------------- /tests/fuzz/generator_fuzz.go: -------------------------------------------------------------------------------- 1 | // +build gofuzz 2 | 3 | package fuzz 4 | 5 | import ( 6 | "io/ioutil" 7 | "os" 8 | 9 | _ "github.com/dvyukov/go-fuzz/go-fuzz-dep" 10 | "github.com/pquerna/ffjson/generator" 11 | ) 12 | 13 | // Fuzz tests code generation. 14 | func FuzzGenerate(fuzz []byte) int { 15 | err := os.MkdirAll("fuzzing", os.ModePerm) 16 | if err != nil { 17 | panic("could not make fuzzing dir") 18 | } 19 | err = ioutil.WriteFile("fuzzing/input.go", fuzz, 0644) 20 | if err != nil { 21 | panic("could not write input file") 22 | } 23 | err = generator.GenerateFiles( 24 | "go", 25 | "fuzzing/input.go", 26 | "fuzzing/output.go", 27 | "", 28 | true, 29 | true, 30 | ) 31 | if err != nil { 32 | return 0 33 | } 34 | return 1 35 | } 36 | -------------------------------------------------------------------------------- /tests/fuzz/go.mod: -------------------------------------------------------------------------------- 1 | module fuzz.test/fuzz 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/dvyukov/go-fuzz v0.0.0-20190828145000-1810d380ab9c // indirect 7 | github.com/elazarl/go-bindata-assetfs v1.0.0 // indirect 8 | github.com/stephens2424/writerset v1.0.2 // indirect 9 | golang.org/x/tools v0.0.0-20190911230505-6bfd74cf029c // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /tests/fuzz/go.sum: -------------------------------------------------------------------------------- 1 | github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/dvyukov/go-fuzz v0.0.0-20190828145000-1810d380ab9c h1:DFsz4uHHaXIkv6K/2JhR5ufgx/pquhSsBgB25b5Oj1k= 4 | github.com/dvyukov/go-fuzz v0.0.0-20190828145000-1810d380ab9c/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= 5 | github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk= 6 | github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= 7 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 8 | github.com/pquerna/ffjson v0.0.0-20190813045741-dac163c6c0a9 h1:kyf9snWXHvQc+yxE9imhdI8YAm4oKeZISlaAR+x73zs= 9 | github.com/pquerna/ffjson v0.0.0-20190813045741-dac163c6c0a9/go.mod h1:YARuvh7BUWHNhzDq2OM5tzR2RiCcN2D7sapiKyCel/M= 10 | github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s= 11 | github.com/stephens2424/writerset v1.0.2 h1:znRLgU6g8RS5euYRcy004XeE4W+Tu44kALzy7ghPif8= 12 | github.com/stephens2424/writerset v1.0.2/go.mod h1:aS2JhsMn6eA7e82oNmW4rfsgAOp9COBTTl8mzkwADnc= 13 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 14 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 15 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 16 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 17 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 18 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 19 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 20 | golang.org/x/tools v0.0.0-20190911230505-6bfd74cf029c h1:ZgedNh8bIOBjyY5XEG0kR/41dSN9H+5jFZWuR/TgA1g= 21 | golang.org/x/tools v0.0.0-20190911230505-6bfd74cf029c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 22 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 23 | -------------------------------------------------------------------------------- /tests/fuzz/target.go: -------------------------------------------------------------------------------- 1 | package fuzz 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Data provides an unmarshaling target 8 | type Data struct { 9 | A uint8 10 | B uint16 11 | C uint32 12 | D uint64 13 | 14 | E int8 15 | F int16 16 | G int32 17 | H int64 18 | 19 | I float32 20 | J float64 21 | 22 | M byte 23 | N rune 24 | 25 | O int 26 | P uint 27 | Q string 28 | R bool 29 | S time.Time 30 | 31 | Ap *uint8 32 | Bp *uint16 33 | Cp *uint32 34 | Dp *uint64 35 | 36 | Ep *int8 37 | Fp *int16 38 | Gp *int32 39 | Hp *int64 40 | 41 | IP *float32 42 | Jp *float64 43 | 44 | Mp *byte 45 | Np *rune 46 | 47 | Op *int 48 | Pp *uint 49 | Qp *string 50 | Rp *bool 51 | Sp *time.Time 52 | 53 | Aa []uint8 54 | Ba []uint16 55 | Ca []uint32 56 | Da []uint64 57 | 58 | Ea []int8 59 | Fa []int16 60 | Ga []int32 61 | Ha []int64 62 | 63 | Ia []float32 64 | Ja []float64 65 | 66 | Ma []byte 67 | Na []rune 68 | 69 | Oa []int 70 | Pa []uint 71 | Qa []string 72 | Ra []bool 73 | 74 | Aap []*uint8 75 | Bap []*uint16 76 | Cap []*uint32 77 | Dap []*uint64 78 | 79 | Eap []*int8 80 | Fap []*int16 81 | Gap []*int32 82 | Hap []*int64 83 | 84 | Iap []*float32 85 | Jap []*float64 86 | 87 | Map []*byte 88 | Nap []*rune 89 | 90 | Oap []*int 91 | Pap []*uint 92 | Qap []*string 93 | Rap []*bool 94 | } 95 | -------------------------------------------------------------------------------- /tests/fuzz/target_fuzz.go: -------------------------------------------------------------------------------- 1 | // +build gofuzz 2 | 3 | package fuzz 4 | 5 | import ( 6 | _ "github.com/dvyukov/go-fuzz/go-fuzz-dep" 7 | ) 8 | 9 | // FuzzUnmarshal tests unmarshaling. 10 | func FuzzUnmarshal(fuzz []byte) int { 11 | data := &Data{} 12 | err := data.UnmarshalJSON(fuzz) 13 | if err != nil { 14 | return 0 15 | } 16 | _, err = data.MarshalJSON() 17 | if err != nil { 18 | return 0 19 | } 20 | return 1 21 | } 22 | -------------------------------------------------------------------------------- /tests/go.stripe/base/customer.go: -------------------------------------------------------------------------------- 1 | package stripe 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Customer encapsulates details about a Customer registered in Stripe. 8 | // 9 | // see https://stripe.com/docs/api#customer_object 10 | type Customer struct { 11 | ID string `json:"id"` 12 | Desc string `json:"description,omitempty"` 13 | Email string `json:"email,omitempty"` 14 | Created int64 `json:"created"` 15 | Balance int64 `json:"account_balance"` 16 | Delinquent bool `json:"delinquent"` 17 | Cards CardData `json:"cards,omitempty"` 18 | Discount *Discount `json:"discount,omitempty"` 19 | Subscription *Subscription `json:"subscription,omitempty"` 20 | Livemode bool `json:"livemode"` 21 | DefaultCard string `json:"default_card"` 22 | } 23 | 24 | // CardData detaiks about cards 25 | type CardData struct { 26 | Object string `json:"object"` 27 | Count int `json:"count"` 28 | URL string `json:"url"` 29 | Data []*Card `json:"data"` 30 | } 31 | 32 | // Credit Card Types accepted by the Stripe API. 33 | const ( 34 | AmericanExpress = "American Express" 35 | DinersClub = "Diners Club" 36 | Discover = "Discover" 37 | JCB = "JCB" 38 | MasterCard = "MasterCard" 39 | Visa = "Visa" 40 | UnknownCard = "Unknown" 41 | ) 42 | 43 | // Card represents details about a Credit Card entered into Stripe. 44 | type Card struct { 45 | ID string `json:"id"` 46 | Name string `json:"name,omitempty"` 47 | Type string `json:"type"` 48 | ExpMonth int `json:"exp_month"` 49 | ExpYear int `json:"exp_year"` 50 | Last4 string `json:"last4"` 51 | Fingerprint string `json:"fingerprint"` 52 | Country string `json:"country,omitempty"` 53 | AddrUess1 string `json:"address_line1,omitempty"` 54 | Address2 string `json:"address_line2,omitempty"` 55 | AddressCountry string `json:"address_country,omitempty"` 56 | AddressState string `json:"address_state,omitempty"` 57 | AddressZip string `json:"address_zip,omitempty"` 58 | AddressCity string `json:"address_city"` 59 | AddressLine1Check string `json:"address_line1_check,omitempty"` 60 | AddressZipCheck string `json:"address_zip_check,omitempty"` 61 | CVCCheck string `json:"cvc_check,omitempty"` 62 | } 63 | 64 | // Discount represents the actual application of a coupon to a particular 65 | // customer. 66 | // 67 | // see https://stripe.com/docs/api#discount_object 68 | type Discount struct { 69 | ID string `json:"id"` 70 | Customer string `json:"customer"` 71 | Start int64 `json:"start"` 72 | End int64 `json:"end"` 73 | Coupon *Coupon `json:"coupon"` 74 | } 75 | 76 | // Coupon represents percent-off discount you might want to apply to a customer. 77 | // 78 | // see https://stripe.com/docs/api#coupon_object 79 | type Coupon struct { 80 | ID string `json:"id"` 81 | Duration string `json:"duration"` 82 | PercentOff int `json:"percent_off"` 83 | DurationInMonths int `json:"duration_in_months,omitempty"` 84 | MaxRedemptions int `json:"max_redemptions,omitempty"` 85 | RedeemBy int64 `json:"redeem_by,omitempty"` 86 | TimesRedeemed int `json:"times_redeemed,omitempty"` 87 | Livemode bool `json:"livemode"` 88 | } 89 | 90 | // Subscription Statuses 91 | const ( 92 | SubscriptionTrialing = "trialing" 93 | SubscriptionActive = "active" 94 | SubscriptionPastDue = "past_due" 95 | SubscriptionCanceled = "canceled" 96 | SubscriptionUnpaid = "unpaid" 97 | ) 98 | 99 | // Subscription represents a recurring charge a customer's card. 100 | // 101 | // see https://stripe.com/docs/api#subscription_object 102 | type Subscription struct { 103 | Customer string `json:"customer"` 104 | Status string `json:"status"` 105 | Plan *Plan `json:"plan"` 106 | Start int64 `json:"start"` 107 | EndedAt int64 `json:"ended_at"` 108 | CurrentPeriodStart int64 `json:"current_period_start"` 109 | CurrentPeriodEnd int64 `json:"current_period_end"` 110 | TrialStart int64 `json:"trial_start"` 111 | TrialEnd int64 `json:"trial_end"` 112 | CanceledAt int64 `json:"canceled_at"` 113 | CancelAtPeriodEnd bool `json:"cancel_at_period_end"` 114 | Quantity int64 `json:"quantity"` 115 | } 116 | 117 | // Plan holds details about pricing information for different products and 118 | // feature levels on your site. For example, you might have a $10/month plan 119 | // for basic features and a different $20/month plan for premium features. 120 | // 121 | // see https://stripe.com/docs/api#plan_object 122 | type Plan struct { 123 | ID string `json:"id"` 124 | Name string `json:"name"` 125 | Amount int64 `json:"amount"` 126 | Interval string `json:"interval"` 127 | IntervalCount int `json:"interval_count"` 128 | Currency string `json:"currency"` 129 | TrialPeriodDays int `json:"trial_period_days"` 130 | Livemode bool `json:"livemode"` 131 | } 132 | 133 | // NewCustomer creates a new customer 134 | func NewCustomer() *Customer { 135 | 136 | return &Customer{ 137 | ID: "hooN5ne7ug", 138 | Desc: "A very nice customer.", 139 | Email: "customer@example.com", 140 | Created: time.Now().UnixNano(), 141 | Balance: 10, 142 | Delinquent: false, 143 | Cards: CardData{ 144 | Object: "A92F4CFE-8B6B-4176-873E-887AC0D120EB", 145 | Count: 1, 146 | URL: "https://stripe.example.com/card/A92F4CFE-8B6B-4176-873E-887AC0D120EB", 147 | Data: []*Card{ 148 | &Card{ 149 | Name: "John Smith", 150 | ID: "7526EC97-A0B6-47B2-AAE5-17443626A116", 151 | Fingerprint: "4242424242424242", 152 | ExpYear: time.Now().Year() + 1, 153 | ExpMonth: 1, 154 | }, 155 | }, 156 | }, 157 | Discount: &Discount{ 158 | ID: "Ee9ieZ8zie", 159 | Customer: "hooN5ne7ug", 160 | Start: time.Now().UnixNano(), 161 | End: time.Now().UnixNano(), 162 | Coupon: &Coupon{ 163 | ID: "ieQuo5Aiph", 164 | Duration: "2m", 165 | PercentOff: 10, 166 | DurationInMonths: 2, 167 | MaxRedemptions: 1, 168 | RedeemBy: time.Now().UnixNano(), 169 | TimesRedeemed: 1, 170 | Livemode: true, 171 | }, 172 | }, 173 | Subscription: &Subscription{ 174 | Customer: "hooN5ne7ug", 175 | Status: SubscriptionActive, 176 | Plan: &Plan{ 177 | ID: "gaiyeLua5u", 178 | Name: "Great Plan (TM)", 179 | Amount: 10, 180 | Interval: "monthly", 181 | IntervalCount: 3, 182 | Currency: "USD", 183 | TrialPeriodDays: 15, 184 | Livemode: true, 185 | }, 186 | Start: time.Now().UnixNano(), 187 | EndedAt: 0, 188 | CurrentPeriodStart: time.Now().UnixNano(), 189 | CurrentPeriodEnd: time.Now().UnixNano(), 190 | TrialStart: time.Now().UnixNano(), 191 | TrialEnd: time.Now().UnixNano(), 192 | CanceledAt: 0, 193 | CancelAtPeriodEnd: false, 194 | Quantity: 2, 195 | }, 196 | Livemode: true, 197 | DefaultCard: "7526EC97-A0B6-47B2-AAE5-17443626A116", 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /tests/go.stripe/ff/customer.go: -------------------------------------------------------------------------------- 1 | package stripe 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Customer encapsulates details about a Customer registered in Stripe. 8 | // 9 | // see https://stripe.com/docs/api#customer_object 10 | type Customer struct { 11 | ID string `json:"id"` 12 | Desc string `json:"description,omitempty"` 13 | Email string `json:"email,omitempty"` 14 | Created int64 `json:"created"` 15 | Balance int64 `json:"account_balance"` 16 | Delinquent bool `json:"delinquent"` 17 | Cards CardData `json:"cards,omitempty"` 18 | Discount *Discount `json:"discount,omitempty"` 19 | Subscription *Subscription `json:"subscription,omitempty"` 20 | Livemode bool `json:"livemode"` 21 | DefaultCard string `json:"default_card"` 22 | } 23 | 24 | // CardData struct 25 | type CardData struct { 26 | Object string `json:"object"` 27 | Count int `json:"count"` 28 | URL string `json:"url"` 29 | Data []*Card `json:"data"` 30 | } 31 | 32 | // Credit Card Types accepted by the Stripe API. 33 | const ( 34 | AmericanExpress = "American Express" 35 | DinersClub = "Diners Club" 36 | Discover = "Discover" 37 | JCB = "JCB" 38 | MasterCard = "MasterCard" 39 | Visa = "Visa" 40 | UnknownCard = "Unknown" 41 | ) 42 | 43 | // Card represents details about a Credit Card entered into Stripe. 44 | type Card struct { 45 | ID string `json:"id"` 46 | Name string `json:"name,omitempty"` 47 | Type string `json:"type"` 48 | ExpMonth int `json:"exp_month"` 49 | ExpYear int `json:"exp_year"` 50 | Last4 string `json:"last4"` 51 | Fingerprint string `json:"fingerprint"` 52 | Country string `json:"country,omitempty"` 53 | AddrUess1 string `json:"address_line1,omitempty"` 54 | Address2 string `json:"address_line2,omitempty"` 55 | AddressCountry string `json:"address_country,omitempty"` 56 | AddressState string `json:"address_state,omitempty"` 57 | AddressZip string `json:"address_zip,omitempty"` 58 | AddressCity string `json:"address_city"` 59 | AddressLine1Check string `json:"address_line1_check,omitempty"` 60 | AddressZipCheck string `json:"address_zip_check,omitempty"` 61 | CVCCheck string `json:"cvc_check,omitempty"` 62 | } 63 | 64 | // Discount represents the actual application of a coupon to a particular 65 | // customer. 66 | // 67 | // see https://stripe.com/docs/api#discount_object 68 | type Discount struct { 69 | ID string `json:"id"` 70 | Customer string `json:"customer"` 71 | Start int64 `json:"start"` 72 | End int64 `json:"end"` 73 | Coupon *Coupon `json:"coupon"` 74 | } 75 | 76 | // Coupon represents percent-off discount you might want to apply to a customer. 77 | // 78 | // see https://stripe.com/docs/api#coupon_object 79 | type Coupon struct { 80 | ID string `json:"id"` 81 | Duration string `json:"duration"` 82 | PercentOff int `json:"percent_off"` 83 | DurationInMonths int `json:"duration_in_months,omitempty"` 84 | MaxRedemptions int `json:"max_redemptions,omitempty"` 85 | RedeemBy int64 `json:"redeem_by,omitempty"` 86 | TimesRedeemed int `json:"times_redeemed,omitempty"` 87 | Livemode bool `json:"livemode"` 88 | } 89 | 90 | // Subscription Statuses 91 | const ( 92 | SubscriptionTrialing = "trialing" 93 | SubscriptionActive = "active" 94 | SubscriptionPastDue = "past_due" 95 | SubscriptionCanceled = "canceled" 96 | SubscriptionUnpaid = "unpaid" 97 | ) 98 | 99 | // Subscription represents a recurring charge a customer's card. 100 | // 101 | // see https://stripe.com/docs/api#subscription_object 102 | type Subscription struct { 103 | Customer string `json:"customer"` 104 | Status string `json:"status"` 105 | Plan *Plan `json:"plan"` 106 | Start int64 `json:"start"` 107 | EndedAt int64 `json:"ended_at"` 108 | CurrentPeriodStart int64 `json:"current_period_start"` 109 | CurrentPeriodEnd int64 `json:"current_period_end"` 110 | TrialStart int64 `json:"trial_start"` 111 | TrialEnd int64 `json:"trial_end"` 112 | CanceledAt int64 `json:"canceled_at"` 113 | CancelAtPeriodEnd bool `json:"cancel_at_period_end"` 114 | Quantity int64 `json:"quantity"` 115 | } 116 | 117 | // Plan holds details about pricing information for different products and 118 | // feature levels on your site. For example, you might have a $10/month plan 119 | // for basic features and a different $20/month plan for premium features. 120 | // 121 | // see https://stripe.com/docs/api#plan_object 122 | type Plan struct { 123 | ID string `json:"id"` 124 | Name string `json:"name"` 125 | Amount int64 `json:"amount"` 126 | Interval string `json:"interval"` 127 | IntervalCount int `json:"interval_count"` 128 | Currency string `json:"currency"` 129 | TrialPeriodDays int `json:"trial_period_days"` 130 | Livemode bool `json:"livemode"` 131 | } 132 | 133 | // NewCustomer creates a customer 134 | func NewCustomer() *Customer { 135 | 136 | return &Customer{ 137 | ID: "hooN5ne7ug", 138 | Desc: "A very nice customer.", 139 | Email: "customer@example.com", 140 | Created: time.Now().UnixNano(), 141 | Balance: 10, 142 | Delinquent: false, 143 | Cards: CardData{ 144 | Object: "A92F4CFE-8B6B-4176-873E-887AC0D120EB", 145 | Count: 1, 146 | URL: "https://stripe.example.com/card/A92F4CFE-8B6B-4176-873E-887AC0D120EB", 147 | Data: []*Card{ 148 | &Card{ 149 | Name: "John Smith", 150 | ID: "7526EC97-A0B6-47B2-AAE5-17443626A116", 151 | Fingerprint: "4242424242424242", 152 | ExpYear: time.Now().Year() + 1, 153 | ExpMonth: 1, 154 | }, 155 | }, 156 | }, 157 | Discount: &Discount{ 158 | ID: "Ee9ieZ8zie", 159 | Customer: "hooN5ne7ug", 160 | Start: time.Now().UnixNano(), 161 | End: time.Now().UnixNano(), 162 | Coupon: &Coupon{ 163 | ID: "ieQuo5Aiph", 164 | Duration: "2m", 165 | PercentOff: 10, 166 | DurationInMonths: 2, 167 | MaxRedemptions: 1, 168 | RedeemBy: time.Now().UnixNano(), 169 | TimesRedeemed: 1, 170 | Livemode: true, 171 | }, 172 | }, 173 | Subscription: &Subscription{ 174 | Customer: "hooN5ne7ug", 175 | Status: SubscriptionActive, 176 | Plan: &Plan{ 177 | ID: "gaiyeLua5u", 178 | Name: "Great Plan (TM)", 179 | Amount: 10, 180 | Interval: "monthly", 181 | IntervalCount: 3, 182 | Currency: "USD", 183 | TrialPeriodDays: 15, 184 | Livemode: true, 185 | }, 186 | Start: time.Now().UnixNano(), 187 | EndedAt: 0, 188 | CurrentPeriodStart: time.Now().UnixNano(), 189 | CurrentPeriodEnd: time.Now().UnixNano(), 190 | TrialStart: time.Now().UnixNano(), 191 | TrialEnd: time.Now().UnixNano(), 192 | CanceledAt: 0, 193 | CancelAtPeriodEnd: false, 194 | Quantity: 2, 195 | }, 196 | Livemode: true, 197 | DefaultCard: "7526EC97-A0B6-47B2-AAE5-17443626A116", 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /tests/go.stripe/stripe_test.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package goser 19 | 20 | import ( 21 | "encoding/json" 22 | base "github.com/pquerna/ffjson/tests/go.stripe/base" 23 | ff "github.com/pquerna/ffjson/tests/go.stripe/ff" 24 | "testing" 25 | ) 26 | 27 | func TestRoundTrip(t *testing.T) { 28 | var customerTripped ff.Customer 29 | customer := ff.NewCustomer() 30 | 31 | buf1, err := json.Marshal(&customer) 32 | if err != nil { 33 | t.Fatalf("Marshal: %v", err) 34 | } 35 | 36 | err = json.Unmarshal(buf1, &customerTripped) 37 | if err != nil { 38 | print(string(buf1)) 39 | t.Fatalf("Unmarshal: %v", err) 40 | } 41 | } 42 | 43 | func BenchmarkMarshalJSON(b *testing.B) { 44 | cust := base.NewCustomer() 45 | 46 | buf, err := json.Marshal(&cust) 47 | if err != nil { 48 | b.Fatalf("Marshal: %v", err) 49 | } 50 | b.SetBytes(int64(len(buf))) 51 | 52 | b.ResetTimer() 53 | for i := 0; i < b.N; i++ { 54 | _, err := json.Marshal(&cust) 55 | if err != nil { 56 | b.Fatalf("Marshal: %v", err) 57 | } 58 | } 59 | } 60 | 61 | func BenchmarkFFMarshalJSON(b *testing.B) { 62 | cust := ff.NewCustomer() 63 | 64 | buf, err := cust.MarshalJSON() 65 | if err != nil { 66 | b.Fatalf("Marshal: %v", err) 67 | } 68 | b.SetBytes(int64(len(buf))) 69 | 70 | b.ResetTimer() 71 | for i := 0; i < b.N; i++ { 72 | _, err := cust.MarshalJSON() 73 | if err != nil { 74 | b.Fatalf("Marshal: %v", err) 75 | } 76 | } 77 | } 78 | 79 | type fatalF interface { 80 | Fatalf(format string, args ...interface{}) 81 | } 82 | 83 | func getBaseData(b fatalF) []byte { 84 | cust := base.NewCustomer() 85 | buf, err := json.MarshalIndent(&cust, "", " ") 86 | if err != nil { 87 | b.Fatalf("Marshal: %v", err) 88 | } 89 | return buf 90 | } 91 | 92 | func BenchmarkUnmarshalJSON(b *testing.B) { 93 | rec := base.Customer{} 94 | buf := getBaseData(b) 95 | b.SetBytes(int64(len(buf))) 96 | 97 | b.ResetTimer() 98 | for i := 0; i < b.N; i++ { 99 | err := json.Unmarshal(buf, &rec) 100 | if err != nil { 101 | b.Fatalf("Marshal: %v", err) 102 | } 103 | } 104 | } 105 | 106 | func BenchmarkFFUnmarshalJSON(b *testing.B) { 107 | rec := ff.Customer{} 108 | buf := getBaseData(b) 109 | b.SetBytes(int64(len(buf))) 110 | 111 | b.ResetTimer() 112 | for i := 0; i < b.N; i++ { 113 | err := rec.UnmarshalJSON(buf) 114 | if err != nil { 115 | b.Fatalf("UnmarshalJSON: %v", err) 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tests/goser/base/goser.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package goser 19 | 20 | import ( 21 | "io" 22 | "net" 23 | "time" 24 | ) 25 | 26 | // CacheStatus of goser 27 | type CacheStatus int32 28 | 29 | const ( 30 | // CACHESTATUSUNKNOWN unknown cache status 31 | CACHESTATUSUNKNOWN CacheStatus = 0 32 | // CACHESTATUSMISS miss cache status 33 | CACHESTATUSMISS CacheStatus = 1 34 | // CACHESTATUSEXPIRED exipred cache status 35 | CACHESTATUSEXPIRED CacheStatus = 2 36 | // CACHESTATUSHIT hit cache status 37 | CACHESTATUSHIT CacheStatus = 3 38 | ) 39 | 40 | // HTTPProtocol of goser 41 | type HTTPProtocol int32 42 | 43 | const ( 44 | // HTTPPROTOCOLUNKNOWN http protocol unknown 45 | HTTPPROTOCOLUNKNOWN HTTPProtocol = 0 46 | // HTTPPROTOCOL10 http protocol 10 47 | HTTPPROTOCOL10 HTTPProtocol = 1 48 | // HTTPPROTOCOL11 http protocol 11 49 | HTTPPROTOCOL11 HTTPProtocol = 2 50 | ) 51 | 52 | // HTTPMethod of goser 53 | type HTTPMethod int32 54 | 55 | const ( 56 | // HTTPMETHODUNKNOWN unknown http method 57 | HTTPMETHODUNKNOWN HTTPMethod = 0 58 | // HTTPMETHODGET get http method 59 | HTTPMETHODGET HTTPMethod = 1 60 | // HTTPMETHODPOST post http method 61 | HTTPMETHODPOST HTTPMethod = 2 62 | // HTTPMETHODDELETE delete http method 63 | HTTPMETHODDELETE HTTPMethod = 3 64 | // HTTPMETHODPUT put http method 65 | HTTPMETHODPUT HTTPMethod = 4 66 | // HTTPMETHODHEAD head http method 67 | HTTPMETHODHEAD HTTPMethod = 5 68 | // HTTPMETHODPURGE purge http method 69 | HTTPMETHODPURGE HTTPMethod = 6 70 | // HTTPMETHODOPTIONS options http method 71 | HTTPMETHODOPTIONS HTTPMethod = 7 72 | // HTTPMETHODPROPFIND propfind http method 73 | HTTPMETHODPROPFIND HTTPMethod = 8 74 | // HTTPMETHODMKCOL mkcol http method 75 | HTTPMETHODMKCOL HTTPMethod = 9 76 | // HTTPMETHODPATCH patch http method 77 | HTTPMETHODPATCH HTTPMethod = 10 78 | ) 79 | 80 | // OriginProtocol type 81 | type OriginProtocol int32 82 | 83 | const ( 84 | // ORIGINPROTOCOLUNKNOWN origin protocol unknown 85 | ORIGINPROTOCOLUNKNOWN OriginProtocol = 0 86 | // ORIGINPROTOCOLHTTP origin protocol http 87 | ORIGINPROTOCOLHTTP OriginProtocol = 1 88 | // ORIGINPROTOCOLHTTPS origin protocol https 89 | ORIGINPROTOCOLHTTPS OriginProtocol = 2 90 | ) 91 | 92 | // HTTP struct type 93 | type HTTP struct { 94 | Protocol HTTPProtocol `json:"protocol"` 95 | Status uint32 `json:"status"` 96 | HostStatus uint32 `json:"hostStatus"` 97 | UpStatus uint32 `json:"upStatus"` 98 | Method HTTPMethod `json:"method"` 99 | ContentType string `json:"contentType"` 100 | UserAgent string `json:"userAgent"` 101 | Referer string `json:"referer"` 102 | RequestURI string `json:"requestURI"` 103 | Unrecognized []byte `json:"-"` 104 | } 105 | 106 | // Origin struct 107 | type Origin struct { 108 | IP IP `json:"ip"` 109 | Port uint32 `json:"port"` 110 | Hostname string `json:"hostname"` 111 | Protocol OriginProtocol `json:"protocol"` 112 | } 113 | 114 | // ZonePlan type 115 | type ZonePlan int32 116 | 117 | const ( 118 | // ZONEPLANUNKNOWN unknwon zone plan 119 | ZONEPLANUNKNOWN ZonePlan = 0 120 | // ZONEPLANFREE free zone plan 121 | ZONEPLANFREE ZonePlan = 1 122 | // ZONEPLANPRO pro zone plan 123 | ZONEPLANPRO ZonePlan = 2 124 | // ZONEPLANBIZ biz zone plan 125 | ZONEPLANBIZ ZonePlan = 3 126 | // ZONEPLANENT ent zone plan 127 | ZONEPLANENT ZonePlan = 4 128 | ) 129 | 130 | // Country type 131 | type Country int32 132 | 133 | const ( 134 | // COUNTRYUNKNOWN unknwon country 135 | COUNTRYUNKNOWN Country = 0 136 | // COUNTRYUS us country 137 | COUNTRYUS Country = 238 138 | ) 139 | 140 | // Log struct 141 | type Log struct { 142 | Timestamp int64 `json:"timestamp"` 143 | ZoneID uint32 `json:"zoneId"` 144 | ZonePlan ZonePlan `json:"zonePlan"` 145 | HTTP HTTP `json:"http"` 146 | Origin Origin `json:"origin"` 147 | Country Country `json:"country"` 148 | CacheStatus CacheStatus `json:"cacheStatus"` 149 | ServerIP IP `json:"serverIp"` 150 | ServerName string `json:"serverName"` 151 | RemoteIP IP `json:"remoteIp"` 152 | BytesDlv uint64 `json:"bytesDlv"` 153 | RayID string `json:"rayId"` 154 | Unrecognized []byte `json:"-"` 155 | } 156 | 157 | // IP type 158 | type IP net.IP 159 | 160 | // MarshalJSON function 161 | func (ip IP) MarshalJSON() ([]byte, error) { 162 | return []byte("\"" + net.IP(ip).String() + "\""), nil 163 | } 164 | 165 | // UnmarshalJSON function 166 | func (ip *IP) UnmarshalJSON(data []byte) error { 167 | if len(data) < 2 { 168 | return io.ErrShortBuffer 169 | } 170 | *ip = IP(net.ParseIP(string(data[1 : len(data)-1])).To4()) 171 | return nil 172 | } 173 | 174 | const userAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.146 Safari/537.36" 175 | 176 | // NewLog creates a new log 177 | func NewLog(record *Log) { 178 | record.Timestamp = time.Now().UnixNano() 179 | record.ZoneID = 123456 180 | record.ZonePlan = ZONEPLANFREE 181 | 182 | record.HTTP = HTTP{ 183 | Protocol: HTTPPROTOCOL11, 184 | Status: 200, 185 | HostStatus: 503, 186 | UpStatus: 520, 187 | Method: HTTPMETHODGET, 188 | ContentType: "text/html", 189 | UserAgent: userAgent, 190 | Referer: "https://www.cloudflare.com/", 191 | RequestURI: "/cdn-cgi/trace", 192 | } 193 | 194 | record.Origin = Origin{ 195 | IP: IP(net.IPv4(1, 2, 3, 4).To4()), 196 | Port: 8080, 197 | Hostname: "www.example.com", 198 | Protocol: ORIGINPROTOCOLHTTPS, 199 | } 200 | 201 | record.Country = COUNTRYUS 202 | record.CacheStatus = CACHESTATUSHIT 203 | record.ServerIP = IP(net.IPv4(192, 168, 1, 1).To4()) 204 | record.ServerName = "metal.cloudflare.com" 205 | record.RemoteIP = IP(net.IPv4(10, 1, 2, 3).To4()) 206 | record.BytesDlv = 123456 207 | record.RayID = "10c73629cce30078-LAX" 208 | } 209 | -------------------------------------------------------------------------------- /tests/goser/ff/goser.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package goser 19 | 20 | import ( 21 | fflib "github.com/pquerna/ffjson/fflib/v1" 22 | 23 | "io" 24 | "net" 25 | "time" 26 | ) 27 | 28 | // CacheStatus of goser 29 | type CacheStatus int32 30 | 31 | const ( 32 | // CACHESTATUSUNKNOWN unknown cache status 33 | CACHESTATUSUNKNOWN CacheStatus = 0 34 | // CACHESTATUSMISS miss cache status 35 | CACHESTATUSMISS CacheStatus = 1 36 | // CACHESTATUSEXPIRED exipred cache status 37 | CACHESTATUSEXPIRED CacheStatus = 2 38 | // CACHESTATUSHIT hit cache status 39 | CACHESTATUSHIT CacheStatus = 3 40 | ) 41 | 42 | // HTTPProtocol of goser 43 | type HTTPProtocol int32 44 | 45 | const ( 46 | // HTTPPROTOCOLUNKNOWN http protocol unknown 47 | HTTPPROTOCOLUNKNOWN HTTPProtocol = 0 48 | // HTTPPROTOCOL10 http protocol 10 49 | HTTPPROTOCOL10 HTTPProtocol = 1 50 | // HTTPPROTOCOL11 http protocol 11 51 | HTTPPROTOCOL11 HTTPProtocol = 2 52 | ) 53 | 54 | // HTTPMethod of goser 55 | type HTTPMethod int32 56 | 57 | const ( 58 | // HTTPMETHODUNKNOWN unknown http method 59 | HTTPMETHODUNKNOWN HTTPMethod = 0 60 | // HTTPMETHODGET get http method 61 | HTTPMETHODGET HTTPMethod = 1 62 | // HTTPMETHODPOST post http method 63 | HTTPMETHODPOST HTTPMethod = 2 64 | // HTTPMETHODDELETE delete http method 65 | HTTPMETHODDELETE HTTPMethod = 3 66 | // HTTPMETHODPUT put http method 67 | HTTPMETHODPUT HTTPMethod = 4 68 | // HTTPMETHODHEAD head http method 69 | HTTPMETHODHEAD HTTPMethod = 5 70 | // HTTPMETHODPURGE purge http method 71 | HTTPMETHODPURGE HTTPMethod = 6 72 | // HTTPMETHODOPTIONS options http method 73 | HTTPMETHODOPTIONS HTTPMethod = 7 74 | // HTTPMETHODPROPFIND propfind http method 75 | HTTPMETHODPROPFIND HTTPMethod = 8 76 | // HTTPMETHODMKCOL mkcol http method 77 | HTTPMETHODMKCOL HTTPMethod = 9 78 | // HTTPMETHODPATCH patch http method 79 | HTTPMETHODPATCH HTTPMethod = 10 80 | ) 81 | 82 | // OriginProtocol type 83 | type OriginProtocol int32 84 | 85 | const ( 86 | // ORIGINPROTOCOLUNKNOWN origin protocol unknown 87 | ORIGINPROTOCOLUNKNOWN OriginProtocol = 0 88 | // ORIGINPROTOCOLHTTP origin protocol http 89 | ORIGINPROTOCOLHTTP OriginProtocol = 1 90 | // ORIGINPROTOCOLHTTPS origin protocol https 91 | ORIGINPROTOCOLHTTPS OriginProtocol = 2 92 | ) 93 | 94 | // HTTP struct type 95 | type HTTP struct { 96 | Protocol HTTPProtocol `json:"protocol"` 97 | Status uint32 `json:"status"` 98 | HostStatus uint32 `json:"hostStatus"` 99 | UpStatus uint32 `json:"upStatus"` 100 | Method HTTPMethod `json:"method"` 101 | ContentType string `json:"contentType"` 102 | UserAgent string `json:"userAgent"` 103 | Referer string `json:"referer"` 104 | RequestURI string `json:"requestURI"` 105 | Unrecognized []byte `json:"-"` 106 | } 107 | 108 | // Origin struct 109 | type Origin struct { 110 | IP IP `json:"ip"` 111 | Port uint32 `json:"port"` 112 | Hostname string `json:"hostname"` 113 | Protocol OriginProtocol `json:"protocol"` 114 | } 115 | 116 | // ZonePlan type 117 | type ZonePlan int32 118 | 119 | const ( 120 | // ZONEPLANUNKNOWN unknwon zone plan 121 | ZONEPLANUNKNOWN ZonePlan = 0 122 | // ZONEPLANFREE free zone plan 123 | ZONEPLANFREE ZonePlan = 1 124 | // ZONEPLANPRO pro zone plan 125 | ZONEPLANPRO ZonePlan = 2 126 | // ZONEPLANBIZ biz zone plan 127 | ZONEPLANBIZ ZonePlan = 3 128 | // ZONEPLANENT ent zone plan 129 | ZONEPLANENT ZonePlan = 4 130 | ) 131 | 132 | // Country type 133 | type Country int32 134 | 135 | const ( 136 | // COUNTRYUNKNOWN unknwon country 137 | COUNTRYUNKNOWN Country = 0 138 | // COUNTRYUS us country 139 | COUNTRYUS Country = 238 140 | ) 141 | 142 | // Log struct 143 | type Log struct { 144 | Timestamp int64 `json:"timestamp"` 145 | ZoneID uint32 `json:"zoneId"` 146 | ZonePlan ZonePlan `json:"zonePlan"` 147 | HTTP HTTP `json:"http"` 148 | Origin Origin `json:"origin"` 149 | Country Country `json:"country"` 150 | CacheStatus CacheStatus `json:"cacheStatus"` 151 | ServerIP IP `json:"serverIp"` 152 | ServerName string `json:"serverName"` 153 | RemoteIP IP `json:"remoteIp"` 154 | BytesDlv uint64 `json:"bytesDlv"` 155 | RayID string `json:"rayId"` 156 | Unrecognized []byte `json:"-"` 157 | } 158 | 159 | // IP type 160 | type IP net.IP 161 | 162 | // MarshalJSON set ip to json 163 | func (ip IP) MarshalJSON() ([]byte, error) { 164 | return []byte("\"" + net.IP(ip).String() + "\""), nil 165 | } 166 | 167 | // MarshalJSONBuf set ip to json with buf 168 | func (ip IP) MarshalJSONBuf(buf fflib.EncodingBuffer) error { 169 | buf.WriteByte('"') 170 | buf.WriteString(net.IP(ip).String()) 171 | buf.WriteByte('"') 172 | return nil 173 | } 174 | 175 | // UnmarshalJSON umarshall json to ip 176 | func (ip *IP) UnmarshalJSON(data []byte) error { 177 | if len(data) < 2 { 178 | return io.ErrShortBuffer 179 | } 180 | *ip = IP(net.ParseIP(string(data[1 : len(data)-1])).To4()) 181 | return nil 182 | } 183 | 184 | const userAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.146 Safari/537.36" 185 | 186 | // NewLog creates a new log 187 | func NewLog(record *Log) { 188 | record.Timestamp = time.Now().UnixNano() 189 | record.ZoneID = 123456 190 | record.ZonePlan = ZONEPLANFREE 191 | 192 | record.HTTP = HTTP{ 193 | Protocol: HTTPPROTOCOL11, 194 | Status: 200, 195 | HostStatus: 503, 196 | UpStatus: 520, 197 | Method: HTTPMETHODGET, 198 | ContentType: "text/html", 199 | UserAgent: userAgent, 200 | Referer: "https://www.cloudflare.com/", 201 | RequestURI: "/cdn-cgi/trace", 202 | } 203 | 204 | record.Origin = Origin{ 205 | IP: IP(net.IPv4(1, 2, 3, 4).To4()), 206 | Port: 8080, 207 | Hostname: "www.example.com", 208 | Protocol: ORIGINPROTOCOLHTTPS, 209 | } 210 | 211 | record.Country = COUNTRYUS 212 | record.CacheStatus = CACHESTATUSHIT 213 | record.ServerIP = IP(net.IPv4(192, 168, 1, 1).To4()) 214 | record.ServerName = "metal.cloudflare.com" 215 | record.RemoteIP = IP(net.IPv4(10, 1, 2, 3).To4()) 216 | record.BytesDlv = 123456 217 | record.RayID = "10c73629cce30078-LAX" 218 | } 219 | -------------------------------------------------------------------------------- /tests/goser/goser_test.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package goser 19 | 20 | import ( 21 | "encoding/json" 22 | "fmt" 23 | base "github.com/pquerna/ffjson/tests/goser/base" 24 | ff "github.com/pquerna/ffjson/tests/goser/ff" 25 | "reflect" 26 | "testing" 27 | ) 28 | 29 | func TestRoundTrip(t *testing.T) { 30 | var record ff.Log 31 | var recordTripped ff.Log 32 | ff.NewLog(&record) 33 | 34 | buf1, err := json.Marshal(&record) 35 | if err != nil { 36 | t.Fatalf("Marshal: %v", err) 37 | } 38 | err = json.Unmarshal(buf1, &recordTripped) 39 | if err != nil { 40 | t.Fatalf("Unmarshal: %v", err) 41 | } 42 | 43 | good := reflect.DeepEqual(record, recordTripped) 44 | if !good { 45 | t.Fatalf("Expected: %v\n Got: %v", record, recordTripped) 46 | } 47 | } 48 | 49 | func BenchmarkMarshalJSON(b *testing.B) { 50 | var record base.Log 51 | base.NewLog(&record) 52 | 53 | buf, err := json.Marshal(&record) 54 | if err != nil { 55 | b.Fatalf("Marshal: %v", err) 56 | } 57 | b.SetBytes(int64(len(buf))) 58 | 59 | b.ResetTimer() 60 | for i := 0; i < b.N; i++ { 61 | _, err := json.Marshal(&record) 62 | if err != nil { 63 | b.Fatalf("Marshal: %v", err) 64 | } 65 | } 66 | } 67 | 68 | func BenchmarkFFMarshalJSON(b *testing.B) { 69 | var record ff.Log 70 | ff.NewLog(&record) 71 | 72 | buf, err := record.MarshalJSON() 73 | if err != nil { 74 | b.Fatalf("Marshal: %v", err) 75 | } 76 | b.SetBytes(int64(len(buf))) 77 | 78 | b.ResetTimer() 79 | for i := 0; i < b.N; i++ { 80 | _, err := record.MarshalJSON() 81 | if err != nil { 82 | b.Fatalf("Marshal: %v", err) 83 | } 84 | } 85 | } 86 | 87 | type fatalF interface { 88 | Fatalf(format string, args ...interface{}) 89 | } 90 | 91 | func getBaseData(b fatalF) []byte { 92 | var record base.Log 93 | base.NewLog(&record) 94 | buf, err := json.MarshalIndent(&record, "", " ") 95 | if err != nil { 96 | b.Fatalf("Marshal: %v", err) 97 | } 98 | return buf 99 | } 100 | 101 | func BenchmarkUnmarshalJSON(b *testing.B) { 102 | rec := base.Log{} 103 | buf := getBaseData(b) 104 | b.SetBytes(int64(len(buf))) 105 | 106 | b.ResetTimer() 107 | for i := 0; i < b.N; i++ { 108 | err := json.Unmarshal(buf, &rec) 109 | if err != nil { 110 | b.Fatalf("Marshal: %v", err) 111 | } 112 | } 113 | } 114 | 115 | func BenchmarkFFUnmarshalJSON(b *testing.B) { 116 | rec := ff.Log{} 117 | buf := getBaseData(b) 118 | b.SetBytes(int64(len(buf))) 119 | 120 | b.ResetTimer() 121 | for i := 0; i < b.N; i++ { 122 | err := rec.UnmarshalJSON(buf) 123 | if err != nil { 124 | b.Fatalf("UnmarshalJSON: %v", err) 125 | } 126 | } 127 | } 128 | 129 | func TestUnmarshal(t *testing.T) { 130 | rec := ff.Log{} 131 | buf := getBaseData(t) 132 | 133 | err := rec.UnmarshalJSON(buf) 134 | if err != nil { 135 | t.Fatalf("Unmarshal: %v from %s", err, string(buf)) 136 | } 137 | 138 | rec2 := base.Log{} 139 | json.Unmarshal(buf, &rec2) 140 | 141 | a := fmt.Sprintf("%v", rec) 142 | b := fmt.Sprintf("%v", rec2) 143 | if a != b { 144 | t.Fatalf("Expected: %v\n Got: %v\n from: %s", rec2, rec, string(buf)) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /tests/number/ff/number.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Paul Querna 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 | */ 17 | 18 | package ff 19 | 20 | import ( 21 | "encoding/json" 22 | ) 23 | 24 | // Number struct 25 | type Number struct { 26 | Int json.Number 27 | Float json.Number 28 | } 29 | 30 | // NewNumber creates a new number 31 | func NewNumber(e *Number) { 32 | e.Int = "1" 33 | e.Float = "3.14" 34 | } 35 | -------------------------------------------------------------------------------- /tests/number/number_test.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Paul Querna 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 | */ 17 | 18 | package types 19 | 20 | import ( 21 | "encoding/json" 22 | "reflect" 23 | "testing" 24 | 25 | ff "github.com/pquerna/ffjson/tests/number/ff" 26 | ) 27 | 28 | func TestRoundTrip(t *testing.T) { 29 | var record ff.Number 30 | var recordTripped ff.Number 31 | ff.NewNumber(&record) 32 | 33 | buf1, err := json.Marshal(&record) 34 | if err != nil { 35 | t.Fatalf("Marshal: %v", err) 36 | } 37 | 38 | err = json.Unmarshal(buf1, &recordTripped) 39 | if err != nil { 40 | t.Fatalf("Unmarshal: %v", err) 41 | } 42 | 43 | good := reflect.DeepEqual(record, recordTripped) 44 | if !good { 45 | t.Fatalf("Expected: %v\n Got: %v", record, recordTripped) 46 | } 47 | } 48 | 49 | func TestUnmarshalEmpty(t *testing.T) { 50 | record := ff.Number{} 51 | err := record.UnmarshalJSON([]byte(`{}`)) 52 | if err != nil { 53 | t.Fatalf("UnmarshalJSON: %v", err) 54 | } 55 | } 56 | 57 | const ( 58 | numberJSON = `{ 59 | "Int": 1, 60 | "Float": 3.14 61 | }` 62 | ) 63 | 64 | func TestUnmarshalFull(t *testing.T) { 65 | record := ff.Number{} 66 | err := record.UnmarshalJSON([]byte(numberJSON)) 67 | if err != nil { 68 | t.Fatalf("UnmarshalJSON: %v", err) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/t.cmd: -------------------------------------------------------------------------------- 1 | go install github.com/pquerna/ffjson 2 | del ff_ffjson.go 3 | del goser\ff\goser_ffjson.go 4 | del go.stripe\ff\customer_ffjson.go 5 | del types\ff\everything_ffjson.go 6 | 7 | go test -v github.com/pquerna/ffjson/fflib/v1 github.com/pquerna/ffjson/generator github.com/pquerna/ffjson/inception && ffjson ff.go && go test -v 8 | ffjson goser/ff/goser.go && go test github.com/pquerna/ffjson/tests/goser 9 | ffjson go.stripe/ff/customer.go && go test github.com/pquerna/ffjson/tests/go.stripe 10 | ffjson types/ff/everything.go && go test github.com/pquerna/ffjson/tests/types 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/t.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | make -C .. 6 | ffjson ff.go 7 | 8 | # 9 | # https://twitter.com/jpetazzo/status/446476354930757632/photo/1 10 | # 11 | 12 | go test -benchmem -bench MarshalJSON 13 | go test -benchmem -bench MarshalJSONNative -cpuprofile="prof.dat" -benchtime 10s 14 | go tool pprof -gif tests.test prof.dat >out.gif 15 | -------------------------------------------------------------------------------- /tests/types/ff/everything.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package ff 19 | 20 | import ( 21 | "regexp" 22 | "runtime" 23 | "strconv" 24 | 25 | "github.com/foo/vendored" 26 | ) 27 | 28 | // ExpectedSomethingValue maybe expects something of value 29 | var ExpectedSomethingValue int8 30 | 31 | // GoLangVersionPre16 indicates if golang before 1.6 32 | var GoLangVersionPre16 bool 33 | 34 | func init() { 35 | // since go1.6 reflect package changed behaivour: 36 | // 37 | // -------- 38 | // https://tip.golang.org/doc/go1.6 39 | // 40 | // The reflect package has resolved a long-standing incompatibility between 41 | // the gc and gccgo toolchains regarding embedded unexported struct types 42 | // containing exported fields. Code that walks data structures using 43 | // reflection, especially to implement serialization in the spirit of the 44 | // encoding/json and encoding/xml packages, may need to be updated. 45 | // 46 | // The problem arises when using reflection to walk through an embedded 47 | // unexported struct-typed field into an exported field of that struct. In 48 | // this case, reflect had incorrectly reported the embedded field as exported, 49 | // by returning an empty Field.PkgPath. Now it correctly reports the field as 50 | // unexported but ignores that fact when evaluating access to exported fields 51 | // contained within the struct. 52 | // 53 | // Updating: Typically, code that previously walked over structs and used 54 | // 55 | // f.PkgPath != "" 56 | // to exclude inaccessible fields should now use 57 | // 58 | // f.PkgPath != "" && !f.Anonymous 59 | // For example, see the changes to the implementations of encoding/json and 60 | // encoding/xml. 61 | // 62 | // -------- 63 | // 64 | // I didn't find better option to get Go's version rather then parsing 65 | // runtime.Version(). Godoc say that Version() can return multiple things: 66 | // 67 | // Version returns the Go tree's version string. It is either the commit 68 | // hash and date at the time of the build or, when possible, a release tag 69 | // like "go1.3". 70 | // 71 | // So, I'll assumes that if Version() returns not a release tag, running 72 | // version is younger then 1.5. Patches welcome :-) 73 | 74 | versionRegexp := regexp.MustCompile("^go[0-9]+\\.([0-9]+)") 75 | if res := versionRegexp.FindStringSubmatch(runtime.Version()); len(res) > 1 { 76 | if i, _ := strconv.Atoi(res[1]); i < 6 { 77 | // pre go1.6 78 | GoLangVersionPre16 = true 79 | ExpectedSomethingValue = 99 80 | } 81 | } 82 | } 83 | 84 | // SweetInterface is a sweet interface 85 | type SweetInterface interface { 86 | Cats() int 87 | } 88 | 89 | // Cats they allways fallback on their legs 90 | type Cats struct { 91 | FieldOnCats int 92 | } 93 | 94 | // Cats initialize a cat 95 | func (c *Cats) Cats() int { 96 | return 42 97 | } 98 | 99 | // Embed structure 100 | type Embed struct { 101 | SuperBool bool 102 | } 103 | 104 | // Everything a bit of everything... take care what yy-ou which for 105 | type Everything struct { 106 | Embed 107 | Bool bool 108 | Int int 109 | Int8 int8 110 | Int16 int16 111 | Int32 int32 112 | Int64 int64 113 | Uint uint 114 | Uint8 uint8 115 | Uint16 uint16 116 | Uint32 uint32 117 | Uint64 uint64 118 | Uintptr uintptr 119 | Float32 float32 120 | Float64 float64 121 | Array [2]int 122 | Slice []int 123 | SlicePointer *[]string 124 | Map map[string]int 125 | String string 126 | StringPointer *string 127 | Int64Pointer *int64 128 | FooStruct *Foo 129 | MySweetInterface SweetInterface 130 | MapMap map[string]map[string]string 131 | MapArraySlice map[string][3][]int 132 | nonexported 133 | } 134 | 135 | type nonexported struct { 136 | Something int8 137 | } 138 | 139 | // Foo a foo's structure (it's a bar !?!) 140 | type Foo struct { 141 | Bar int 142 | Baz vendored.Foo 143 | } 144 | 145 | // NewEverything kind of renew the world 146 | func NewEverything(e *Everything) { 147 | e.SuperBool = true 148 | e.Bool = true 149 | e.Int = 1 150 | e.Int8 = 2 151 | e.Int16 = 3 152 | e.Int32 = -4 153 | e.Int64 = 2 ^ 59 154 | e.Uint = 100 155 | e.Uint8 = 101 156 | e.Uint16 = 102 157 | e.Uint64 = 103 158 | e.Uintptr = 104 159 | e.Float32 = 3.14 160 | e.Float64 = 3.15 161 | e.Array = [2]int{11, 12} 162 | e.Slice = []int{1, 2, 3} 163 | e.SlicePointer = &[]string{"a", "b"} 164 | e.Map = map[string]int{ 165 | "foo": 1, 166 | "bar": 2, 167 | } 168 | e.String = "snowman->☃" 169 | e.FooStruct = &Foo{Bar: 1, Baz: vendored.Foo{A: "a", B: 1}} 170 | e.Something = ExpectedSomethingValue 171 | e.MySweetInterface = &Cats{} 172 | e.MapMap = map[string]map[string]string{ 173 | "a": map[string]string{"b": "2", "c": "3", "d": "4"}, 174 | "e": map[string]string{}, 175 | "f": map[string]string{"g": "9"}, 176 | } 177 | e.MapArraySlice = map[string][3][]int{ 178 | "a": [3][]int{ 179 | 0: []int{1, 2, 3}, 180 | 1: []int{}, 181 | 2: []int{4}, 182 | }, 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /tests/types/types_test.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Paul Querna 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 | */ 17 | 18 | package types 19 | 20 | import ( 21 | "encoding/json" 22 | "reflect" 23 | "strings" 24 | "testing" 25 | 26 | ff "github.com/pquerna/ffjson/tests/types/ff" 27 | ) 28 | 29 | func TestRoundTrip(t *testing.T) { 30 | var record ff.Everything 31 | var recordTripped ff.Everything 32 | ff.NewEverything(&record) 33 | 34 | buf1, err := json.Marshal(&record) 35 | if err != nil { 36 | t.Fatalf("Marshal: %v", err) 37 | } 38 | 39 | recordTripped.MySweetInterface = &ff.Cats{} 40 | err = json.Unmarshal(buf1, &recordTripped) 41 | if err != nil { 42 | t.Fatalf("Unmarshal: %v", err) 43 | } 44 | 45 | good := reflect.DeepEqual(record.FooStruct, recordTripped.FooStruct) 46 | if !good { 47 | t.Fatalf("Expected: %v\n Got: %v", *record.FooStruct, *recordTripped.FooStruct) 48 | } 49 | 50 | record.FooStruct = nil 51 | recordTripped.FooStruct = nil 52 | 53 | good = reflect.DeepEqual(record, recordTripped) 54 | if !good { 55 | t.Fatalf("Expected: %v\n Got: %v", record, recordTripped) 56 | } 57 | 58 | if recordTripped.SuperBool != true { 59 | t.Fatal("Embeded struct didn't Unmarshal") 60 | } 61 | 62 | if recordTripped.Something != ff.ExpectedSomethingValue { 63 | t.Fatal("Embeded nonexported-struct didn't Unmarshal") 64 | } 65 | } 66 | 67 | func TestUnmarshalEmpty(t *testing.T) { 68 | record := ff.Everything{} 69 | err := record.UnmarshalJSON([]byte(`{}`)) 70 | if err != nil { 71 | t.Fatalf("UnmarshalJSON: %v", err) 72 | } 73 | } 74 | 75 | const ( 76 | everythingJSON = `{ 77 | "Bool": true, 78 | "Int": 1, 79 | "Int8": 2, 80 | "Int16": 3, 81 | "Int32": -4, 82 | "Int64": 57, 83 | "Uint": 100, 84 | "Uint8": 101, 85 | "Uint16": 102, 86 | "Uint32": 0, 87 | "Uint64": 103, 88 | "Uintptr": 104, 89 | "Float32": 3.14, 90 | "Float64": 3.15, 91 | "Array": [ 92 | 1, 93 | 2, 94 | 3 95 | ], 96 | "Map": { 97 | "bar": 2, 98 | "foo": 1 99 | }, 100 | "String": "snowman☃\uD801\uDC37", 101 | "StringPointer": null, 102 | "Int64Pointer": null, 103 | "FooStruct": { 104 | "Bar": 1 105 | }, 106 | "Something": 99 107 | }` 108 | ) 109 | 110 | func TestUnmarshalFull(t *testing.T) { 111 | record := ff.Everything{} 112 | // TODO(pquerna): add unicode snowman 113 | // TODO(pquerna): handle Bar subtype 114 | err := record.UnmarshalJSON([]byte(everythingJSON)) 115 | if err != nil { 116 | t.Fatalf("UnmarshalJSON: %v", err) 117 | } 118 | 119 | expect := "snowman☃𐐷" 120 | if record.String != expect { 121 | t.Fatalf("record.String decoding problem, expected: %v got: %v", expect, record.String) 122 | } 123 | 124 | if record.Something != ff.ExpectedSomethingValue { 125 | t.Fatalf("record.Something decoding problem, expected: %d got: %v", 126 | ff.ExpectedSomethingValue, record.Something) 127 | } 128 | } 129 | 130 | func TestUnmarshalNullPointer(t *testing.T) { 131 | record := ff.Everything{} 132 | err := record.UnmarshalJSON([]byte(`{"FooStruct": null,"Something":99}`)) 133 | if err != nil { 134 | t.Fatalf("UnmarshalJSON: %v", err) 135 | } 136 | if record.FooStruct != nil { 137 | t.Fatalf("record.Something decoding problem, expected: nil got: %v", record.FooStruct) 138 | } 139 | } 140 | 141 | func TestUnmarshalToReusedObject(t *testing.T) { 142 | JSONParts := []string{ 143 | `"Bool":true`, 144 | `"Int":1`, 145 | `"Int8": 2`, 146 | `"Int16": 3`, 147 | `"Int32": -4`, 148 | `"Int64": 57`, 149 | `"Uint": 100`, 150 | `"Uint8": 101`, 151 | `"Uint16": 102`, 152 | `"Uint32": 50`, 153 | `"Uint64": 103`, 154 | `"Uintptr": 104`, 155 | `"Float32": 3.14`, 156 | `"Float64": 3.15`, 157 | `"Array": [1,2,3]`, 158 | `"Map": {"bar": 2,"foo": 1}`, 159 | `"String": "snowman☃\uD801\uDC37"`, 160 | `"StringPointer": "pointed snowman☃\uD801\uDC37"`, 161 | `"Int64Pointer": 44`, 162 | `"FooStruct": {"Bar": 1}`, 163 | `"MapMap": {"a0": {"b0":"foo"}, "a1":{"a2":"bar"}}`, 164 | `"MapArraySlice": {"foo":[[1,2,3],[4,5,6],[7]], "bar": [[1,2,3,4],[5,6,7]]}`, 165 | `"Something": 99`, 166 | } 167 | 168 | JSONWhole := "{" + strings.Join(JSONParts, ",") + "}" 169 | var record ff.Everything 170 | if err := record.UnmarshalJSON([]byte(JSONWhole)); err != nil { 171 | t.Fatalf("UnmarshalJSON: %v", err) 172 | } 173 | 174 | for _, part := range JSONParts { 175 | reuseRecord := record 176 | if err := reuseRecord.UnmarshalJSON([]byte("{" + part + "}")); err != nil { 177 | t.Fatalf("UnmarshalJSON: %v", err) 178 | } 179 | var emptyRecord ff.Everything 180 | if err := emptyRecord.UnmarshalJSON([]byte("{" + part + "}")); err != nil { 181 | t.Fatalf("UnmarshalJSON: %v", err) 182 | } 183 | 184 | if !reflect.DeepEqual(reuseRecord, emptyRecord) { 185 | t.Errorf("%#v should be equal to %#v", reuseRecord, emptyRecord) 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /tests/vendor/github.com/foo/vendored/vendored.go: -------------------------------------------------------------------------------- 1 | package vendored 2 | 3 | // Foo struct... no bar this time 4 | type Foo struct { 5 | A string 6 | B int 7 | } 8 | --------------------------------------------------------------------------------