├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── banner.png ├── binpack └── binpack.go ├── cache ├── cache.go └── cache_test.go ├── cli ├── .goreleaser.yml ├── cmd │ ├── compose.go │ ├── list.go │ ├── pull.go │ ├── render.go │ └── root.go └── main.go ├── composer └── composer.go ├── data ├── data.go └── data_test.go ├── examples ├── aws_tileset.yml ├── highlight.css ├── links_tileset.yml ├── tilemap_demo_1.png └── tilemap_demo_1.yml ├── go.mod ├── grid ├── grid.go └── grid_test.go ├── imagelist └── imagelist.go ├── tilemap ├── tilemap.go └── tilemap_test.go └── tileset └── tileset.go /.gitignore: -------------------------------------------------------------------------------- 1 | ## Intellij 2 | .idea/**/workspace.xml 3 | .idea/**/tasks.xml 4 | .idea/**/encodings.xml 5 | .idea/**/compiler.xml 6 | .idea/**/misc.xml 7 | .idea/**/modules.xml 8 | .idea/**/vcs.xml 9 | 10 | ## VSCode 11 | .vscode/ 12 | 13 | ## File-based project format: 14 | *.iws 15 | *.iml 16 | .idea/ 17 | 18 | # Binaries for programs and plugins 19 | *.exe 20 | *.exe~ 21 | *.dll 22 | *.so 23 | *.dylib 24 | *.dat 25 | *.DS_Store 26 | go.sum 27 | 28 | # Test binary, built with `go test -c` 29 | *.test 30 | 31 | # Output of the go coverage tool, specifically when used with LiteIDE 32 | *.out 33 | 34 | # Goreleaser builds 35 | **/dist/** 36 | 37 | icons/** -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [0.1.0] - 2020-08-28 8 | - 🎉 First release! 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Luca Sepe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # **Tiles** 3 | 4 | > Commandline tool that makes building tilesets and rendering static tilemaps super easy! 5 | 6 | **Features** 7 | 8 | - create your own tilesets _"libraries"_ (ready to reuse) 9 | - inspect, list and extract tiles from tilesets 10 | - define a tilemap using one or more tileset 11 | - render a tilemap as PNG images 12 | - eventually add a watermark to the tilemap 13 | 14 | ![](./banner.png) 15 | 16 | ### Overview 17 | 18 | Tilemaps are a very popular technique in 2D game development, consisting of building the game world or level map out of small, regular-shaped images called tiles. 19 | 20 | The most efficient way to store the tile images is in an atlas or tileset 21 | 22 | - all of the required tiles grouped together in a single image file 23 | 24 | When it's time to draw a tile, only a small section of this bigger image is rendered on the grid. 25 | 26 | #### Static square tilemaps 27 | 28 | Square-based tilemaps are the most simple implementation for two perspectives: 29 | 30 | - top-down (like many RPG's or strategy games) 31 | - side-view (like platformers such as Super Mario Bros) 32 | - architecture diagrams...why not!? 😏 33 | 34 | # How to use **tiles** 35 | 36 | ## All available commands 37 | 38 | ```bash 39 | tiles --help 40 | ``` 41 | 42 | ## Generate a tileset 43 | 44 | Let's say you have all your PNG images (square in size, 96x96 for example) in one folder and you want to create a new tileset: 45 | 46 | ```bash 47 | tiles compose /path/to/png/images/ 48 | ``` 49 | 50 | By default the generated tileset (it's a YAML) is printed on the terminal. If you want to save the result to a file you can redirect `>` the output: 51 | 52 | ```bash 53 | tiles compose /path/to/png/images/ > my_tileset.yml 54 | ``` 55 | 56 | ### Ready-To-Use tilesets 57 | 58 | | Set | URL | 59 | |:-----------------------|:---------------------------------------------------------| 60 | | AWS Icons | [./examples/aws_tileset.yml](./examples/aws_tileset.yml) | 61 | | Arrows and Connectors | [./examples/links_tileset.yml](./examples/links_tileset.yml) | 62 | 63 | 64 | ## Lists all tiles identifiers contained in the specified tilset 65 | 66 | ```bash 67 | tiles list /path/to/my_tileset.yml 68 | ``` 69 | 70 | ## Extracts the tile PNG with the specified identifier from the tileset 71 | 72 | ```bash 73 | tiles pull --id aws_waf ../examples/aws_tileset.yml 74 | ``` 75 | 76 | By default the PNG data is dumped on the terminal. If you want to save the result to a file you can redirect `>` the output: 77 | 78 | ```bash 79 | tiles pull --id aws_waf ../examples/aws_tileset.yml > aws_waf.png 80 | ``` 81 | 82 | ## Rendering a static tilemap 83 | 84 | The first step is to create the static tilemap using the following YAML syntax: 85 | 86 | ```yml 87 | # Nr. of Columns 88 | cols: 4 89 | # Nr. of Rows 90 | rows: 7 91 | # Tile size (Grid cell size) 92 | tile_size: 64 93 | # Canvas margin (optional) 94 | margin: 16 95 | # Canvas watermark (optional) 96 | watermark: Draft 97 | # Canvas background color (optional) 98 | bg_color: "#ffffff" 99 | # List of used tileset 100 | atlas_list: 101 | - ../examples/aws_tileset.yml 102 | - ../examples/links_tileset.yml 103 | # Tiles mapping (associate an index to each tile) 104 | mapping: 105 | 1: aws_lambda 106 | 2: aws_elastic_container_service 107 | 3: aws_api_gateway 108 | 4: aws_rds_mysql_instance 109 | 5: aws_simple_storage_service_s3 110 | 6: aws_elasticache_for_redis 111 | 10: link_vertical 112 | 11: link_vertical_arrow_up 113 | 20: link_cross_arrow_left_up_down 114 | 30: link_horizontal 115 | 40: link_tee_right_arrow_up_down 116 | # Static map layout 117 | layout: > 118 | 0,5,0,0 119 | 4,20,30,2 120 | 0,6,0,11 121 | 0,0,0,10 122 | 0,1,0,10 123 | 0,40,30,3 124 | 0,1,0,0,0 125 | ``` 126 | 127 | 👉 [examples/tilemap_demo_1.yml](./examples/tilemap_demo_1.yml). 128 | 129 | Then execute the _'render'_ command: 130 | 131 | ```sh 132 | tiles render ./examples/tilemap_demo_1.yml > ./examples/tilemap_demo_1.png 133 | ``` 134 | 135 | output: 136 | 137 | ![](./examples/tilemap_demo_1.png) 138 | 139 | # Installation Steps 140 | 141 | To build the binaries by yourself, assuming that you have Go installed, you need [GoReleaser](https://goreleaser.com/intro/). 142 | 143 | Here the steps: 144 | 145 | ### Grab the source code 146 | 147 | ```bash 148 | git clone https://github.com/lucasepe/tiles.git 149 | ``` 150 | 151 | ### Change dir to the tool folder 152 | 153 | ```bash 154 | cd tiles/cli 155 | ``` 156 | 157 | ### Run GoReleaser 158 | 159 | ```bash 160 | goreleaser --rm-dist --snapshot --skip-publish 161 | ``` 162 | 163 | you will found the binaries for: 164 | 165 | - MacOS into the folder _dist/tiles_darwin_amd64/_ 166 | - Linux into the folder _dist/tiles_linux_amd64/_ 167 | - Windows into the folder _dist/tiles_windows_amd64/_ 168 | 169 | ## Ready-To-Use Releases 170 | 171 | If you don't want to compile the sourcecode yourself, [Here you can find the tool already compiled](https://github.com/lucasepe/tiles/releases/latest) for: 172 | 173 | - MacOS 174 | - Linux 175 | - Windows 176 | 177 | --- 178 | 179 | # CHANGE LOG 180 | 181 | 👉 [Record of all notable changes made to a project](./CHANGELOG.md) 182 | -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/tiles/f758e9d7a86cbce74d27dd0a3f4fb18488394767/banner.png -------------------------------------------------------------------------------- /binpack/binpack.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Azul3D 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 binpack implements Jake Gordon's 2D binpacking algorithm. 6 | // 7 | // The algorithm used is described on Jake's blog here: 8 | // 9 | // http://codeincomplete.com/posts/2011/5/7/bin_packing/ 10 | // 11 | // And is also implemented by him in JavaScript here: 12 | // 13 | // https://github.com/jakesgordon/bin-packing 14 | // 15 | package binpack 16 | 17 | type Packable interface { 18 | // Len should return the number of blocks in total. 19 | Len() int 20 | 21 | // Size should return the width and height of the block n 22 | Size(n int) (width, height int) 23 | 24 | // Place should place the block n, at the position [x, y]. 25 | Place(n, x, y int) 26 | } 27 | 28 | // Pack uses the packable interface, p, to pack two dimensional blocks onto a 29 | // larger two dimensional grid. 30 | // 31 | // The algorithm does not start with an fixed width and height, instead it 32 | // starts with the width and height of the first block and then grows as 33 | // neccessary to fit each block into the overall grid. As the grid is grown the 34 | // algorithm attempts to maintain an roughly square ratio by making 'smart' 35 | // choices about whether to grow right or down. 36 | // 37 | // The returned width and height reflect how large the overall grid must be to 38 | // contain at least each packed block. 39 | // 40 | // When growing, the algorithm can only grow to the right or down. If the new 41 | // block is both wider and taller than the first [p.Size(0)] block, then the 42 | // algorithm will be unable to pack the blocks, and [-1, -1] will be returned. 43 | // 44 | // To avoid the above problem, sort blocks by max(width, height). 45 | // 46 | // If the number of blocks is zero (p.Len() == 0) then this function is no-op 47 | // and [0, 0] is returned. 48 | func Pack(p Packable) (width, height int) { 49 | numBlocks := p.Len() 50 | 51 | if numBlocks == 0 { 52 | return 0, 0 53 | } 54 | 55 | w, h := p.Size(0) 56 | root := &node{ 57 | x: 0, 58 | y: 0, 59 | width: w, 60 | height: h, 61 | } 62 | 63 | p.Place(0, 0, 0) 64 | 65 | for i := 0; i < numBlocks; i++ { 66 | w, h = p.Size(i) 67 | 68 | node := root.find(w, h) 69 | if node != nil { 70 | node = node.split(w, h) 71 | 72 | // Update block in-place 73 | p.Place(i, node.x, node.y) 74 | 75 | } else { 76 | newRoot, grown := root.grow(w, h) 77 | if newRoot == nil { 78 | return -1, -1 79 | } 80 | 81 | // Update block in-place 82 | p.Place(i, grown.x, grown.y) 83 | 84 | root = newRoot 85 | } 86 | } 87 | 88 | return root.width, root.height 89 | } 90 | 91 | type node struct { 92 | x, y, width, height int 93 | right, down *node 94 | } 95 | 96 | func (n *node) find(width, height int) *node { 97 | if n.right != nil || n.down != nil { 98 | right := n.right.find(width, height) 99 | if right != nil { 100 | return right 101 | } 102 | return n.down.find(width, height) 103 | } else if width <= n.width && height <= n.height { 104 | return n 105 | } 106 | return nil 107 | } 108 | 109 | func (n *node) split(width, height int) *node { 110 | n.down = &node{ 111 | x: n.x, 112 | y: n.y + height, 113 | width: n.width, 114 | height: n.height - height, 115 | } 116 | 117 | n.right = &node{ 118 | x: n.x + width, 119 | y: n.y, 120 | width: n.width - width, 121 | height: height, 122 | } 123 | 124 | return n 125 | } 126 | 127 | func (n *node) grow(width, height int) (root, grown *node) { 128 | canGrowDown := width <= n.width 129 | canGrowRight := height <= n.height 130 | 131 | // attempt to keep square-ish by growing right when height is much greater than width 132 | shouldGrowRight := canGrowRight && (n.height >= (n.width + width)) 133 | 134 | // attempt to keep square-ish by growing down when width is much greater than height 135 | shouldGrowDown := canGrowDown && (n.width >= (n.height + height)) 136 | 137 | if shouldGrowRight { 138 | return n.growRight(width, height) 139 | } else if shouldGrowDown { 140 | return n.growDown(width, height) 141 | } else if canGrowRight { 142 | return n.growRight(width, height) 143 | } else if canGrowDown { 144 | return n.growDown(width, height) 145 | } 146 | 147 | // need to ensure sensible root starting size to avoid this happening 148 | return nil, nil 149 | } 150 | 151 | func (n *node) growRight(width, height int) (root, grown *node) { 152 | newRoot := &node{ 153 | x: 0, 154 | y: 0, 155 | width: n.width + width, 156 | height: n.height, 157 | down: n, 158 | right: &node{ 159 | x: n.width, 160 | y: 0, 161 | width: width, 162 | height: n.height, 163 | }, 164 | } 165 | 166 | node := newRoot.find(width, height) 167 | if node != nil { 168 | return newRoot, node.split(width, height) 169 | } 170 | return nil, nil 171 | } 172 | 173 | func (n *node) growDown(width, height int) (root, grown *node) { 174 | newRoot := &node{ 175 | x: 0, 176 | y: 0, 177 | width: n.width, 178 | height: n.height + height, 179 | down: &node{ 180 | x: 0, 181 | y: n.height, 182 | width: n.width, 183 | height: height, 184 | }, 185 | right: n, 186 | } 187 | 188 | node := newRoot.find(width, height) 189 | if node != nil { 190 | return newRoot, node.split(width, height) 191 | } 192 | return nil, nil 193 | } 194 | -------------------------------------------------------------------------------- /cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "encoding/gob" 5 | "fmt" 6 | "io" 7 | "os" 8 | "runtime" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | type Item struct { 14 | Object interface{} 15 | Expiration int64 16 | } 17 | 18 | // Returns true if the item has expired. 19 | func (item Item) Expired() bool { 20 | if item.Expiration == 0 { 21 | return false 22 | } 23 | return time.Now().UnixNano() > item.Expiration 24 | } 25 | 26 | const ( 27 | // For use with functions that take an expiration time. 28 | NoExpiration time.Duration = -1 29 | // For use with functions that take an expiration time. Equivalent to 30 | // passing in the same expiration duration as was given to New() or 31 | // NewFrom() when the cache was created (e.g. 5 minutes.) 32 | DefaultExpiration time.Duration = 0 33 | ) 34 | 35 | type Cache struct { 36 | *cache 37 | // If this is confusing, see the comment at the bottom of New() 38 | } 39 | 40 | type cache struct { 41 | defaultExpiration time.Duration 42 | items map[string]Item 43 | mu sync.RWMutex 44 | onEvicted func(string, interface{}) 45 | janitor *janitor 46 | } 47 | 48 | // Add an item to the cache, replacing any existing item. If the duration is 0 49 | // (DefaultExpiration), the cache's default expiration time is used. If it is -1 50 | // (NoExpiration), the item never expires. 51 | func (c *cache) Set(k string, x interface{}, d time.Duration) { 52 | // "Inlining" of set 53 | var e int64 54 | if d == DefaultExpiration { 55 | d = c.defaultExpiration 56 | } 57 | if d > 0 { 58 | e = time.Now().Add(d).UnixNano() 59 | } 60 | c.mu.Lock() 61 | c.items[k] = Item{ 62 | Object: x, 63 | Expiration: e, 64 | } 65 | // TODO: Calls to mu.Unlock are currently not deferred because defer 66 | // adds ~200 ns (as of go1.) 67 | c.mu.Unlock() 68 | } 69 | 70 | func (c *cache) set(k string, x interface{}, d time.Duration) { 71 | var e int64 72 | if d == DefaultExpiration { 73 | d = c.defaultExpiration 74 | } 75 | if d > 0 { 76 | e = time.Now().Add(d).UnixNano() 77 | } 78 | c.items[k] = Item{ 79 | Object: x, 80 | Expiration: e, 81 | } 82 | } 83 | 84 | // Add an item to the cache, replacing any existing item, using the default 85 | // expiration. 86 | func (c *cache) SetDefault(k string, x interface{}) { 87 | c.Set(k, x, DefaultExpiration) 88 | } 89 | 90 | // Add an item to the cache only if an item doesn't already exist for the given 91 | // key, or if the existing item has expired. Returns an error otherwise. 92 | func (c *cache) Add(k string, x interface{}, d time.Duration) error { 93 | c.mu.Lock() 94 | _, found := c.get(k) 95 | if found { 96 | c.mu.Unlock() 97 | return fmt.Errorf("Item %s already exists", k) 98 | } 99 | c.set(k, x, d) 100 | c.mu.Unlock() 101 | return nil 102 | } 103 | 104 | // Set a new value for the cache key only if it already exists, and the existing 105 | // item hasn't expired. Returns an error otherwise. 106 | func (c *cache) Replace(k string, x interface{}, d time.Duration) error { 107 | c.mu.Lock() 108 | _, found := c.get(k) 109 | if !found { 110 | c.mu.Unlock() 111 | return fmt.Errorf("Item %s doesn't exist", k) 112 | } 113 | c.set(k, x, d) 114 | c.mu.Unlock() 115 | return nil 116 | } 117 | 118 | // Get an item from the cache. Returns the item or nil, and a bool indicating 119 | // whether the key was found. 120 | func (c *cache) Get(k string) (interface{}, bool) { 121 | c.mu.RLock() 122 | // "Inlining" of get and Expired 123 | item, found := c.items[k] 124 | if !found { 125 | c.mu.RUnlock() 126 | return nil, false 127 | } 128 | if item.Expiration > 0 { 129 | if time.Now().UnixNano() > item.Expiration { 130 | c.mu.RUnlock() 131 | return nil, false 132 | } 133 | } 134 | c.mu.RUnlock() 135 | return item.Object, true 136 | } 137 | 138 | // GetWithExpiration returns an item and its expiration time from the cache. 139 | // It returns the item or nil, the expiration time if one is set (if the item 140 | // never expires a zero value for time.Time is returned), and a bool indicating 141 | // whether the key was found. 142 | func (c *cache) GetWithExpiration(k string) (interface{}, time.Time, bool) { 143 | c.mu.RLock() 144 | // "Inlining" of get and Expired 145 | item, found := c.items[k] 146 | if !found { 147 | c.mu.RUnlock() 148 | return nil, time.Time{}, false 149 | } 150 | 151 | if item.Expiration > 0 { 152 | if time.Now().UnixNano() > item.Expiration { 153 | c.mu.RUnlock() 154 | return nil, time.Time{}, false 155 | } 156 | 157 | // Return the item and the expiration time 158 | c.mu.RUnlock() 159 | return item.Object, time.Unix(0, item.Expiration), true 160 | } 161 | 162 | // If expiration <= 0 (i.e. no expiration time set) then return the item 163 | // and a zeroed time.Time 164 | c.mu.RUnlock() 165 | return item.Object, time.Time{}, true 166 | } 167 | 168 | func (c *cache) get(k string) (interface{}, bool) { 169 | item, found := c.items[k] 170 | if !found { 171 | return nil, false 172 | } 173 | // "Inlining" of Expired 174 | if item.Expiration > 0 { 175 | if time.Now().UnixNano() > item.Expiration { 176 | return nil, false 177 | } 178 | } 179 | return item.Object, true 180 | } 181 | 182 | // Increment an item of type int, int8, int16, int32, int64, uintptr, uint, 183 | // uint8, uint32, or uint64, float32 or float64 by n. Returns an error if the 184 | // item's value is not an integer, if it was not found, or if it is not 185 | // possible to increment it by n. To retrieve the incremented value, use one 186 | // of the specialized methods, e.g. IncrementInt64. 187 | func (c *cache) Increment(k string, n int64) error { 188 | c.mu.Lock() 189 | v, found := c.items[k] 190 | if !found || v.Expired() { 191 | c.mu.Unlock() 192 | return fmt.Errorf("Item %s not found", k) 193 | } 194 | switch v.Object.(type) { 195 | case int: 196 | v.Object = v.Object.(int) + int(n) 197 | case int8: 198 | v.Object = v.Object.(int8) + int8(n) 199 | case int16: 200 | v.Object = v.Object.(int16) + int16(n) 201 | case int32: 202 | v.Object = v.Object.(int32) + int32(n) 203 | case int64: 204 | v.Object = v.Object.(int64) + n 205 | case uint: 206 | v.Object = v.Object.(uint) + uint(n) 207 | case uintptr: 208 | v.Object = v.Object.(uintptr) + uintptr(n) 209 | case uint8: 210 | v.Object = v.Object.(uint8) + uint8(n) 211 | case uint16: 212 | v.Object = v.Object.(uint16) + uint16(n) 213 | case uint32: 214 | v.Object = v.Object.(uint32) + uint32(n) 215 | case uint64: 216 | v.Object = v.Object.(uint64) + uint64(n) 217 | case float32: 218 | v.Object = v.Object.(float32) + float32(n) 219 | case float64: 220 | v.Object = v.Object.(float64) + float64(n) 221 | default: 222 | c.mu.Unlock() 223 | return fmt.Errorf("The value for %s is not an integer", k) 224 | } 225 | c.items[k] = v 226 | c.mu.Unlock() 227 | return nil 228 | } 229 | 230 | // Increment an item of type float32 or float64 by n. Returns an error if the 231 | // item's value is not floating point, if it was not found, or if it is not 232 | // possible to increment it by n. Pass a negative number to decrement the 233 | // value. To retrieve the incremented value, use one of the specialized methods, 234 | // e.g. IncrementFloat64. 235 | func (c *cache) IncrementFloat(k string, n float64) error { 236 | c.mu.Lock() 237 | v, found := c.items[k] 238 | if !found || v.Expired() { 239 | c.mu.Unlock() 240 | return fmt.Errorf("Item %s not found", k) 241 | } 242 | switch v.Object.(type) { 243 | case float32: 244 | v.Object = v.Object.(float32) + float32(n) 245 | case float64: 246 | v.Object = v.Object.(float64) + n 247 | default: 248 | c.mu.Unlock() 249 | return fmt.Errorf("The value for %s does not have type float32 or float64", k) 250 | } 251 | c.items[k] = v 252 | c.mu.Unlock() 253 | return nil 254 | } 255 | 256 | // Increment an item of type int by n. Returns an error if the item's value is 257 | // not an int, or if it was not found. If there is no error, the incremented 258 | // value is returned. 259 | func (c *cache) IncrementInt(k string, n int) (int, error) { 260 | c.mu.Lock() 261 | v, found := c.items[k] 262 | if !found || v.Expired() { 263 | c.mu.Unlock() 264 | return 0, fmt.Errorf("Item %s not found", k) 265 | } 266 | rv, ok := v.Object.(int) 267 | if !ok { 268 | c.mu.Unlock() 269 | return 0, fmt.Errorf("The value for %s is not an int", k) 270 | } 271 | nv := rv + n 272 | v.Object = nv 273 | c.items[k] = v 274 | c.mu.Unlock() 275 | return nv, nil 276 | } 277 | 278 | // Increment an item of type int8 by n. Returns an error if the item's value is 279 | // not an int8, or if it was not found. If there is no error, the incremented 280 | // value is returned. 281 | func (c *cache) IncrementInt8(k string, n int8) (int8, error) { 282 | c.mu.Lock() 283 | v, found := c.items[k] 284 | if !found || v.Expired() { 285 | c.mu.Unlock() 286 | return 0, fmt.Errorf("Item %s not found", k) 287 | } 288 | rv, ok := v.Object.(int8) 289 | if !ok { 290 | c.mu.Unlock() 291 | return 0, fmt.Errorf("The value for %s is not an int8", k) 292 | } 293 | nv := rv + n 294 | v.Object = nv 295 | c.items[k] = v 296 | c.mu.Unlock() 297 | return nv, nil 298 | } 299 | 300 | // Increment an item of type int16 by n. Returns an error if the item's value is 301 | // not an int16, or if it was not found. If there is no error, the incremented 302 | // value is returned. 303 | func (c *cache) IncrementInt16(k string, n int16) (int16, error) { 304 | c.mu.Lock() 305 | v, found := c.items[k] 306 | if !found || v.Expired() { 307 | c.mu.Unlock() 308 | return 0, fmt.Errorf("Item %s not found", k) 309 | } 310 | rv, ok := v.Object.(int16) 311 | if !ok { 312 | c.mu.Unlock() 313 | return 0, fmt.Errorf("The value for %s is not an int16", k) 314 | } 315 | nv := rv + n 316 | v.Object = nv 317 | c.items[k] = v 318 | c.mu.Unlock() 319 | return nv, nil 320 | } 321 | 322 | // Increment an item of type int32 by n. Returns an error if the item's value is 323 | // not an int32, or if it was not found. If there is no error, the incremented 324 | // value is returned. 325 | func (c *cache) IncrementInt32(k string, n int32) (int32, error) { 326 | c.mu.Lock() 327 | v, found := c.items[k] 328 | if !found || v.Expired() { 329 | c.mu.Unlock() 330 | return 0, fmt.Errorf("Item %s not found", k) 331 | } 332 | rv, ok := v.Object.(int32) 333 | if !ok { 334 | c.mu.Unlock() 335 | return 0, fmt.Errorf("The value for %s is not an int32", k) 336 | } 337 | nv := rv + n 338 | v.Object = nv 339 | c.items[k] = v 340 | c.mu.Unlock() 341 | return nv, nil 342 | } 343 | 344 | // Increment an item of type int64 by n. Returns an error if the item's value is 345 | // not an int64, or if it was not found. If there is no error, the incremented 346 | // value is returned. 347 | func (c *cache) IncrementInt64(k string, n int64) (int64, error) { 348 | c.mu.Lock() 349 | v, found := c.items[k] 350 | if !found || v.Expired() { 351 | c.mu.Unlock() 352 | return 0, fmt.Errorf("Item %s not found", k) 353 | } 354 | rv, ok := v.Object.(int64) 355 | if !ok { 356 | c.mu.Unlock() 357 | return 0, fmt.Errorf("The value for %s is not an int64", k) 358 | } 359 | nv := rv + n 360 | v.Object = nv 361 | c.items[k] = v 362 | c.mu.Unlock() 363 | return nv, nil 364 | } 365 | 366 | // Increment an item of type uint by n. Returns an error if the item's value is 367 | // not an uint, or if it was not found. If there is no error, the incremented 368 | // value is returned. 369 | func (c *cache) IncrementUint(k string, n uint) (uint, error) { 370 | c.mu.Lock() 371 | v, found := c.items[k] 372 | if !found || v.Expired() { 373 | c.mu.Unlock() 374 | return 0, fmt.Errorf("Item %s not found", k) 375 | } 376 | rv, ok := v.Object.(uint) 377 | if !ok { 378 | c.mu.Unlock() 379 | return 0, fmt.Errorf("The value for %s is not an uint", k) 380 | } 381 | nv := rv + n 382 | v.Object = nv 383 | c.items[k] = v 384 | c.mu.Unlock() 385 | return nv, nil 386 | } 387 | 388 | // Increment an item of type uintptr by n. Returns an error if the item's value 389 | // is not an uintptr, or if it was not found. If there is no error, the 390 | // incremented value is returned. 391 | func (c *cache) IncrementUintptr(k string, n uintptr) (uintptr, error) { 392 | c.mu.Lock() 393 | v, found := c.items[k] 394 | if !found || v.Expired() { 395 | c.mu.Unlock() 396 | return 0, fmt.Errorf("Item %s not found", k) 397 | } 398 | rv, ok := v.Object.(uintptr) 399 | if !ok { 400 | c.mu.Unlock() 401 | return 0, fmt.Errorf("The value for %s is not an uintptr", k) 402 | } 403 | nv := rv + n 404 | v.Object = nv 405 | c.items[k] = v 406 | c.mu.Unlock() 407 | return nv, nil 408 | } 409 | 410 | // Increment an item of type uint8 by n. Returns an error if the item's value 411 | // is not an uint8, or if it was not found. If there is no error, the 412 | // incremented value is returned. 413 | func (c *cache) IncrementUint8(k string, n uint8) (uint8, error) { 414 | c.mu.Lock() 415 | v, found := c.items[k] 416 | if !found || v.Expired() { 417 | c.mu.Unlock() 418 | return 0, fmt.Errorf("Item %s not found", k) 419 | } 420 | rv, ok := v.Object.(uint8) 421 | if !ok { 422 | c.mu.Unlock() 423 | return 0, fmt.Errorf("The value for %s is not an uint8", k) 424 | } 425 | nv := rv + n 426 | v.Object = nv 427 | c.items[k] = v 428 | c.mu.Unlock() 429 | return nv, nil 430 | } 431 | 432 | // Increment an item of type uint16 by n. Returns an error if the item's value 433 | // is not an uint16, or if it was not found. If there is no error, the 434 | // incremented value is returned. 435 | func (c *cache) IncrementUint16(k string, n uint16) (uint16, error) { 436 | c.mu.Lock() 437 | v, found := c.items[k] 438 | if !found || v.Expired() { 439 | c.mu.Unlock() 440 | return 0, fmt.Errorf("Item %s not found", k) 441 | } 442 | rv, ok := v.Object.(uint16) 443 | if !ok { 444 | c.mu.Unlock() 445 | return 0, fmt.Errorf("The value for %s is not an uint16", k) 446 | } 447 | nv := rv + n 448 | v.Object = nv 449 | c.items[k] = v 450 | c.mu.Unlock() 451 | return nv, nil 452 | } 453 | 454 | // Increment an item of type uint32 by n. Returns an error if the item's value 455 | // is not an uint32, or if it was not found. If there is no error, the 456 | // incremented value is returned. 457 | func (c *cache) IncrementUint32(k string, n uint32) (uint32, error) { 458 | c.mu.Lock() 459 | v, found := c.items[k] 460 | if !found || v.Expired() { 461 | c.mu.Unlock() 462 | return 0, fmt.Errorf("Item %s not found", k) 463 | } 464 | rv, ok := v.Object.(uint32) 465 | if !ok { 466 | c.mu.Unlock() 467 | return 0, fmt.Errorf("The value for %s is not an uint32", k) 468 | } 469 | nv := rv + n 470 | v.Object = nv 471 | c.items[k] = v 472 | c.mu.Unlock() 473 | return nv, nil 474 | } 475 | 476 | // Increment an item of type uint64 by n. Returns an error if the item's value 477 | // is not an uint64, or if it was not found. If there is no error, the 478 | // incremented value is returned. 479 | func (c *cache) IncrementUint64(k string, n uint64) (uint64, error) { 480 | c.mu.Lock() 481 | v, found := c.items[k] 482 | if !found || v.Expired() { 483 | c.mu.Unlock() 484 | return 0, fmt.Errorf("Item %s not found", k) 485 | } 486 | rv, ok := v.Object.(uint64) 487 | if !ok { 488 | c.mu.Unlock() 489 | return 0, fmt.Errorf("The value for %s is not an uint64", k) 490 | } 491 | nv := rv + n 492 | v.Object = nv 493 | c.items[k] = v 494 | c.mu.Unlock() 495 | return nv, nil 496 | } 497 | 498 | // Increment an item of type float32 by n. Returns an error if the item's value 499 | // is not an float32, or if it was not found. If there is no error, the 500 | // incremented value is returned. 501 | func (c *cache) IncrementFloat32(k string, n float32) (float32, error) { 502 | c.mu.Lock() 503 | v, found := c.items[k] 504 | if !found || v.Expired() { 505 | c.mu.Unlock() 506 | return 0, fmt.Errorf("Item %s not found", k) 507 | } 508 | rv, ok := v.Object.(float32) 509 | if !ok { 510 | c.mu.Unlock() 511 | return 0, fmt.Errorf("The value for %s is not an float32", k) 512 | } 513 | nv := rv + n 514 | v.Object = nv 515 | c.items[k] = v 516 | c.mu.Unlock() 517 | return nv, nil 518 | } 519 | 520 | // Increment an item of type float64 by n. Returns an error if the item's value 521 | // is not an float64, or if it was not found. If there is no error, the 522 | // incremented value is returned. 523 | func (c *cache) IncrementFloat64(k string, n float64) (float64, error) { 524 | c.mu.Lock() 525 | v, found := c.items[k] 526 | if !found || v.Expired() { 527 | c.mu.Unlock() 528 | return 0, fmt.Errorf("Item %s not found", k) 529 | } 530 | rv, ok := v.Object.(float64) 531 | if !ok { 532 | c.mu.Unlock() 533 | return 0, fmt.Errorf("The value for %s is not an float64", k) 534 | } 535 | nv := rv + n 536 | v.Object = nv 537 | c.items[k] = v 538 | c.mu.Unlock() 539 | return nv, nil 540 | } 541 | 542 | // Decrement an item of type int, int8, int16, int32, int64, uintptr, uint, 543 | // uint8, uint32, or uint64, float32 or float64 by n. Returns an error if the 544 | // item's value is not an integer, if it was not found, or if it is not 545 | // possible to decrement it by n. To retrieve the decremented value, use one 546 | // of the specialized methods, e.g. DecrementInt64. 547 | func (c *cache) Decrement(k string, n int64) error { 548 | // TODO: Implement Increment and Decrement more cleanly. 549 | // (Cannot do Increment(k, n*-1) for uints.) 550 | c.mu.Lock() 551 | v, found := c.items[k] 552 | if !found || v.Expired() { 553 | c.mu.Unlock() 554 | return fmt.Errorf("Item not found") 555 | } 556 | switch v.Object.(type) { 557 | case int: 558 | v.Object = v.Object.(int) - int(n) 559 | case int8: 560 | v.Object = v.Object.(int8) - int8(n) 561 | case int16: 562 | v.Object = v.Object.(int16) - int16(n) 563 | case int32: 564 | v.Object = v.Object.(int32) - int32(n) 565 | case int64: 566 | v.Object = v.Object.(int64) - n 567 | case uint: 568 | v.Object = v.Object.(uint) - uint(n) 569 | case uintptr: 570 | v.Object = v.Object.(uintptr) - uintptr(n) 571 | case uint8: 572 | v.Object = v.Object.(uint8) - uint8(n) 573 | case uint16: 574 | v.Object = v.Object.(uint16) - uint16(n) 575 | case uint32: 576 | v.Object = v.Object.(uint32) - uint32(n) 577 | case uint64: 578 | v.Object = v.Object.(uint64) - uint64(n) 579 | case float32: 580 | v.Object = v.Object.(float32) - float32(n) 581 | case float64: 582 | v.Object = v.Object.(float64) - float64(n) 583 | default: 584 | c.mu.Unlock() 585 | return fmt.Errorf("The value for %s is not an integer", k) 586 | } 587 | c.items[k] = v 588 | c.mu.Unlock() 589 | return nil 590 | } 591 | 592 | // Decrement an item of type float32 or float64 by n. Returns an error if the 593 | // item's value is not floating point, if it was not found, or if it is not 594 | // possible to decrement it by n. Pass a negative number to decrement the 595 | // value. To retrieve the decremented value, use one of the specialized methods, 596 | // e.g. DecrementFloat64. 597 | func (c *cache) DecrementFloat(k string, n float64) error { 598 | c.mu.Lock() 599 | v, found := c.items[k] 600 | if !found || v.Expired() { 601 | c.mu.Unlock() 602 | return fmt.Errorf("Item %s not found", k) 603 | } 604 | switch v.Object.(type) { 605 | case float32: 606 | v.Object = v.Object.(float32) - float32(n) 607 | case float64: 608 | v.Object = v.Object.(float64) - n 609 | default: 610 | c.mu.Unlock() 611 | return fmt.Errorf("The value for %s does not have type float32 or float64", k) 612 | } 613 | c.items[k] = v 614 | c.mu.Unlock() 615 | return nil 616 | } 617 | 618 | // Decrement an item of type int by n. Returns an error if the item's value is 619 | // not an int, or if it was not found. If there is no error, the decremented 620 | // value is returned. 621 | func (c *cache) DecrementInt(k string, n int) (int, error) { 622 | c.mu.Lock() 623 | v, found := c.items[k] 624 | if !found || v.Expired() { 625 | c.mu.Unlock() 626 | return 0, fmt.Errorf("Item %s not found", k) 627 | } 628 | rv, ok := v.Object.(int) 629 | if !ok { 630 | c.mu.Unlock() 631 | return 0, fmt.Errorf("The value for %s is not an int", k) 632 | } 633 | nv := rv - n 634 | v.Object = nv 635 | c.items[k] = v 636 | c.mu.Unlock() 637 | return nv, nil 638 | } 639 | 640 | // Decrement an item of type int8 by n. Returns an error if the item's value is 641 | // not an int8, or if it was not found. If there is no error, the decremented 642 | // value is returned. 643 | func (c *cache) DecrementInt8(k string, n int8) (int8, error) { 644 | c.mu.Lock() 645 | v, found := c.items[k] 646 | if !found || v.Expired() { 647 | c.mu.Unlock() 648 | return 0, fmt.Errorf("Item %s not found", k) 649 | } 650 | rv, ok := v.Object.(int8) 651 | if !ok { 652 | c.mu.Unlock() 653 | return 0, fmt.Errorf("The value for %s is not an int8", k) 654 | } 655 | nv := rv - n 656 | v.Object = nv 657 | c.items[k] = v 658 | c.mu.Unlock() 659 | return nv, nil 660 | } 661 | 662 | // Decrement an item of type int16 by n. Returns an error if the item's value is 663 | // not an int16, or if it was not found. If there is no error, the decremented 664 | // value is returned. 665 | func (c *cache) DecrementInt16(k string, n int16) (int16, error) { 666 | c.mu.Lock() 667 | v, found := c.items[k] 668 | if !found || v.Expired() { 669 | c.mu.Unlock() 670 | return 0, fmt.Errorf("Item %s not found", k) 671 | } 672 | rv, ok := v.Object.(int16) 673 | if !ok { 674 | c.mu.Unlock() 675 | return 0, fmt.Errorf("The value for %s is not an int16", k) 676 | } 677 | nv := rv - n 678 | v.Object = nv 679 | c.items[k] = v 680 | c.mu.Unlock() 681 | return nv, nil 682 | } 683 | 684 | // Decrement an item of type int32 by n. Returns an error if the item's value is 685 | // not an int32, or if it was not found. If there is no error, the decremented 686 | // value is returned. 687 | func (c *cache) DecrementInt32(k string, n int32) (int32, error) { 688 | c.mu.Lock() 689 | v, found := c.items[k] 690 | if !found || v.Expired() { 691 | c.mu.Unlock() 692 | return 0, fmt.Errorf("Item %s not found", k) 693 | } 694 | rv, ok := v.Object.(int32) 695 | if !ok { 696 | c.mu.Unlock() 697 | return 0, fmt.Errorf("The value for %s is not an int32", k) 698 | } 699 | nv := rv - n 700 | v.Object = nv 701 | c.items[k] = v 702 | c.mu.Unlock() 703 | return nv, nil 704 | } 705 | 706 | // Decrement an item of type int64 by n. Returns an error if the item's value is 707 | // not an int64, or if it was not found. If there is no error, the decremented 708 | // value is returned. 709 | func (c *cache) DecrementInt64(k string, n int64) (int64, error) { 710 | c.mu.Lock() 711 | v, found := c.items[k] 712 | if !found || v.Expired() { 713 | c.mu.Unlock() 714 | return 0, fmt.Errorf("Item %s not found", k) 715 | } 716 | rv, ok := v.Object.(int64) 717 | if !ok { 718 | c.mu.Unlock() 719 | return 0, fmt.Errorf("The value for %s is not an int64", k) 720 | } 721 | nv := rv - n 722 | v.Object = nv 723 | c.items[k] = v 724 | c.mu.Unlock() 725 | return nv, nil 726 | } 727 | 728 | // Decrement an item of type uint by n. Returns an error if the item's value is 729 | // not an uint, or if it was not found. If there is no error, the decremented 730 | // value is returned. 731 | func (c *cache) DecrementUint(k string, n uint) (uint, error) { 732 | c.mu.Lock() 733 | v, found := c.items[k] 734 | if !found || v.Expired() { 735 | c.mu.Unlock() 736 | return 0, fmt.Errorf("Item %s not found", k) 737 | } 738 | rv, ok := v.Object.(uint) 739 | if !ok { 740 | c.mu.Unlock() 741 | return 0, fmt.Errorf("The value for %s is not an uint", k) 742 | } 743 | nv := rv - n 744 | v.Object = nv 745 | c.items[k] = v 746 | c.mu.Unlock() 747 | return nv, nil 748 | } 749 | 750 | // Decrement an item of type uintptr by n. Returns an error if the item's value 751 | // is not an uintptr, or if it was not found. If there is no error, the 752 | // decremented value is returned. 753 | func (c *cache) DecrementUintptr(k string, n uintptr) (uintptr, error) { 754 | c.mu.Lock() 755 | v, found := c.items[k] 756 | if !found || v.Expired() { 757 | c.mu.Unlock() 758 | return 0, fmt.Errorf("Item %s not found", k) 759 | } 760 | rv, ok := v.Object.(uintptr) 761 | if !ok { 762 | c.mu.Unlock() 763 | return 0, fmt.Errorf("The value for %s is not an uintptr", k) 764 | } 765 | nv := rv - n 766 | v.Object = nv 767 | c.items[k] = v 768 | c.mu.Unlock() 769 | return nv, nil 770 | } 771 | 772 | // Decrement an item of type uint8 by n. Returns an error if the item's value is 773 | // not an uint8, or if it was not found. If there is no error, the decremented 774 | // value is returned. 775 | func (c *cache) DecrementUint8(k string, n uint8) (uint8, error) { 776 | c.mu.Lock() 777 | v, found := c.items[k] 778 | if !found || v.Expired() { 779 | c.mu.Unlock() 780 | return 0, fmt.Errorf("Item %s not found", k) 781 | } 782 | rv, ok := v.Object.(uint8) 783 | if !ok { 784 | c.mu.Unlock() 785 | return 0, fmt.Errorf("The value for %s is not an uint8", k) 786 | } 787 | nv := rv - n 788 | v.Object = nv 789 | c.items[k] = v 790 | c.mu.Unlock() 791 | return nv, nil 792 | } 793 | 794 | // Decrement an item of type uint16 by n. Returns an error if the item's value 795 | // is not an uint16, or if it was not found. If there is no error, the 796 | // decremented value is returned. 797 | func (c *cache) DecrementUint16(k string, n uint16) (uint16, error) { 798 | c.mu.Lock() 799 | v, found := c.items[k] 800 | if !found || v.Expired() { 801 | c.mu.Unlock() 802 | return 0, fmt.Errorf("Item %s not found", k) 803 | } 804 | rv, ok := v.Object.(uint16) 805 | if !ok { 806 | c.mu.Unlock() 807 | return 0, fmt.Errorf("The value for %s is not an uint16", k) 808 | } 809 | nv := rv - n 810 | v.Object = nv 811 | c.items[k] = v 812 | c.mu.Unlock() 813 | return nv, nil 814 | } 815 | 816 | // Decrement an item of type uint32 by n. Returns an error if the item's value 817 | // is not an uint32, or if it was not found. If there is no error, the 818 | // decremented value is returned. 819 | func (c *cache) DecrementUint32(k string, n uint32) (uint32, error) { 820 | c.mu.Lock() 821 | v, found := c.items[k] 822 | if !found || v.Expired() { 823 | c.mu.Unlock() 824 | return 0, fmt.Errorf("Item %s not found", k) 825 | } 826 | rv, ok := v.Object.(uint32) 827 | if !ok { 828 | c.mu.Unlock() 829 | return 0, fmt.Errorf("The value for %s is not an uint32", k) 830 | } 831 | nv := rv - n 832 | v.Object = nv 833 | c.items[k] = v 834 | c.mu.Unlock() 835 | return nv, nil 836 | } 837 | 838 | // Decrement an item of type uint64 by n. Returns an error if the item's value 839 | // is not an uint64, or if it was not found. If there is no error, the 840 | // decremented value is returned. 841 | func (c *cache) DecrementUint64(k string, n uint64) (uint64, error) { 842 | c.mu.Lock() 843 | v, found := c.items[k] 844 | if !found || v.Expired() { 845 | c.mu.Unlock() 846 | return 0, fmt.Errorf("Item %s not found", k) 847 | } 848 | rv, ok := v.Object.(uint64) 849 | if !ok { 850 | c.mu.Unlock() 851 | return 0, fmt.Errorf("The value for %s is not an uint64", k) 852 | } 853 | nv := rv - n 854 | v.Object = nv 855 | c.items[k] = v 856 | c.mu.Unlock() 857 | return nv, nil 858 | } 859 | 860 | // Decrement an item of type float32 by n. Returns an error if the item's value 861 | // is not an float32, or if it was not found. If there is no error, the 862 | // decremented value is returned. 863 | func (c *cache) DecrementFloat32(k string, n float32) (float32, error) { 864 | c.mu.Lock() 865 | v, found := c.items[k] 866 | if !found || v.Expired() { 867 | c.mu.Unlock() 868 | return 0, fmt.Errorf("Item %s not found", k) 869 | } 870 | rv, ok := v.Object.(float32) 871 | if !ok { 872 | c.mu.Unlock() 873 | return 0, fmt.Errorf("The value for %s is not an float32", k) 874 | } 875 | nv := rv - n 876 | v.Object = nv 877 | c.items[k] = v 878 | c.mu.Unlock() 879 | return nv, nil 880 | } 881 | 882 | // Decrement an item of type float64 by n. Returns an error if the item's value 883 | // is not an float64, or if it was not found. If there is no error, the 884 | // decremented value is returned. 885 | func (c *cache) DecrementFloat64(k string, n float64) (float64, error) { 886 | c.mu.Lock() 887 | v, found := c.items[k] 888 | if !found || v.Expired() { 889 | c.mu.Unlock() 890 | return 0, fmt.Errorf("Item %s not found", k) 891 | } 892 | rv, ok := v.Object.(float64) 893 | if !ok { 894 | c.mu.Unlock() 895 | return 0, fmt.Errorf("The value for %s is not an float64", k) 896 | } 897 | nv := rv - n 898 | v.Object = nv 899 | c.items[k] = v 900 | c.mu.Unlock() 901 | return nv, nil 902 | } 903 | 904 | // Delete an item from the cache. Does nothing if the key is not in the cache. 905 | func (c *cache) Delete(k string) { 906 | c.mu.Lock() 907 | v, evicted := c.delete(k) 908 | c.mu.Unlock() 909 | if evicted { 910 | c.onEvicted(k, v) 911 | } 912 | } 913 | 914 | func (c *cache) delete(k string) (interface{}, bool) { 915 | if c.onEvicted != nil { 916 | if v, found := c.items[k]; found { 917 | delete(c.items, k) 918 | return v.Object, true 919 | } 920 | } 921 | delete(c.items, k) 922 | return nil, false 923 | } 924 | 925 | type keyAndValue struct { 926 | key string 927 | value interface{} 928 | } 929 | 930 | // Delete all expired items from the cache. 931 | func (c *cache) DeleteExpired() { 932 | var evictedItems []keyAndValue 933 | now := time.Now().UnixNano() 934 | c.mu.Lock() 935 | for k, v := range c.items { 936 | // "Inlining" of expired 937 | if v.Expiration > 0 && now > v.Expiration { 938 | ov, evicted := c.delete(k) 939 | if evicted { 940 | evictedItems = append(evictedItems, keyAndValue{k, ov}) 941 | } 942 | } 943 | } 944 | c.mu.Unlock() 945 | for _, v := range evictedItems { 946 | c.onEvicted(v.key, v.value) 947 | } 948 | } 949 | 950 | // Sets an (optional) function that is called with the key and value when an 951 | // item is evicted from the cache. (Including when it is deleted manually, but 952 | // not when it is overwritten.) Set to nil to disable. 953 | func (c *cache) OnEvicted(f func(string, interface{})) { 954 | c.mu.Lock() 955 | c.onEvicted = f 956 | c.mu.Unlock() 957 | } 958 | 959 | // Write the cache's items (using Gob) to an io.Writer. 960 | // 961 | // NOTE: This method is deprecated in favor of c.Items() and NewFrom() (see the 962 | // documentation for NewFrom().) 963 | func (c *cache) Save(w io.Writer) (err error) { 964 | enc := gob.NewEncoder(w) 965 | defer func() { 966 | if x := recover(); x != nil { 967 | err = fmt.Errorf("Error registering item types with Gob library") 968 | } 969 | }() 970 | c.mu.RLock() 971 | defer c.mu.RUnlock() 972 | for _, v := range c.items { 973 | gob.Register(v.Object) 974 | } 975 | err = enc.Encode(&c.items) 976 | return 977 | } 978 | 979 | // Save the cache's items to the given filename, creating the file if it 980 | // doesn't exist, and overwriting it if it does. 981 | // 982 | // NOTE: This method is deprecated in favor of c.Items() and NewFrom() (see the 983 | // documentation for NewFrom().) 984 | func (c *cache) SaveFile(fname string) error { 985 | fp, err := os.Create(fname) 986 | if err != nil { 987 | return err 988 | } 989 | err = c.Save(fp) 990 | if err != nil { 991 | fp.Close() 992 | return err 993 | } 994 | return fp.Close() 995 | } 996 | 997 | // Add (Gob-serialized) cache items from an io.Reader, excluding any items with 998 | // keys that already exist (and haven't expired) in the current cache. 999 | // 1000 | // NOTE: This method is deprecated in favor of c.Items() and NewFrom() (see the 1001 | // documentation for NewFrom().) 1002 | func (c *cache) Load(r io.Reader) error { 1003 | dec := gob.NewDecoder(r) 1004 | items := map[string]Item{} 1005 | err := dec.Decode(&items) 1006 | if err == nil { 1007 | c.mu.Lock() 1008 | defer c.mu.Unlock() 1009 | for k, v := range items { 1010 | ov, found := c.items[k] 1011 | if !found || ov.Expired() { 1012 | c.items[k] = v 1013 | } 1014 | } 1015 | } 1016 | return err 1017 | } 1018 | 1019 | // Load and add cache items from the given filename, excluding any items with 1020 | // keys that already exist in the current cache. 1021 | // 1022 | // NOTE: This method is deprecated in favor of c.Items() and NewFrom() (see the 1023 | // documentation for NewFrom().) 1024 | func (c *cache) LoadFile(fname string) error { 1025 | fp, err := os.Open(fname) 1026 | if err != nil { 1027 | return err 1028 | } 1029 | err = c.Load(fp) 1030 | if err != nil { 1031 | fp.Close() 1032 | return err 1033 | } 1034 | return fp.Close() 1035 | } 1036 | 1037 | // Copies all unexpired items in the cache into a new map and returns it. 1038 | func (c *cache) Items() map[string]Item { 1039 | c.mu.RLock() 1040 | defer c.mu.RUnlock() 1041 | m := make(map[string]Item, len(c.items)) 1042 | now := time.Now().UnixNano() 1043 | for k, v := range c.items { 1044 | // "Inlining" of Expired 1045 | if v.Expiration > 0 { 1046 | if now > v.Expiration { 1047 | continue 1048 | } 1049 | } 1050 | m[k] = v 1051 | } 1052 | return m 1053 | } 1054 | 1055 | // Returns the number of items in the cache. This may include items that have 1056 | // expired, but have not yet been cleaned up. 1057 | func (c *cache) ItemCount() int { 1058 | c.mu.RLock() 1059 | n := len(c.items) 1060 | c.mu.RUnlock() 1061 | return n 1062 | } 1063 | 1064 | // Delete all items from the cache. 1065 | func (c *cache) Flush() { 1066 | c.mu.Lock() 1067 | c.items = map[string]Item{} 1068 | c.mu.Unlock() 1069 | } 1070 | 1071 | type janitor struct { 1072 | Interval time.Duration 1073 | stop chan bool 1074 | } 1075 | 1076 | func (j *janitor) Run(c *cache) { 1077 | ticker := time.NewTicker(j.Interval) 1078 | for { 1079 | select { 1080 | case <-ticker.C: 1081 | c.DeleteExpired() 1082 | case <-j.stop: 1083 | ticker.Stop() 1084 | return 1085 | } 1086 | } 1087 | } 1088 | 1089 | func stopJanitor(c *Cache) { 1090 | c.janitor.stop <- true 1091 | } 1092 | 1093 | func runJanitor(c *cache, ci time.Duration) { 1094 | j := &janitor{ 1095 | Interval: ci, 1096 | stop: make(chan bool), 1097 | } 1098 | c.janitor = j 1099 | go j.Run(c) 1100 | } 1101 | 1102 | func newCache(de time.Duration, m map[string]Item) *cache { 1103 | if de == 0 { 1104 | de = -1 1105 | } 1106 | c := &cache{ 1107 | defaultExpiration: de, 1108 | items: m, 1109 | } 1110 | return c 1111 | } 1112 | 1113 | func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache { 1114 | c := newCache(de, m) 1115 | // This trick ensures that the janitor goroutine (which--granted it 1116 | // was enabled--is running DeleteExpired on c forever) does not keep 1117 | // the returned C object from being garbage collected. When it is 1118 | // garbage collected, the finalizer stops the janitor goroutine, after 1119 | // which c can be collected. 1120 | C := &Cache{c} 1121 | if ci > 0 { 1122 | runJanitor(c, ci) 1123 | runtime.SetFinalizer(C, stopJanitor) 1124 | } 1125 | return C 1126 | } 1127 | 1128 | // Return a new cache with a given default expiration duration and cleanup 1129 | // interval. If the expiration duration is less than one (or NoExpiration), 1130 | // the items in the cache never expire (by default), and must be deleted 1131 | // manually. If the cleanup interval is less than one, expired items are not 1132 | // deleted from the cache before calling c.DeleteExpired(). 1133 | func New(defaultExpiration, cleanupInterval time.Duration) *Cache { 1134 | items := make(map[string]Item) 1135 | return newCacheWithJanitor(defaultExpiration, cleanupInterval, items) 1136 | } 1137 | 1138 | // Return a new cache with a given default expiration duration and cleanup 1139 | // interval. If the expiration duration is less than one (or NoExpiration), 1140 | // the items in the cache never expire (by default), and must be deleted 1141 | // manually. If the cleanup interval is less than one, expired items are not 1142 | // deleted from the cache before calling c.DeleteExpired(). 1143 | // 1144 | // NewFrom() also accepts an items map which will serve as the underlying map 1145 | // for the cache. This is useful for starting from a deserialized cache 1146 | // (serialized using e.g. gob.Encode() on c.Items()), or passing in e.g. 1147 | // make(map[string]Item, 500) to improve startup performance when the cache 1148 | // is expected to reach a certain minimum size. 1149 | // 1150 | // Only the cache's methods synchronize access to this map, so it is not 1151 | // recommended to keep any references to the map around after creating a cache. 1152 | // If need be, the map can be accessed at a later point using c.Items() (subject 1153 | // to the same caveat.) 1154 | // 1155 | // Note regarding serialization: When using e.g. gob, make sure to 1156 | // gob.Register() the individual types stored in the cache before encoding a 1157 | // map retrieved with c.Items(), and to register those same types before 1158 | // decoding a blob containing an items map. 1159 | func NewFrom(defaultExpiration, cleanupInterval time.Duration, items map[string]Item) *Cache { 1160 | return newCacheWithJanitor(defaultExpiration, cleanupInterval, items) 1161 | } 1162 | -------------------------------------------------------------------------------- /cache/cache_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "runtime" 7 | "strconv" 8 | "sync" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | type TestStruct struct { 14 | Num int 15 | Children []*TestStruct 16 | } 17 | 18 | func TestCache(t *testing.T) { 19 | tc := New(DefaultExpiration, 0) 20 | 21 | a, found := tc.Get("a") 22 | if found || a != nil { 23 | t.Error("Getting A found value that shouldn't exist:", a) 24 | } 25 | 26 | b, found := tc.Get("b") 27 | if found || b != nil { 28 | t.Error("Getting B found value that shouldn't exist:", b) 29 | } 30 | 31 | c, found := tc.Get("c") 32 | if found || c != nil { 33 | t.Error("Getting C found value that shouldn't exist:", c) 34 | } 35 | 36 | tc.Set("a", 1, DefaultExpiration) 37 | tc.Set("b", "b", DefaultExpiration) 38 | tc.Set("c", 3.5, DefaultExpiration) 39 | 40 | x, found := tc.Get("a") 41 | if !found { 42 | t.Error("a was not found while getting a2") 43 | } 44 | if x == nil { 45 | t.Error("x for a is nil") 46 | } else if a2 := x.(int); a2+2 != 3 { 47 | t.Error("a2 (which should be 1) plus 2 does not equal 3; value:", a2) 48 | } 49 | 50 | x, found = tc.Get("b") 51 | if !found { 52 | t.Error("b was not found while getting b2") 53 | } 54 | if x == nil { 55 | t.Error("x for b is nil") 56 | } else if b2 := x.(string); b2+"B" != "bB" { 57 | t.Error("b2 (which should be b) plus B does not equal bB; value:", b2) 58 | } 59 | 60 | x, found = tc.Get("c") 61 | if !found { 62 | t.Error("c was not found while getting c2") 63 | } 64 | if x == nil { 65 | t.Error("x for c is nil") 66 | } else if c2 := x.(float64); c2+1.2 != 4.7 { 67 | t.Error("c2 (which should be 3.5) plus 1.2 does not equal 4.7; value:", c2) 68 | } 69 | } 70 | 71 | func TestCacheTimes(t *testing.T) { 72 | var found bool 73 | 74 | tc := New(50*time.Millisecond, 1*time.Millisecond) 75 | tc.Set("a", 1, DefaultExpiration) 76 | tc.Set("b", 2, NoExpiration) 77 | tc.Set("c", 3, 20*time.Millisecond) 78 | tc.Set("d", 4, 70*time.Millisecond) 79 | 80 | <-time.After(25 * time.Millisecond) 81 | _, found = tc.Get("c") 82 | if found { 83 | t.Error("Found c when it should have been automatically deleted") 84 | } 85 | 86 | <-time.After(30 * time.Millisecond) 87 | _, found = tc.Get("a") 88 | if found { 89 | t.Error("Found a when it should have been automatically deleted") 90 | } 91 | 92 | _, found = tc.Get("b") 93 | if !found { 94 | t.Error("Did not find b even though it was set to never expire") 95 | } 96 | 97 | _, found = tc.Get("d") 98 | if !found { 99 | t.Error("Did not find d even though it was set to expire later than the default") 100 | } 101 | 102 | <-time.After(20 * time.Millisecond) 103 | _, found = tc.Get("d") 104 | if found { 105 | t.Error("Found d when it should have been automatically deleted (later than the default)") 106 | } 107 | } 108 | 109 | func TestNewFrom(t *testing.T) { 110 | m := map[string]Item{ 111 | "a": Item{ 112 | Object: 1, 113 | Expiration: 0, 114 | }, 115 | "b": Item{ 116 | Object: 2, 117 | Expiration: 0, 118 | }, 119 | } 120 | tc := NewFrom(DefaultExpiration, 0, m) 121 | a, found := tc.Get("a") 122 | if !found { 123 | t.Fatal("Did not find a") 124 | } 125 | if a.(int) != 1 { 126 | t.Fatal("a is not 1") 127 | } 128 | b, found := tc.Get("b") 129 | if !found { 130 | t.Fatal("Did not find b") 131 | } 132 | if b.(int) != 2 { 133 | t.Fatal("b is not 2") 134 | } 135 | } 136 | 137 | func TestStorePointerToStruct(t *testing.T) { 138 | tc := New(DefaultExpiration, 0) 139 | tc.Set("foo", &TestStruct{Num: 1}, DefaultExpiration) 140 | x, found := tc.Get("foo") 141 | if !found { 142 | t.Fatal("*TestStruct was not found for foo") 143 | } 144 | foo := x.(*TestStruct) 145 | foo.Num++ 146 | 147 | y, found := tc.Get("foo") 148 | if !found { 149 | t.Fatal("*TestStruct was not found for foo (second time)") 150 | } 151 | bar := y.(*TestStruct) 152 | if bar.Num != 2 { 153 | t.Fatal("TestStruct.Num is not 2") 154 | } 155 | } 156 | 157 | func TestIncrementWithInt(t *testing.T) { 158 | tc := New(DefaultExpiration, 0) 159 | tc.Set("tint", 1, DefaultExpiration) 160 | err := tc.Increment("tint", 2) 161 | if err != nil { 162 | t.Error("Error incrementing:", err) 163 | } 164 | x, found := tc.Get("tint") 165 | if !found { 166 | t.Error("tint was not found") 167 | } 168 | if x.(int) != 3 { 169 | t.Error("tint is not 3:", x) 170 | } 171 | } 172 | 173 | func TestIncrementWithInt8(t *testing.T) { 174 | tc := New(DefaultExpiration, 0) 175 | tc.Set("tint8", int8(1), DefaultExpiration) 176 | err := tc.Increment("tint8", 2) 177 | if err != nil { 178 | t.Error("Error incrementing:", err) 179 | } 180 | x, found := tc.Get("tint8") 181 | if !found { 182 | t.Error("tint8 was not found") 183 | } 184 | if x.(int8) != 3 { 185 | t.Error("tint8 is not 3:", x) 186 | } 187 | } 188 | 189 | func TestIncrementWithInt16(t *testing.T) { 190 | tc := New(DefaultExpiration, 0) 191 | tc.Set("tint16", int16(1), DefaultExpiration) 192 | err := tc.Increment("tint16", 2) 193 | if err != nil { 194 | t.Error("Error incrementing:", err) 195 | } 196 | x, found := tc.Get("tint16") 197 | if !found { 198 | t.Error("tint16 was not found") 199 | } 200 | if x.(int16) != 3 { 201 | t.Error("tint16 is not 3:", x) 202 | } 203 | } 204 | 205 | func TestIncrementWithInt32(t *testing.T) { 206 | tc := New(DefaultExpiration, 0) 207 | tc.Set("tint32", int32(1), DefaultExpiration) 208 | err := tc.Increment("tint32", 2) 209 | if err != nil { 210 | t.Error("Error incrementing:", err) 211 | } 212 | x, found := tc.Get("tint32") 213 | if !found { 214 | t.Error("tint32 was not found") 215 | } 216 | if x.(int32) != 3 { 217 | t.Error("tint32 is not 3:", x) 218 | } 219 | } 220 | 221 | func TestIncrementWithInt64(t *testing.T) { 222 | tc := New(DefaultExpiration, 0) 223 | tc.Set("tint64", int64(1), DefaultExpiration) 224 | err := tc.Increment("tint64", 2) 225 | if err != nil { 226 | t.Error("Error incrementing:", err) 227 | } 228 | x, found := tc.Get("tint64") 229 | if !found { 230 | t.Error("tint64 was not found") 231 | } 232 | if x.(int64) != 3 { 233 | t.Error("tint64 is not 3:", x) 234 | } 235 | } 236 | 237 | func TestIncrementWithUint(t *testing.T) { 238 | tc := New(DefaultExpiration, 0) 239 | tc.Set("tuint", uint(1), DefaultExpiration) 240 | err := tc.Increment("tuint", 2) 241 | if err != nil { 242 | t.Error("Error incrementing:", err) 243 | } 244 | x, found := tc.Get("tuint") 245 | if !found { 246 | t.Error("tuint was not found") 247 | } 248 | if x.(uint) != 3 { 249 | t.Error("tuint is not 3:", x) 250 | } 251 | } 252 | 253 | func TestIncrementWithUintptr(t *testing.T) { 254 | tc := New(DefaultExpiration, 0) 255 | tc.Set("tuintptr", uintptr(1), DefaultExpiration) 256 | err := tc.Increment("tuintptr", 2) 257 | if err != nil { 258 | t.Error("Error incrementing:", err) 259 | } 260 | 261 | x, found := tc.Get("tuintptr") 262 | if !found { 263 | t.Error("tuintptr was not found") 264 | } 265 | if x.(uintptr) != 3 { 266 | t.Error("tuintptr is not 3:", x) 267 | } 268 | } 269 | 270 | func TestIncrementWithUint8(t *testing.T) { 271 | tc := New(DefaultExpiration, 0) 272 | tc.Set("tuint8", uint8(1), DefaultExpiration) 273 | err := tc.Increment("tuint8", 2) 274 | if err != nil { 275 | t.Error("Error incrementing:", err) 276 | } 277 | x, found := tc.Get("tuint8") 278 | if !found { 279 | t.Error("tuint8 was not found") 280 | } 281 | if x.(uint8) != 3 { 282 | t.Error("tuint8 is not 3:", x) 283 | } 284 | } 285 | 286 | func TestIncrementWithUint16(t *testing.T) { 287 | tc := New(DefaultExpiration, 0) 288 | tc.Set("tuint16", uint16(1), DefaultExpiration) 289 | err := tc.Increment("tuint16", 2) 290 | if err != nil { 291 | t.Error("Error incrementing:", err) 292 | } 293 | 294 | x, found := tc.Get("tuint16") 295 | if !found { 296 | t.Error("tuint16 was not found") 297 | } 298 | if x.(uint16) != 3 { 299 | t.Error("tuint16 is not 3:", x) 300 | } 301 | } 302 | 303 | func TestIncrementWithUint32(t *testing.T) { 304 | tc := New(DefaultExpiration, 0) 305 | tc.Set("tuint32", uint32(1), DefaultExpiration) 306 | err := tc.Increment("tuint32", 2) 307 | if err != nil { 308 | t.Error("Error incrementing:", err) 309 | } 310 | x, found := tc.Get("tuint32") 311 | if !found { 312 | t.Error("tuint32 was not found") 313 | } 314 | if x.(uint32) != 3 { 315 | t.Error("tuint32 is not 3:", x) 316 | } 317 | } 318 | 319 | func TestIncrementWithUint64(t *testing.T) { 320 | tc := New(DefaultExpiration, 0) 321 | tc.Set("tuint64", uint64(1), DefaultExpiration) 322 | err := tc.Increment("tuint64", 2) 323 | if err != nil { 324 | t.Error("Error incrementing:", err) 325 | } 326 | 327 | x, found := tc.Get("tuint64") 328 | if !found { 329 | t.Error("tuint64 was not found") 330 | } 331 | if x.(uint64) != 3 { 332 | t.Error("tuint64 is not 3:", x) 333 | } 334 | } 335 | 336 | func TestIncrementWithFloat32(t *testing.T) { 337 | tc := New(DefaultExpiration, 0) 338 | tc.Set("float32", float32(1.5), DefaultExpiration) 339 | err := tc.Increment("float32", 2) 340 | if err != nil { 341 | t.Error("Error incrementing:", err) 342 | } 343 | x, found := tc.Get("float32") 344 | if !found { 345 | t.Error("float32 was not found") 346 | } 347 | if x.(float32) != 3.5 { 348 | t.Error("float32 is not 3.5:", x) 349 | } 350 | } 351 | 352 | func TestIncrementWithFloat64(t *testing.T) { 353 | tc := New(DefaultExpiration, 0) 354 | tc.Set("float64", float64(1.5), DefaultExpiration) 355 | err := tc.Increment("float64", 2) 356 | if err != nil { 357 | t.Error("Error incrementing:", err) 358 | } 359 | x, found := tc.Get("float64") 360 | if !found { 361 | t.Error("float64 was not found") 362 | } 363 | if x.(float64) != 3.5 { 364 | t.Error("float64 is not 3.5:", x) 365 | } 366 | } 367 | 368 | func TestIncrementFloatWithFloat32(t *testing.T) { 369 | tc := New(DefaultExpiration, 0) 370 | tc.Set("float32", float32(1.5), DefaultExpiration) 371 | err := tc.IncrementFloat("float32", 2) 372 | if err != nil { 373 | t.Error("Error incrementfloating:", err) 374 | } 375 | x, found := tc.Get("float32") 376 | if !found { 377 | t.Error("float32 was not found") 378 | } 379 | if x.(float32) != 3.5 { 380 | t.Error("float32 is not 3.5:", x) 381 | } 382 | } 383 | 384 | func TestIncrementFloatWithFloat64(t *testing.T) { 385 | tc := New(DefaultExpiration, 0) 386 | tc.Set("float64", float64(1.5), DefaultExpiration) 387 | err := tc.IncrementFloat("float64", 2) 388 | if err != nil { 389 | t.Error("Error incrementfloating:", err) 390 | } 391 | x, found := tc.Get("float64") 392 | if !found { 393 | t.Error("float64 was not found") 394 | } 395 | if x.(float64) != 3.5 { 396 | t.Error("float64 is not 3.5:", x) 397 | } 398 | } 399 | 400 | func TestDecrementWithInt(t *testing.T) { 401 | tc := New(DefaultExpiration, 0) 402 | tc.Set("int", int(5), DefaultExpiration) 403 | err := tc.Decrement("int", 2) 404 | if err != nil { 405 | t.Error("Error decrementing:", err) 406 | } 407 | x, found := tc.Get("int") 408 | if !found { 409 | t.Error("int was not found") 410 | } 411 | if x.(int) != 3 { 412 | t.Error("int is not 3:", x) 413 | } 414 | } 415 | 416 | func TestDecrementWithInt8(t *testing.T) { 417 | tc := New(DefaultExpiration, 0) 418 | tc.Set("int8", int8(5), DefaultExpiration) 419 | err := tc.Decrement("int8", 2) 420 | if err != nil { 421 | t.Error("Error decrementing:", err) 422 | } 423 | x, found := tc.Get("int8") 424 | if !found { 425 | t.Error("int8 was not found") 426 | } 427 | if x.(int8) != 3 { 428 | t.Error("int8 is not 3:", x) 429 | } 430 | } 431 | 432 | func TestDecrementWithInt16(t *testing.T) { 433 | tc := New(DefaultExpiration, 0) 434 | tc.Set("int16", int16(5), DefaultExpiration) 435 | err := tc.Decrement("int16", 2) 436 | if err != nil { 437 | t.Error("Error decrementing:", err) 438 | } 439 | x, found := tc.Get("int16") 440 | if !found { 441 | t.Error("int16 was not found") 442 | } 443 | if x.(int16) != 3 { 444 | t.Error("int16 is not 3:", x) 445 | } 446 | } 447 | 448 | func TestDecrementWithInt32(t *testing.T) { 449 | tc := New(DefaultExpiration, 0) 450 | tc.Set("int32", int32(5), DefaultExpiration) 451 | err := tc.Decrement("int32", 2) 452 | if err != nil { 453 | t.Error("Error decrementing:", err) 454 | } 455 | x, found := tc.Get("int32") 456 | if !found { 457 | t.Error("int32 was not found") 458 | } 459 | if x.(int32) != 3 { 460 | t.Error("int32 is not 3:", x) 461 | } 462 | } 463 | 464 | func TestDecrementWithInt64(t *testing.T) { 465 | tc := New(DefaultExpiration, 0) 466 | tc.Set("int64", int64(5), DefaultExpiration) 467 | err := tc.Decrement("int64", 2) 468 | if err != nil { 469 | t.Error("Error decrementing:", err) 470 | } 471 | x, found := tc.Get("int64") 472 | if !found { 473 | t.Error("int64 was not found") 474 | } 475 | if x.(int64) != 3 { 476 | t.Error("int64 is not 3:", x) 477 | } 478 | } 479 | 480 | func TestDecrementWithUint(t *testing.T) { 481 | tc := New(DefaultExpiration, 0) 482 | tc.Set("uint", uint(5), DefaultExpiration) 483 | err := tc.Decrement("uint", 2) 484 | if err != nil { 485 | t.Error("Error decrementing:", err) 486 | } 487 | x, found := tc.Get("uint") 488 | if !found { 489 | t.Error("uint was not found") 490 | } 491 | if x.(uint) != 3 { 492 | t.Error("uint is not 3:", x) 493 | } 494 | } 495 | 496 | func TestDecrementWithUintptr(t *testing.T) { 497 | tc := New(DefaultExpiration, 0) 498 | tc.Set("uintptr", uintptr(5), DefaultExpiration) 499 | err := tc.Decrement("uintptr", 2) 500 | if err != nil { 501 | t.Error("Error decrementing:", err) 502 | } 503 | x, found := tc.Get("uintptr") 504 | if !found { 505 | t.Error("uintptr was not found") 506 | } 507 | if x.(uintptr) != 3 { 508 | t.Error("uintptr is not 3:", x) 509 | } 510 | } 511 | 512 | func TestDecrementWithUint8(t *testing.T) { 513 | tc := New(DefaultExpiration, 0) 514 | tc.Set("uint8", uint8(5), DefaultExpiration) 515 | err := tc.Decrement("uint8", 2) 516 | if err != nil { 517 | t.Error("Error decrementing:", err) 518 | } 519 | x, found := tc.Get("uint8") 520 | if !found { 521 | t.Error("uint8 was not found") 522 | } 523 | if x.(uint8) != 3 { 524 | t.Error("uint8 is not 3:", x) 525 | } 526 | } 527 | 528 | func TestDecrementWithUint16(t *testing.T) { 529 | tc := New(DefaultExpiration, 0) 530 | tc.Set("uint16", uint16(5), DefaultExpiration) 531 | err := tc.Decrement("uint16", 2) 532 | if err != nil { 533 | t.Error("Error decrementing:", err) 534 | } 535 | x, found := tc.Get("uint16") 536 | if !found { 537 | t.Error("uint16 was not found") 538 | } 539 | if x.(uint16) != 3 { 540 | t.Error("uint16 is not 3:", x) 541 | } 542 | } 543 | 544 | func TestDecrementWithUint32(t *testing.T) { 545 | tc := New(DefaultExpiration, 0) 546 | tc.Set("uint32", uint32(5), DefaultExpiration) 547 | err := tc.Decrement("uint32", 2) 548 | if err != nil { 549 | t.Error("Error decrementing:", err) 550 | } 551 | x, found := tc.Get("uint32") 552 | if !found { 553 | t.Error("uint32 was not found") 554 | } 555 | if x.(uint32) != 3 { 556 | t.Error("uint32 is not 3:", x) 557 | } 558 | } 559 | 560 | func TestDecrementWithUint64(t *testing.T) { 561 | tc := New(DefaultExpiration, 0) 562 | tc.Set("uint64", uint64(5), DefaultExpiration) 563 | err := tc.Decrement("uint64", 2) 564 | if err != nil { 565 | t.Error("Error decrementing:", err) 566 | } 567 | x, found := tc.Get("uint64") 568 | if !found { 569 | t.Error("uint64 was not found") 570 | } 571 | if x.(uint64) != 3 { 572 | t.Error("uint64 is not 3:", x) 573 | } 574 | } 575 | 576 | func TestDecrementWithFloat32(t *testing.T) { 577 | tc := New(DefaultExpiration, 0) 578 | tc.Set("float32", float32(5.5), DefaultExpiration) 579 | err := tc.Decrement("float32", 2) 580 | if err != nil { 581 | t.Error("Error decrementing:", err) 582 | } 583 | x, found := tc.Get("float32") 584 | if !found { 585 | t.Error("float32 was not found") 586 | } 587 | if x.(float32) != 3.5 { 588 | t.Error("float32 is not 3:", x) 589 | } 590 | } 591 | 592 | func TestDecrementWithFloat64(t *testing.T) { 593 | tc := New(DefaultExpiration, 0) 594 | tc.Set("float64", float64(5.5), DefaultExpiration) 595 | err := tc.Decrement("float64", 2) 596 | if err != nil { 597 | t.Error("Error decrementing:", err) 598 | } 599 | x, found := tc.Get("float64") 600 | if !found { 601 | t.Error("float64 was not found") 602 | } 603 | if x.(float64) != 3.5 { 604 | t.Error("float64 is not 3:", x) 605 | } 606 | } 607 | 608 | func TestDecrementFloatWithFloat32(t *testing.T) { 609 | tc := New(DefaultExpiration, 0) 610 | tc.Set("float32", float32(5.5), DefaultExpiration) 611 | err := tc.DecrementFloat("float32", 2) 612 | if err != nil { 613 | t.Error("Error decrementing:", err) 614 | } 615 | x, found := tc.Get("float32") 616 | if !found { 617 | t.Error("float32 was not found") 618 | } 619 | if x.(float32) != 3.5 { 620 | t.Error("float32 is not 3:", x) 621 | } 622 | } 623 | 624 | func TestDecrementFloatWithFloat64(t *testing.T) { 625 | tc := New(DefaultExpiration, 0) 626 | tc.Set("float64", float64(5.5), DefaultExpiration) 627 | err := tc.DecrementFloat("float64", 2) 628 | if err != nil { 629 | t.Error("Error decrementing:", err) 630 | } 631 | x, found := tc.Get("float64") 632 | if !found { 633 | t.Error("float64 was not found") 634 | } 635 | if x.(float64) != 3.5 { 636 | t.Error("float64 is not 3:", x) 637 | } 638 | } 639 | 640 | func TestIncrementInt(t *testing.T) { 641 | tc := New(DefaultExpiration, 0) 642 | tc.Set("tint", 1, DefaultExpiration) 643 | n, err := tc.IncrementInt("tint", 2) 644 | if err != nil { 645 | t.Error("Error incrementing:", err) 646 | } 647 | if n != 3 { 648 | t.Error("Returned number is not 3:", n) 649 | } 650 | x, found := tc.Get("tint") 651 | if !found { 652 | t.Error("tint was not found") 653 | } 654 | if x.(int) != 3 { 655 | t.Error("tint is not 3:", x) 656 | } 657 | } 658 | 659 | func TestIncrementInt8(t *testing.T) { 660 | tc := New(DefaultExpiration, 0) 661 | tc.Set("tint8", int8(1), DefaultExpiration) 662 | n, err := tc.IncrementInt8("tint8", 2) 663 | if err != nil { 664 | t.Error("Error incrementing:", err) 665 | } 666 | if n != 3 { 667 | t.Error("Returned number is not 3:", n) 668 | } 669 | x, found := tc.Get("tint8") 670 | if !found { 671 | t.Error("tint8 was not found") 672 | } 673 | if x.(int8) != 3 { 674 | t.Error("tint8 is not 3:", x) 675 | } 676 | } 677 | 678 | func TestIncrementInt16(t *testing.T) { 679 | tc := New(DefaultExpiration, 0) 680 | tc.Set("tint16", int16(1), DefaultExpiration) 681 | n, err := tc.IncrementInt16("tint16", 2) 682 | if err != nil { 683 | t.Error("Error incrementing:", err) 684 | } 685 | if n != 3 { 686 | t.Error("Returned number is not 3:", n) 687 | } 688 | x, found := tc.Get("tint16") 689 | if !found { 690 | t.Error("tint16 was not found") 691 | } 692 | if x.(int16) != 3 { 693 | t.Error("tint16 is not 3:", x) 694 | } 695 | } 696 | 697 | func TestIncrementInt32(t *testing.T) { 698 | tc := New(DefaultExpiration, 0) 699 | tc.Set("tint32", int32(1), DefaultExpiration) 700 | n, err := tc.IncrementInt32("tint32", 2) 701 | if err != nil { 702 | t.Error("Error incrementing:", err) 703 | } 704 | if n != 3 { 705 | t.Error("Returned number is not 3:", n) 706 | } 707 | x, found := tc.Get("tint32") 708 | if !found { 709 | t.Error("tint32 was not found") 710 | } 711 | if x.(int32) != 3 { 712 | t.Error("tint32 is not 3:", x) 713 | } 714 | } 715 | 716 | func TestIncrementInt64(t *testing.T) { 717 | tc := New(DefaultExpiration, 0) 718 | tc.Set("tint64", int64(1), DefaultExpiration) 719 | n, err := tc.IncrementInt64("tint64", 2) 720 | if err != nil { 721 | t.Error("Error incrementing:", err) 722 | } 723 | if n != 3 { 724 | t.Error("Returned number is not 3:", n) 725 | } 726 | x, found := tc.Get("tint64") 727 | if !found { 728 | t.Error("tint64 was not found") 729 | } 730 | if x.(int64) != 3 { 731 | t.Error("tint64 is not 3:", x) 732 | } 733 | } 734 | 735 | func TestIncrementUint(t *testing.T) { 736 | tc := New(DefaultExpiration, 0) 737 | tc.Set("tuint", uint(1), DefaultExpiration) 738 | n, err := tc.IncrementUint("tuint", 2) 739 | if err != nil { 740 | t.Error("Error incrementing:", err) 741 | } 742 | if n != 3 { 743 | t.Error("Returned number is not 3:", n) 744 | } 745 | x, found := tc.Get("tuint") 746 | if !found { 747 | t.Error("tuint was not found") 748 | } 749 | if x.(uint) != 3 { 750 | t.Error("tuint is not 3:", x) 751 | } 752 | } 753 | 754 | func TestIncrementUintptr(t *testing.T) { 755 | tc := New(DefaultExpiration, 0) 756 | tc.Set("tuintptr", uintptr(1), DefaultExpiration) 757 | n, err := tc.IncrementUintptr("tuintptr", 2) 758 | if err != nil { 759 | t.Error("Error incrementing:", err) 760 | } 761 | if n != 3 { 762 | t.Error("Returned number is not 3:", n) 763 | } 764 | x, found := tc.Get("tuintptr") 765 | if !found { 766 | t.Error("tuintptr was not found") 767 | } 768 | if x.(uintptr) != 3 { 769 | t.Error("tuintptr is not 3:", x) 770 | } 771 | } 772 | 773 | func TestIncrementUint8(t *testing.T) { 774 | tc := New(DefaultExpiration, 0) 775 | tc.Set("tuint8", uint8(1), DefaultExpiration) 776 | n, err := tc.IncrementUint8("tuint8", 2) 777 | if err != nil { 778 | t.Error("Error incrementing:", err) 779 | } 780 | if n != 3 { 781 | t.Error("Returned number is not 3:", n) 782 | } 783 | x, found := tc.Get("tuint8") 784 | if !found { 785 | t.Error("tuint8 was not found") 786 | } 787 | if x.(uint8) != 3 { 788 | t.Error("tuint8 is not 3:", x) 789 | } 790 | } 791 | 792 | func TestIncrementUint16(t *testing.T) { 793 | tc := New(DefaultExpiration, 0) 794 | tc.Set("tuint16", uint16(1), DefaultExpiration) 795 | n, err := tc.IncrementUint16("tuint16", 2) 796 | if err != nil { 797 | t.Error("Error incrementing:", err) 798 | } 799 | if n != 3 { 800 | t.Error("Returned number is not 3:", n) 801 | } 802 | x, found := tc.Get("tuint16") 803 | if !found { 804 | t.Error("tuint16 was not found") 805 | } 806 | if x.(uint16) != 3 { 807 | t.Error("tuint16 is not 3:", x) 808 | } 809 | } 810 | 811 | func TestIncrementUint32(t *testing.T) { 812 | tc := New(DefaultExpiration, 0) 813 | tc.Set("tuint32", uint32(1), DefaultExpiration) 814 | n, err := tc.IncrementUint32("tuint32", 2) 815 | if err != nil { 816 | t.Error("Error incrementing:", err) 817 | } 818 | if n != 3 { 819 | t.Error("Returned number is not 3:", n) 820 | } 821 | x, found := tc.Get("tuint32") 822 | if !found { 823 | t.Error("tuint32 was not found") 824 | } 825 | if x.(uint32) != 3 { 826 | t.Error("tuint32 is not 3:", x) 827 | } 828 | } 829 | 830 | func TestIncrementUint64(t *testing.T) { 831 | tc := New(DefaultExpiration, 0) 832 | tc.Set("tuint64", uint64(1), DefaultExpiration) 833 | n, err := tc.IncrementUint64("tuint64", 2) 834 | if err != nil { 835 | t.Error("Error incrementing:", err) 836 | } 837 | if n != 3 { 838 | t.Error("Returned number is not 3:", n) 839 | } 840 | x, found := tc.Get("tuint64") 841 | if !found { 842 | t.Error("tuint64 was not found") 843 | } 844 | if x.(uint64) != 3 { 845 | t.Error("tuint64 is not 3:", x) 846 | } 847 | } 848 | 849 | func TestIncrementFloat32(t *testing.T) { 850 | tc := New(DefaultExpiration, 0) 851 | tc.Set("float32", float32(1.5), DefaultExpiration) 852 | n, err := tc.IncrementFloat32("float32", 2) 853 | if err != nil { 854 | t.Error("Error incrementing:", err) 855 | } 856 | if n != 3.5 { 857 | t.Error("Returned number is not 3.5:", n) 858 | } 859 | x, found := tc.Get("float32") 860 | if !found { 861 | t.Error("float32 was not found") 862 | } 863 | if x.(float32) != 3.5 { 864 | t.Error("float32 is not 3.5:", x) 865 | } 866 | } 867 | 868 | func TestIncrementFloat64(t *testing.T) { 869 | tc := New(DefaultExpiration, 0) 870 | tc.Set("float64", float64(1.5), DefaultExpiration) 871 | n, err := tc.IncrementFloat64("float64", 2) 872 | if err != nil { 873 | t.Error("Error incrementing:", err) 874 | } 875 | if n != 3.5 { 876 | t.Error("Returned number is not 3.5:", n) 877 | } 878 | x, found := tc.Get("float64") 879 | if !found { 880 | t.Error("float64 was not found") 881 | } 882 | if x.(float64) != 3.5 { 883 | t.Error("float64 is not 3.5:", x) 884 | } 885 | } 886 | 887 | func TestDecrementInt8(t *testing.T) { 888 | tc := New(DefaultExpiration, 0) 889 | tc.Set("int8", int8(5), DefaultExpiration) 890 | n, err := tc.DecrementInt8("int8", 2) 891 | if err != nil { 892 | t.Error("Error decrementing:", err) 893 | } 894 | if n != 3 { 895 | t.Error("Returned number is not 3:", n) 896 | } 897 | x, found := tc.Get("int8") 898 | if !found { 899 | t.Error("int8 was not found") 900 | } 901 | if x.(int8) != 3 { 902 | t.Error("int8 is not 3:", x) 903 | } 904 | } 905 | 906 | func TestDecrementInt16(t *testing.T) { 907 | tc := New(DefaultExpiration, 0) 908 | tc.Set("int16", int16(5), DefaultExpiration) 909 | n, err := tc.DecrementInt16("int16", 2) 910 | if err != nil { 911 | t.Error("Error decrementing:", err) 912 | } 913 | if n != 3 { 914 | t.Error("Returned number is not 3:", n) 915 | } 916 | x, found := tc.Get("int16") 917 | if !found { 918 | t.Error("int16 was not found") 919 | } 920 | if x.(int16) != 3 { 921 | t.Error("int16 is not 3:", x) 922 | } 923 | } 924 | 925 | func TestDecrementInt32(t *testing.T) { 926 | tc := New(DefaultExpiration, 0) 927 | tc.Set("int32", int32(5), DefaultExpiration) 928 | n, err := tc.DecrementInt32("int32", 2) 929 | if err != nil { 930 | t.Error("Error decrementing:", err) 931 | } 932 | if n != 3 { 933 | t.Error("Returned number is not 3:", n) 934 | } 935 | x, found := tc.Get("int32") 936 | if !found { 937 | t.Error("int32 was not found") 938 | } 939 | if x.(int32) != 3 { 940 | t.Error("int32 is not 3:", x) 941 | } 942 | } 943 | 944 | func TestDecrementInt64(t *testing.T) { 945 | tc := New(DefaultExpiration, 0) 946 | tc.Set("int64", int64(5), DefaultExpiration) 947 | n, err := tc.DecrementInt64("int64", 2) 948 | if err != nil { 949 | t.Error("Error decrementing:", err) 950 | } 951 | if n != 3 { 952 | t.Error("Returned number is not 3:", n) 953 | } 954 | x, found := tc.Get("int64") 955 | if !found { 956 | t.Error("int64 was not found") 957 | } 958 | if x.(int64) != 3 { 959 | t.Error("int64 is not 3:", x) 960 | } 961 | } 962 | 963 | func TestDecrementUint(t *testing.T) { 964 | tc := New(DefaultExpiration, 0) 965 | tc.Set("uint", uint(5), DefaultExpiration) 966 | n, err := tc.DecrementUint("uint", 2) 967 | if err != nil { 968 | t.Error("Error decrementing:", err) 969 | } 970 | if n != 3 { 971 | t.Error("Returned number is not 3:", n) 972 | } 973 | x, found := tc.Get("uint") 974 | if !found { 975 | t.Error("uint was not found") 976 | } 977 | if x.(uint) != 3 { 978 | t.Error("uint is not 3:", x) 979 | } 980 | } 981 | 982 | func TestDecrementUintptr(t *testing.T) { 983 | tc := New(DefaultExpiration, 0) 984 | tc.Set("uintptr", uintptr(5), DefaultExpiration) 985 | n, err := tc.DecrementUintptr("uintptr", 2) 986 | if err != nil { 987 | t.Error("Error decrementing:", err) 988 | } 989 | if n != 3 { 990 | t.Error("Returned number is not 3:", n) 991 | } 992 | x, found := tc.Get("uintptr") 993 | if !found { 994 | t.Error("uintptr was not found") 995 | } 996 | if x.(uintptr) != 3 { 997 | t.Error("uintptr is not 3:", x) 998 | } 999 | } 1000 | 1001 | func TestDecrementUint8(t *testing.T) { 1002 | tc := New(DefaultExpiration, 0) 1003 | tc.Set("uint8", uint8(5), DefaultExpiration) 1004 | n, err := tc.DecrementUint8("uint8", 2) 1005 | if err != nil { 1006 | t.Error("Error decrementing:", err) 1007 | } 1008 | if n != 3 { 1009 | t.Error("Returned number is not 3:", n) 1010 | } 1011 | x, found := tc.Get("uint8") 1012 | if !found { 1013 | t.Error("uint8 was not found") 1014 | } 1015 | if x.(uint8) != 3 { 1016 | t.Error("uint8 is not 3:", x) 1017 | } 1018 | } 1019 | 1020 | func TestDecrementUint16(t *testing.T) { 1021 | tc := New(DefaultExpiration, 0) 1022 | tc.Set("uint16", uint16(5), DefaultExpiration) 1023 | n, err := tc.DecrementUint16("uint16", 2) 1024 | if err != nil { 1025 | t.Error("Error decrementing:", err) 1026 | } 1027 | if n != 3 { 1028 | t.Error("Returned number is not 3:", n) 1029 | } 1030 | x, found := tc.Get("uint16") 1031 | if !found { 1032 | t.Error("uint16 was not found") 1033 | } 1034 | if x.(uint16) != 3 { 1035 | t.Error("uint16 is not 3:", x) 1036 | } 1037 | } 1038 | 1039 | func TestDecrementUint32(t *testing.T) { 1040 | tc := New(DefaultExpiration, 0) 1041 | tc.Set("uint32", uint32(5), DefaultExpiration) 1042 | n, err := tc.DecrementUint32("uint32", 2) 1043 | if err != nil { 1044 | t.Error("Error decrementing:", err) 1045 | } 1046 | if n != 3 { 1047 | t.Error("Returned number is not 3:", n) 1048 | } 1049 | x, found := tc.Get("uint32") 1050 | if !found { 1051 | t.Error("uint32 was not found") 1052 | } 1053 | if x.(uint32) != 3 { 1054 | t.Error("uint32 is not 3:", x) 1055 | } 1056 | } 1057 | 1058 | func TestDecrementUint64(t *testing.T) { 1059 | tc := New(DefaultExpiration, 0) 1060 | tc.Set("uint64", uint64(5), DefaultExpiration) 1061 | n, err := tc.DecrementUint64("uint64", 2) 1062 | if err != nil { 1063 | t.Error("Error decrementing:", err) 1064 | } 1065 | if n != 3 { 1066 | t.Error("Returned number is not 3:", n) 1067 | } 1068 | x, found := tc.Get("uint64") 1069 | if !found { 1070 | t.Error("uint64 was not found") 1071 | } 1072 | if x.(uint64) != 3 { 1073 | t.Error("uint64 is not 3:", x) 1074 | } 1075 | } 1076 | 1077 | func TestDecrementFloat32(t *testing.T) { 1078 | tc := New(DefaultExpiration, 0) 1079 | tc.Set("float32", float32(5), DefaultExpiration) 1080 | n, err := tc.DecrementFloat32("float32", 2) 1081 | if err != nil { 1082 | t.Error("Error decrementing:", err) 1083 | } 1084 | if n != 3 { 1085 | t.Error("Returned number is not 3:", n) 1086 | } 1087 | x, found := tc.Get("float32") 1088 | if !found { 1089 | t.Error("float32 was not found") 1090 | } 1091 | if x.(float32) != 3 { 1092 | t.Error("float32 is not 3:", x) 1093 | } 1094 | } 1095 | 1096 | func TestDecrementFloat64(t *testing.T) { 1097 | tc := New(DefaultExpiration, 0) 1098 | tc.Set("float64", float64(5), DefaultExpiration) 1099 | n, err := tc.DecrementFloat64("float64", 2) 1100 | if err != nil { 1101 | t.Error("Error decrementing:", err) 1102 | } 1103 | if n != 3 { 1104 | t.Error("Returned number is not 3:", n) 1105 | } 1106 | x, found := tc.Get("float64") 1107 | if !found { 1108 | t.Error("float64 was not found") 1109 | } 1110 | if x.(float64) != 3 { 1111 | t.Error("float64 is not 3:", x) 1112 | } 1113 | } 1114 | 1115 | func TestAdd(t *testing.T) { 1116 | tc := New(DefaultExpiration, 0) 1117 | err := tc.Add("foo", "bar", DefaultExpiration) 1118 | if err != nil { 1119 | t.Error("Couldn't add foo even though it shouldn't exist") 1120 | } 1121 | err = tc.Add("foo", "baz", DefaultExpiration) 1122 | if err == nil { 1123 | t.Error("Successfully added another foo when it should have returned an error") 1124 | } 1125 | } 1126 | 1127 | func TestReplace(t *testing.T) { 1128 | tc := New(DefaultExpiration, 0) 1129 | err := tc.Replace("foo", "bar", DefaultExpiration) 1130 | if err == nil { 1131 | t.Error("Replaced foo when it shouldn't exist") 1132 | } 1133 | tc.Set("foo", "bar", DefaultExpiration) 1134 | err = tc.Replace("foo", "bar", DefaultExpiration) 1135 | if err != nil { 1136 | t.Error("Couldn't replace existing key foo") 1137 | } 1138 | } 1139 | 1140 | func TestDelete(t *testing.T) { 1141 | tc := New(DefaultExpiration, 0) 1142 | tc.Set("foo", "bar", DefaultExpiration) 1143 | tc.Delete("foo") 1144 | x, found := tc.Get("foo") 1145 | if found { 1146 | t.Error("foo was found, but it should have been deleted") 1147 | } 1148 | if x != nil { 1149 | t.Error("x is not nil:", x) 1150 | } 1151 | } 1152 | 1153 | func TestItemCount(t *testing.T) { 1154 | tc := New(DefaultExpiration, 0) 1155 | tc.Set("foo", "1", DefaultExpiration) 1156 | tc.Set("bar", "2", DefaultExpiration) 1157 | tc.Set("baz", "3", DefaultExpiration) 1158 | if n := tc.ItemCount(); n != 3 { 1159 | t.Errorf("Item count is not 3: %d", n) 1160 | } 1161 | } 1162 | 1163 | func TestFlush(t *testing.T) { 1164 | tc := New(DefaultExpiration, 0) 1165 | tc.Set("foo", "bar", DefaultExpiration) 1166 | tc.Set("baz", "yes", DefaultExpiration) 1167 | tc.Flush() 1168 | x, found := tc.Get("foo") 1169 | if found { 1170 | t.Error("foo was found, but it should have been deleted") 1171 | } 1172 | if x != nil { 1173 | t.Error("x is not nil:", x) 1174 | } 1175 | x, found = tc.Get("baz") 1176 | if found { 1177 | t.Error("baz was found, but it should have been deleted") 1178 | } 1179 | if x != nil { 1180 | t.Error("x is not nil:", x) 1181 | } 1182 | } 1183 | 1184 | func TestIncrementOverflowInt(t *testing.T) { 1185 | tc := New(DefaultExpiration, 0) 1186 | tc.Set("int8", int8(127), DefaultExpiration) 1187 | err := tc.Increment("int8", 1) 1188 | if err != nil { 1189 | t.Error("Error incrementing int8:", err) 1190 | } 1191 | x, _ := tc.Get("int8") 1192 | int8 := x.(int8) 1193 | if int8 != -128 { 1194 | t.Error("int8 did not overflow as expected; value:", int8) 1195 | } 1196 | 1197 | } 1198 | 1199 | func TestIncrementOverflowUint(t *testing.T) { 1200 | tc := New(DefaultExpiration, 0) 1201 | tc.Set("uint8", uint8(255), DefaultExpiration) 1202 | err := tc.Increment("uint8", 1) 1203 | if err != nil { 1204 | t.Error("Error incrementing int8:", err) 1205 | } 1206 | x, _ := tc.Get("uint8") 1207 | uint8 := x.(uint8) 1208 | if uint8 != 0 { 1209 | t.Error("uint8 did not overflow as expected; value:", uint8) 1210 | } 1211 | } 1212 | 1213 | func TestDecrementUnderflowUint(t *testing.T) { 1214 | tc := New(DefaultExpiration, 0) 1215 | tc.Set("uint8", uint8(0), DefaultExpiration) 1216 | err := tc.Decrement("uint8", 1) 1217 | if err != nil { 1218 | t.Error("Error decrementing int8:", err) 1219 | } 1220 | x, _ := tc.Get("uint8") 1221 | uint8 := x.(uint8) 1222 | if uint8 != 255 { 1223 | t.Error("uint8 did not underflow as expected; value:", uint8) 1224 | } 1225 | } 1226 | 1227 | func TestOnEvicted(t *testing.T) { 1228 | tc := New(DefaultExpiration, 0) 1229 | tc.Set("foo", 3, DefaultExpiration) 1230 | if tc.onEvicted != nil { 1231 | t.Fatal("tc.onEvicted is not nil") 1232 | } 1233 | works := false 1234 | tc.OnEvicted(func(k string, v interface{}) { 1235 | if k == "foo" && v.(int) == 3 { 1236 | works = true 1237 | } 1238 | tc.Set("bar", 4, DefaultExpiration) 1239 | }) 1240 | tc.Delete("foo") 1241 | x, _ := tc.Get("bar") 1242 | if !works { 1243 | t.Error("works bool not true") 1244 | } 1245 | if x.(int) != 4 { 1246 | t.Error("bar was not 4") 1247 | } 1248 | } 1249 | 1250 | func TestCacheSerialization(t *testing.T) { 1251 | tc := New(DefaultExpiration, 0) 1252 | testFillAndSerialize(t, tc) 1253 | 1254 | // Check if gob.Register behaves properly even after multiple gob.Register 1255 | // on c.Items (many of which will be the same type) 1256 | testFillAndSerialize(t, tc) 1257 | } 1258 | 1259 | func testFillAndSerialize(t *testing.T, tc *Cache) { 1260 | tc.Set("a", "a", DefaultExpiration) 1261 | tc.Set("b", "b", DefaultExpiration) 1262 | tc.Set("c", "c", DefaultExpiration) 1263 | tc.Set("expired", "foo", 1*time.Millisecond) 1264 | tc.Set("*struct", &TestStruct{Num: 1}, DefaultExpiration) 1265 | tc.Set("[]struct", []TestStruct{ 1266 | {Num: 2}, 1267 | {Num: 3}, 1268 | }, DefaultExpiration) 1269 | tc.Set("[]*struct", []*TestStruct{ 1270 | &TestStruct{Num: 4}, 1271 | &TestStruct{Num: 5}, 1272 | }, DefaultExpiration) 1273 | tc.Set("structception", &TestStruct{ 1274 | Num: 42, 1275 | Children: []*TestStruct{ 1276 | &TestStruct{Num: 6174}, 1277 | &TestStruct{Num: 4716}, 1278 | }, 1279 | }, DefaultExpiration) 1280 | 1281 | fp := &bytes.Buffer{} 1282 | err := tc.Save(fp) 1283 | if err != nil { 1284 | t.Fatal("Couldn't save cache to fp:", err) 1285 | } 1286 | 1287 | oc := New(DefaultExpiration, 0) 1288 | err = oc.Load(fp) 1289 | if err != nil { 1290 | t.Fatal("Couldn't load cache from fp:", err) 1291 | } 1292 | 1293 | a, found := oc.Get("a") 1294 | if !found { 1295 | t.Error("a was not found") 1296 | } 1297 | if a.(string) != "a" { 1298 | t.Error("a is not a") 1299 | } 1300 | 1301 | b, found := oc.Get("b") 1302 | if !found { 1303 | t.Error("b was not found") 1304 | } 1305 | if b.(string) != "b" { 1306 | t.Error("b is not b") 1307 | } 1308 | 1309 | c, found := oc.Get("c") 1310 | if !found { 1311 | t.Error("c was not found") 1312 | } 1313 | if c.(string) != "c" { 1314 | t.Error("c is not c") 1315 | } 1316 | 1317 | <-time.After(5 * time.Millisecond) 1318 | _, found = oc.Get("expired") 1319 | if found { 1320 | t.Error("expired was found") 1321 | } 1322 | 1323 | s1, found := oc.Get("*struct") 1324 | if !found { 1325 | t.Error("*struct was not found") 1326 | } 1327 | if s1.(*TestStruct).Num != 1 { 1328 | t.Error("*struct.Num is not 1") 1329 | } 1330 | 1331 | s2, found := oc.Get("[]struct") 1332 | if !found { 1333 | t.Error("[]struct was not found") 1334 | } 1335 | s2r := s2.([]TestStruct) 1336 | if len(s2r) != 2 { 1337 | t.Error("Length of s2r is not 2") 1338 | } 1339 | if s2r[0].Num != 2 { 1340 | t.Error("s2r[0].Num is not 2") 1341 | } 1342 | if s2r[1].Num != 3 { 1343 | t.Error("s2r[1].Num is not 3") 1344 | } 1345 | 1346 | s3, found := oc.get("[]*struct") 1347 | if !found { 1348 | t.Error("[]*struct was not found") 1349 | } 1350 | s3r := s3.([]*TestStruct) 1351 | if len(s3r) != 2 { 1352 | t.Error("Length of s3r is not 2") 1353 | } 1354 | if s3r[0].Num != 4 { 1355 | t.Error("s3r[0].Num is not 4") 1356 | } 1357 | if s3r[1].Num != 5 { 1358 | t.Error("s3r[1].Num is not 5") 1359 | } 1360 | 1361 | s4, found := oc.get("structception") 1362 | if !found { 1363 | t.Error("structception was not found") 1364 | } 1365 | s4r := s4.(*TestStruct) 1366 | if len(s4r.Children) != 2 { 1367 | t.Error("Length of s4r.Children is not 2") 1368 | } 1369 | if s4r.Children[0].Num != 6174 { 1370 | t.Error("s4r.Children[0].Num is not 6174") 1371 | } 1372 | if s4r.Children[1].Num != 4716 { 1373 | t.Error("s4r.Children[1].Num is not 4716") 1374 | } 1375 | } 1376 | 1377 | func TestFileSerialization(t *testing.T) { 1378 | tc := New(DefaultExpiration, 0) 1379 | tc.Add("a", "a", DefaultExpiration) 1380 | tc.Add("b", "b", DefaultExpiration) 1381 | f, err := ioutil.TempFile("", "go-cache-cache.dat") 1382 | if err != nil { 1383 | t.Fatal("Couldn't create cache file:", err) 1384 | } 1385 | fname := f.Name() 1386 | f.Close() 1387 | tc.SaveFile(fname) 1388 | 1389 | oc := New(DefaultExpiration, 0) 1390 | oc.Add("a", "aa", 0) // this should not be overwritten 1391 | err = oc.LoadFile(fname) 1392 | if err != nil { 1393 | t.Error(err) 1394 | } 1395 | a, found := oc.Get("a") 1396 | if !found { 1397 | t.Error("a was not found") 1398 | } 1399 | astr := a.(string) 1400 | if astr != "aa" { 1401 | if astr == "a" { 1402 | t.Error("a was overwritten") 1403 | } else { 1404 | t.Error("a is not aa") 1405 | } 1406 | } 1407 | b, found := oc.Get("b") 1408 | if !found { 1409 | t.Error("b was not found") 1410 | } 1411 | if b.(string) != "b" { 1412 | t.Error("b is not b") 1413 | } 1414 | } 1415 | 1416 | func TestSerializeUnserializable(t *testing.T) { 1417 | tc := New(DefaultExpiration, 0) 1418 | ch := make(chan bool, 1) 1419 | ch <- true 1420 | tc.Set("chan", ch, DefaultExpiration) 1421 | fp := &bytes.Buffer{} 1422 | err := tc.Save(fp) // this should fail gracefully 1423 | if err.Error() != "gob NewTypeObject can't handle type: chan bool" { 1424 | t.Error("Error from Save was not gob NewTypeObject can't handle type chan bool:", err) 1425 | } 1426 | } 1427 | 1428 | func BenchmarkCacheGetExpiring(b *testing.B) { 1429 | benchmarkCacheGet(b, 5*time.Minute) 1430 | } 1431 | 1432 | func BenchmarkCacheGetNotExpiring(b *testing.B) { 1433 | benchmarkCacheGet(b, NoExpiration) 1434 | } 1435 | 1436 | func benchmarkCacheGet(b *testing.B, exp time.Duration) { 1437 | b.StopTimer() 1438 | tc := New(exp, 0) 1439 | tc.Set("foo", "bar", DefaultExpiration) 1440 | b.StartTimer() 1441 | for i := 0; i < b.N; i++ { 1442 | tc.Get("foo") 1443 | } 1444 | } 1445 | 1446 | func BenchmarkRWMutexMapGet(b *testing.B) { 1447 | b.StopTimer() 1448 | m := map[string]string{ 1449 | "foo": "bar", 1450 | } 1451 | mu := sync.RWMutex{} 1452 | b.StartTimer() 1453 | for i := 0; i < b.N; i++ { 1454 | mu.RLock() 1455 | _, _ = m["foo"] 1456 | mu.RUnlock() 1457 | } 1458 | } 1459 | 1460 | func BenchmarkRWMutexInterfaceMapGetStruct(b *testing.B) { 1461 | b.StopTimer() 1462 | s := struct{ name string }{name: "foo"} 1463 | m := map[interface{}]string{ 1464 | s: "bar", 1465 | } 1466 | mu := sync.RWMutex{} 1467 | b.StartTimer() 1468 | for i := 0; i < b.N; i++ { 1469 | mu.RLock() 1470 | _, _ = m[s] 1471 | mu.RUnlock() 1472 | } 1473 | } 1474 | 1475 | func BenchmarkRWMutexInterfaceMapGetString(b *testing.B) { 1476 | b.StopTimer() 1477 | m := map[interface{}]string{ 1478 | "foo": "bar", 1479 | } 1480 | mu := sync.RWMutex{} 1481 | b.StartTimer() 1482 | for i := 0; i < b.N; i++ { 1483 | mu.RLock() 1484 | _, _ = m["foo"] 1485 | mu.RUnlock() 1486 | } 1487 | } 1488 | 1489 | func BenchmarkCacheGetConcurrentExpiring(b *testing.B) { 1490 | benchmarkCacheGetConcurrent(b, 5*time.Minute) 1491 | } 1492 | 1493 | func BenchmarkCacheGetConcurrentNotExpiring(b *testing.B) { 1494 | benchmarkCacheGetConcurrent(b, NoExpiration) 1495 | } 1496 | 1497 | func benchmarkCacheGetConcurrent(b *testing.B, exp time.Duration) { 1498 | b.StopTimer() 1499 | tc := New(exp, 0) 1500 | tc.Set("foo", "bar", DefaultExpiration) 1501 | wg := new(sync.WaitGroup) 1502 | workers := runtime.NumCPU() 1503 | each := b.N / workers 1504 | wg.Add(workers) 1505 | b.StartTimer() 1506 | for i := 0; i < workers; i++ { 1507 | go func() { 1508 | for j := 0; j < each; j++ { 1509 | tc.Get("foo") 1510 | } 1511 | wg.Done() 1512 | }() 1513 | } 1514 | wg.Wait() 1515 | } 1516 | 1517 | func BenchmarkRWMutexMapGetConcurrent(b *testing.B) { 1518 | b.StopTimer() 1519 | m := map[string]string{ 1520 | "foo": "bar", 1521 | } 1522 | mu := sync.RWMutex{} 1523 | wg := new(sync.WaitGroup) 1524 | workers := runtime.NumCPU() 1525 | each := b.N / workers 1526 | wg.Add(workers) 1527 | b.StartTimer() 1528 | for i := 0; i < workers; i++ { 1529 | go func() { 1530 | for j := 0; j < each; j++ { 1531 | mu.RLock() 1532 | _, _ = m["foo"] 1533 | mu.RUnlock() 1534 | } 1535 | wg.Done() 1536 | }() 1537 | } 1538 | wg.Wait() 1539 | } 1540 | 1541 | func BenchmarkCacheGetManyConcurrentExpiring(b *testing.B) { 1542 | benchmarkCacheGetManyConcurrent(b, 5*time.Minute) 1543 | } 1544 | 1545 | func BenchmarkCacheGetManyConcurrentNotExpiring(b *testing.B) { 1546 | benchmarkCacheGetManyConcurrent(b, NoExpiration) 1547 | } 1548 | 1549 | func benchmarkCacheGetManyConcurrent(b *testing.B, exp time.Duration) { 1550 | // This is the same as BenchmarkCacheGetConcurrent, but its result 1551 | // can be compared against BenchmarkShardedCacheGetManyConcurrent 1552 | // in sharded_test.go. 1553 | b.StopTimer() 1554 | n := 10000 1555 | tc := New(exp, 0) 1556 | keys := make([]string, n) 1557 | for i := 0; i < n; i++ { 1558 | k := "foo" + strconv.Itoa(i) 1559 | keys[i] = k 1560 | tc.Set(k, "bar", DefaultExpiration) 1561 | } 1562 | each := b.N / n 1563 | wg := new(sync.WaitGroup) 1564 | wg.Add(n) 1565 | for _, v := range keys { 1566 | go func(k string) { 1567 | for j := 0; j < each; j++ { 1568 | tc.Get(k) 1569 | } 1570 | wg.Done() 1571 | }(v) 1572 | } 1573 | b.StartTimer() 1574 | wg.Wait() 1575 | } 1576 | 1577 | func BenchmarkCacheSetExpiring(b *testing.B) { 1578 | benchmarkCacheSet(b, 5*time.Minute) 1579 | } 1580 | 1581 | func BenchmarkCacheSetNotExpiring(b *testing.B) { 1582 | benchmarkCacheSet(b, NoExpiration) 1583 | } 1584 | 1585 | func benchmarkCacheSet(b *testing.B, exp time.Duration) { 1586 | b.StopTimer() 1587 | tc := New(exp, 0) 1588 | b.StartTimer() 1589 | for i := 0; i < b.N; i++ { 1590 | tc.Set("foo", "bar", DefaultExpiration) 1591 | } 1592 | } 1593 | 1594 | func BenchmarkRWMutexMapSet(b *testing.B) { 1595 | b.StopTimer() 1596 | m := map[string]string{} 1597 | mu := sync.RWMutex{} 1598 | b.StartTimer() 1599 | for i := 0; i < b.N; i++ { 1600 | mu.Lock() 1601 | m["foo"] = "bar" 1602 | mu.Unlock() 1603 | } 1604 | } 1605 | 1606 | func BenchmarkCacheSetDelete(b *testing.B) { 1607 | b.StopTimer() 1608 | tc := New(DefaultExpiration, 0) 1609 | b.StartTimer() 1610 | for i := 0; i < b.N; i++ { 1611 | tc.Set("foo", "bar", DefaultExpiration) 1612 | tc.Delete("foo") 1613 | } 1614 | } 1615 | 1616 | func BenchmarkRWMutexMapSetDelete(b *testing.B) { 1617 | b.StopTimer() 1618 | m := map[string]string{} 1619 | mu := sync.RWMutex{} 1620 | b.StartTimer() 1621 | for i := 0; i < b.N; i++ { 1622 | mu.Lock() 1623 | m["foo"] = "bar" 1624 | mu.Unlock() 1625 | mu.Lock() 1626 | delete(m, "foo") 1627 | mu.Unlock() 1628 | } 1629 | } 1630 | 1631 | func BenchmarkCacheSetDeleteSingleLock(b *testing.B) { 1632 | b.StopTimer() 1633 | tc := New(DefaultExpiration, 0) 1634 | b.StartTimer() 1635 | for i := 0; i < b.N; i++ { 1636 | tc.mu.Lock() 1637 | tc.set("foo", "bar", DefaultExpiration) 1638 | tc.delete("foo") 1639 | tc.mu.Unlock() 1640 | } 1641 | } 1642 | 1643 | func BenchmarkRWMutexMapSetDeleteSingleLock(b *testing.B) { 1644 | b.StopTimer() 1645 | m := map[string]string{} 1646 | mu := sync.RWMutex{} 1647 | b.StartTimer() 1648 | for i := 0; i < b.N; i++ { 1649 | mu.Lock() 1650 | m["foo"] = "bar" 1651 | delete(m, "foo") 1652 | mu.Unlock() 1653 | } 1654 | } 1655 | 1656 | func BenchmarkIncrementInt(b *testing.B) { 1657 | b.StopTimer() 1658 | tc := New(DefaultExpiration, 0) 1659 | tc.Set("foo", 0, DefaultExpiration) 1660 | b.StartTimer() 1661 | for i := 0; i < b.N; i++ { 1662 | tc.IncrementInt("foo", 1) 1663 | } 1664 | } 1665 | 1666 | func BenchmarkDeleteExpiredLoop(b *testing.B) { 1667 | b.StopTimer() 1668 | tc := New(5*time.Minute, 0) 1669 | tc.mu.Lock() 1670 | for i := 0; i < 100000; i++ { 1671 | tc.set(strconv.Itoa(i), "bar", DefaultExpiration) 1672 | } 1673 | tc.mu.Unlock() 1674 | b.StartTimer() 1675 | for i := 0; i < b.N; i++ { 1676 | tc.DeleteExpired() 1677 | } 1678 | } 1679 | 1680 | func TestGetWithExpiration(t *testing.T) { 1681 | tc := New(DefaultExpiration, 0) 1682 | 1683 | a, expiration, found := tc.GetWithExpiration("a") 1684 | if found || a != nil || !expiration.IsZero() { 1685 | t.Error("Getting A found value that shouldn't exist:", a) 1686 | } 1687 | 1688 | b, expiration, found := tc.GetWithExpiration("b") 1689 | if found || b != nil || !expiration.IsZero() { 1690 | t.Error("Getting B found value that shouldn't exist:", b) 1691 | } 1692 | 1693 | c, expiration, found := tc.GetWithExpiration("c") 1694 | if found || c != nil || !expiration.IsZero() { 1695 | t.Error("Getting C found value that shouldn't exist:", c) 1696 | } 1697 | 1698 | tc.Set("a", 1, DefaultExpiration) 1699 | tc.Set("b", "b", DefaultExpiration) 1700 | tc.Set("c", 3.5, DefaultExpiration) 1701 | tc.Set("d", 1, NoExpiration) 1702 | tc.Set("e", 1, 50*time.Millisecond) 1703 | 1704 | x, expiration, found := tc.GetWithExpiration("a") 1705 | if !found { 1706 | t.Error("a was not found while getting a2") 1707 | } 1708 | if x == nil { 1709 | t.Error("x for a is nil") 1710 | } else if a2 := x.(int); a2+2 != 3 { 1711 | t.Error("a2 (which should be 1) plus 2 does not equal 3; value:", a2) 1712 | } 1713 | if !expiration.IsZero() { 1714 | t.Error("expiration for a is not a zeroed time") 1715 | } 1716 | 1717 | x, expiration, found = tc.GetWithExpiration("b") 1718 | if !found { 1719 | t.Error("b was not found while getting b2") 1720 | } 1721 | if x == nil { 1722 | t.Error("x for b is nil") 1723 | } else if b2 := x.(string); b2+"B" != "bB" { 1724 | t.Error("b2 (which should be b) plus B does not equal bB; value:", b2) 1725 | } 1726 | if !expiration.IsZero() { 1727 | t.Error("expiration for b is not a zeroed time") 1728 | } 1729 | 1730 | x, expiration, found = tc.GetWithExpiration("c") 1731 | if !found { 1732 | t.Error("c was not found while getting c2") 1733 | } 1734 | if x == nil { 1735 | t.Error("x for c is nil") 1736 | } else if c2 := x.(float64); c2+1.2 != 4.7 { 1737 | t.Error("c2 (which should be 3.5) plus 1.2 does not equal 4.7; value:", c2) 1738 | } 1739 | if !expiration.IsZero() { 1740 | t.Error("expiration for c is not a zeroed time") 1741 | } 1742 | 1743 | x, expiration, found = tc.GetWithExpiration("d") 1744 | if !found { 1745 | t.Error("d was not found while getting d2") 1746 | } 1747 | if x == nil { 1748 | t.Error("x for d is nil") 1749 | } else if d2 := x.(int); d2+2 != 3 { 1750 | t.Error("d (which should be 1) plus 2 does not equal 3; value:", d2) 1751 | } 1752 | if !expiration.IsZero() { 1753 | t.Error("expiration for d is not a zeroed time") 1754 | } 1755 | 1756 | x, expiration, found = tc.GetWithExpiration("e") 1757 | if !found { 1758 | t.Error("e was not found while getting e2") 1759 | } 1760 | if x == nil { 1761 | t.Error("x for e is nil") 1762 | } else if e2 := x.(int); e2+2 != 3 { 1763 | t.Error("e (which should be 1) plus 2 does not equal 3; value:", e2) 1764 | } 1765 | if expiration.UnixNano() != tc.items["e"].Expiration { 1766 | t.Error("expiration for e is not the correct time") 1767 | } 1768 | if expiration.UnixNano() < time.Now().UnixNano() { 1769 | t.Error("expiration for e is in the past") 1770 | } 1771 | } 1772 | -------------------------------------------------------------------------------- /cli/.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example goreleaser.yaml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | # Run locally with: goreleaser --rm-dist --snapshot --skip-publish 4 | project_name: tiles 5 | before: 6 | hooks: 7 | - go mod tidy 8 | - go mod download 9 | builds: 10 | - binary: '{{ .ProjectName }}' 11 | main: ./main.go 12 | env: 13 | - CGO_ENABLED=0 14 | ldflags: 15 | - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} 16 | - -a -extldflags "-static" 17 | goos: 18 | - windows 19 | - linux 20 | - darwin 21 | goarch: 22 | - amd64 23 | archives: 24 | - replacements: 25 | darwin: macOS 26 | windows: win 27 | amd64: 64-bit 28 | checksum: 29 | name_template: 'checksums.txt' 30 | snapshot: 31 | name_template: "{{ .ProjectName }}_{{ .Tag }}" 32 | nfpms: 33 | - 34 | package_name: tiles 35 | vendor: Luca Sepe 36 | homepage: https://lucasepe.it/ 37 | maintainer: Luca Sepe 38 | description: Commandline tool that makes building tilesets and rendering static tilemaps super easy! 39 | license: MIT 40 | replacements: 41 | amd64: 64-bit 42 | formats: 43 | - deb 44 | - rpm 45 | changelog: 46 | sort: asc 47 | filters: 48 | exclude: 49 | - '^docs:' 50 | - '^test:' 51 | -------------------------------------------------------------------------------- /cli/cmd/compose.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/lucasepe/tiles/composer" 8 | "github.com/lucasepe/tiles/imagelist" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // composeCmd represents the compose command 13 | var composeCmd = &cobra.Command{ 14 | DisableSuggestions: true, 15 | DisableFlagsInUseLine: true, 16 | Args: cobra.MinimumNArgs(1), 17 | Use: "compose ", 18 | Short: "Generate a tileset from all PNG images in the specified directory", 19 | Example: composeCmdExample(), 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | images, err := imagelist.Load(args[0]) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | return composer.Do(images, os.Stdout) 27 | }, 28 | } 29 | 30 | func init() { 31 | rootCmd.AddCommand(composeCmd) 32 | } 33 | 34 | func composeCmdExample() string { 35 | tpl := ` {{APP}} compose /path/to/png/images/ > my_tileset.yml` 36 | return strings.Replace(tpl, "{{APP}}", appName(), -1) 37 | } 38 | -------------------------------------------------------------------------------- /cli/cmd/list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/lucasepe/tiles/tileset" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // listCmd represents the list command 13 | var listCmd = &cobra.Command{ 14 | DisableSuggestions: true, 15 | DisableFlagsInUseLine: true, 16 | Args: cobra.MinimumNArgs(1), 17 | Use: "list ", 18 | Short: "Lists all tiles identifiers contained in the specified tilset", 19 | Example: listCmdExample(), 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | sets, err := tileset.Load(args[0]) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | for _, ts := range sets { 27 | for _, el := range ts.Tiles { 28 | if _, err := fmt.Fprintln(os.Stdout, el.ID); err != nil { 29 | return err 30 | } 31 | } 32 | } 33 | 34 | return nil 35 | }, 36 | } 37 | 38 | func init() { 39 | rootCmd.AddCommand(listCmd) 40 | } 41 | 42 | func listCmdExample() string { 43 | tpl := ` {{APP}} list https://github.com/lucasepe/tiles/TBD 44 | {{APP}} list /path/to/tileset.yml` 45 | 46 | return strings.Replace(tpl, "{{APP}}", appName(), -1) 47 | } 48 | -------------------------------------------------------------------------------- /cli/cmd/pull.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "image/png" 6 | "os" 7 | "strings" 8 | 9 | "github.com/lucasepe/tiles/tileset" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | // pullCmd represents the pull command 14 | var pullCmd = &cobra.Command{ 15 | DisableSuggestions: true, 16 | DisableFlagsInUseLine: true, 17 | Args: cobra.MinimumNArgs(1), 18 | Use: "pull ", 19 | Example: pullCmdExample(), 20 | Short: "Extracts the tile with the specified identifier from the tileset", 21 | RunE: func(cmd *cobra.Command, args []string) error { 22 | id, err := cmd.Flags().GetString(optID) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | ts, err := tileset.Load(args[0]) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | tile, ok := ts[0].Get(id) 33 | if !ok { 34 | return fmt.Errorf("tile with id = %s not found", id) 35 | } 36 | 37 | img, err := ts[0].Image(tile) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | // TODO save image to stdout 43 | enc := png.Encoder{ 44 | CompressionLevel: png.BestSpeed, 45 | } 46 | 47 | return enc.Encode(os.Stdout, img) 48 | }, 49 | } 50 | 51 | func init() { 52 | pullCmd.Flags().String(optID, "", "the tile identifier in the specified tileset") 53 | pullCmd.MarkFlagRequired(optID) 54 | 55 | rootCmd.AddCommand(pullCmd) 56 | } 57 | 58 | func pullCmdExample() string { 59 | tpl := ` {{APP}} pull --id aws_waf ../examples/aws_tileset.yml 60 | {{APP}} pull --id aws_waf ../examples/aws_tileset.yml > aws_waf.png 61 | {{APP}} pull --id aws_waf ../examples/aws_tileset.yml | viu -` 62 | 63 | return strings.Replace(tpl, "{{APP}}", appName(), -1) 64 | } 65 | -------------------------------------------------------------------------------- /cli/cmd/render.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/lucasepe/tiles/tilemap" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // renderCmd represents the render command 12 | var renderCmd = &cobra.Command{ 13 | DisableSuggestions: true, 14 | DisableFlagsInUseLine: true, 15 | Args: cobra.MinimumNArgs(1), 16 | Use: "render ", 17 | Short: "Render a square static tilemap", 18 | Example: renderCmdExample(), 19 | RunE: func(cmd *cobra.Command, args []string) error { 20 | tm, err := tilemap.Load(args[0]) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | return tm.Render(os.Stdout) 26 | }, 27 | } 28 | 29 | func init() { 30 | rootCmd.AddCommand(renderCmd) 31 | } 32 | 33 | func renderCmdExample() string { 34 | tpl := ` {{APP}} render https://github.com/lucasepe/tiles/examples/ark.yml 35 | {{APP}} render /path/to/my_map.yml 36 | {{APP}} render /path/to/my_map.yml | viu -` 37 | 38 | return strings.Replace(tpl, "{{APP}}", appName(), -1) 39 | } 40 | -------------------------------------------------------------------------------- /cli/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | const ( 12 | banner = ` 13 | ______ ____ _ ___ _____ 14 | | || || | / _] / ___/ 15 | | | | | | | / [_ ( \_ 16 | |_| |_| | | | |___ | _] \__ | 17 | | | | | | || [_ / \ | 18 | | | | | | || | \ | 19 | |__| |____||_____||_____| \___| ` 20 | 21 | appSummary = "Create and inspect a tile set from multiple PNG images." 22 | 23 | optID = "id" 24 | ) 25 | 26 | // rootCmd represents the base command when called without any subcommands 27 | var ( 28 | version = "dev" 29 | rootCmd = &cobra.Command{ 30 | DisableSuggestions: true, 31 | DisableFlagsInUseLine: true, 32 | Version: version, 33 | Use: fmt.Sprintf("%s ", appName()), 34 | Short: appSummary, 35 | Long: banner, 36 | } 37 | ) 38 | 39 | // Execute adds all child commands to the root command and sets flags appropriately. 40 | // This is called by main.main(). It only needs to happen once to the rootCmd. 41 | func Execute() { 42 | if err := rootCmd.Execute(); err != nil { 43 | fmt.Println(err) 44 | os.Exit(1) 45 | } 46 | } 47 | 48 | func init() { 49 | rootCmd.SetVersionTemplate(`{{with .Name}}{{printf "%s " .}}{{end}}{{printf "%s" .Version}} - Luca Sepe 50 | `) 51 | } 52 | 53 | func appName() string { 54 | return filepath.Base(os.Args[0]) 55 | } 56 | -------------------------------------------------------------------------------- /cli/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 Luca Sepe 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in 10 | all copies or substantial portions of the Software. 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 17 | THE SOFTWARE. 18 | */ 19 | package main 20 | 21 | import "github.com/lucasepe/tiles/cli/cmd" 22 | 23 | func main() { 24 | cmd.Execute() 25 | } 26 | -------------------------------------------------------------------------------- /composer/composer.go: -------------------------------------------------------------------------------- 1 | package composer 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "image" 7 | "image/draw" 8 | "image/png" 9 | "io" 10 | "os" 11 | "path/filepath" 12 | "sort" 13 | "strings" 14 | 15 | "github.com/lucasepe/tiles/binpack" 16 | "github.com/lucasepe/tiles/data" 17 | "github.com/lucasepe/tiles/tileset" 18 | "github.com/pkg/errors" 19 | "gopkg.in/yaml.v2" 20 | ) 21 | 22 | // Do generates a tileset from the image 23 | // list and print the result to the specified writer. 24 | func Do(il []string, wr io.Writer) error { 25 | items, err := decodeImageList(il) 26 | if err != nil { 27 | return err 28 | } 29 | sort.Sort(byMaxOfWidthAndHeight(items)) 30 | 31 | bl := blockList{blocks: items} 32 | bl.width, bl.height = binpack.Pack(&bl) 33 | 34 | if err := bl.createPNG(); err != nil { 35 | return err 36 | } 37 | 38 | return bl.dump(wr) 39 | } 40 | 41 | // block holds tile position, 42 | // dimensions and source image. 43 | type block struct { 44 | src string 45 | x, y int 46 | w, h int 47 | id string 48 | } 49 | 50 | type blockList struct { 51 | blocks []*block 52 | width, height int 53 | data []byte 54 | } 55 | 56 | // createImage assembles all tiles in a 57 | // bigger bin-packed image. 58 | func (bl *blockList) createPNG() error { 59 | sheet := image.NewRGBA(image.Rect(0, 0, bl.width, bl.height)) 60 | 61 | for _, el := range bl.blocks { 62 | fp, err := os.Open(el.src) 63 | if err != nil { 64 | return err 65 | } 66 | defer fp.Close() 67 | 68 | img, _, err := image.Decode(fp) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | r := image.Rect(el.x, el.y, el.x+el.w, el.y+el.h) 74 | draw.Draw(sheet, r, img, image.Point{}, draw.Over) 75 | } 76 | 77 | buf := new(bytes.Buffer) 78 | enc := png.Encoder{CompressionLevel: png.BestCompression} 79 | if err := enc.Encode(buf, sheet); err != nil { 80 | return err 81 | } 82 | 83 | bl.data = buf.Bytes() 84 | 85 | return nil 86 | } 87 | 88 | func (bl *blockList) dump(wr io.Writer) error { 89 | 90 | res := tileset.Tileset{ 91 | Width: bl.width, 92 | Height: bl.height, 93 | Tiles: make([]*tileset.Tile, len(bl.blocks)), 94 | Data: data.Wrap(base64.StdEncoding.EncodeToString(bl.data), 76), 95 | } 96 | 97 | for i, el := range bl.blocks { 98 | res.Tiles[i] = &tileset.Tile{ 99 | MinX: el.x, MinY: el.y, 100 | MaxX: el.x + el.w, MaxY: el.y + el.h, 101 | ID: el.id, 102 | } 103 | } 104 | 105 | dat, err := yaml.Marshal(&res) 106 | if err != nil { 107 | return err 108 | } 109 | _, err = wr.Write(dat) 110 | return err 111 | } 112 | 113 | // Len returns the number of blocks in total. 114 | func (bl *blockList) Len() int { 115 | return len(bl.blocks) 116 | } 117 | 118 | // Size returns the width and height of the block n. 119 | func (bl *blockList) Size(n int) (width, height int) { 120 | el := bl.blocks[n] 121 | return el.w, el.h 122 | } 123 | 124 | // Place places the block n, at the position [x, y]. 125 | func (bl *blockList) Place(n, x, y int) { 126 | el := bl.blocks[n] 127 | el.x = x 128 | el.y = y 129 | } 130 | 131 | // byMaxOfWidthAndHeight implements sort.Interface based on the max(width, height). 132 | type byMaxOfWidthAndHeight []*block 133 | 134 | func (a byMaxOfWidthAndHeight) Len() int { return len(a) } 135 | func (a byMaxOfWidthAndHeight) Less(i, j int) bool { 136 | m1 := maxInt(a[i].w, a[i].h) 137 | m2 := maxInt(a[j].w, a[j].h) 138 | return m1 < m2 139 | } 140 | func (a byMaxOfWidthAndHeight) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 141 | 142 | // decodeImageList load images from the specified list 143 | // and decodes the dimensions of each image. 144 | // Returns a map which keys are the source image file 145 | // and values are tiles with derived ID and decoded dimensions. 146 | func decodeImageList(list []string) ([]*block, error) { 147 | makeID := func(filename string) string { 148 | base := filepath.Base(filename) 149 | ext := filepath.Ext(filename) 150 | return strings.TrimSuffix(base, ext) 151 | } 152 | 153 | res := []*block{} 154 | 155 | for _, el := range list { 156 | r, err := os.Open(el) 157 | if err != nil { 158 | return nil, err 159 | } 160 | defer r.Close() 161 | 162 | im, _, err := image.DecodeConfig(r) 163 | if err != nil { 164 | return nil, errors.Wrapf(err, "image <%s>", el) 165 | } 166 | 167 | res = append(res, &block{ 168 | id: makeID(el), 169 | src: el, 170 | w: im.Width, 171 | h: im.Height, 172 | }) 173 | } 174 | 175 | return res, nil 176 | } 177 | 178 | func maxInt(a, b int) int { 179 | if b > a { 180 | return b 181 | } 182 | return a 183 | } 184 | -------------------------------------------------------------------------------- /data/data.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "net/http" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | // Fetch gets the bytes at the specified URI. 12 | // The URI can be remote (http) or local. 13 | // if 'limit' is greater then zero, fetch stops 14 | // with EOF after 'limit' bytes. 15 | func Fetch(uri string, limit int64) ([]byte, error) { 16 | if strings.HasPrefix(uri, "http") { 17 | return FetchFromURI(uri, limit) 18 | } 19 | 20 | return FetchFromFile(uri, limit) 21 | } 22 | 23 | // FetchFromURI fetch data (with limit) from an HTTP URL. 24 | // if 'limit' is greater then zero, fetch stops 25 | // with EOF after 'limit' bytes. 26 | func FetchFromURI(uri string, limit int64) ([]byte, error) { 27 | res, err := http.Get(uri) 28 | if err != nil { 29 | return nil, err 30 | } 31 | defer res.Body.Close() 32 | 33 | if limit > 0 { 34 | return ioutil.ReadAll(io.LimitReader(res.Body, limit)) 35 | } 36 | 37 | return ioutil.ReadAll(res.Body) 38 | } 39 | 40 | // FetchFromFile fetch data (with limit) from an file. 41 | // if 'limit' is greater then zero, fetch stops 42 | // with EOF after 'limit' bytes. 43 | func FetchFromFile(filename string, limit int64) ([]byte, error) { 44 | fp, err := os.Open(filename) 45 | if err != nil { 46 | return nil, err 47 | } 48 | defer fp.Close() 49 | 50 | if limit > 0 { 51 | return ioutil.ReadAll(io.LimitReader(fp, limit)) 52 | } 53 | 54 | return ioutil.ReadAll(fp) 55 | } 56 | 57 | // Wrap hard wrap text at the specified colBreak column. 58 | func Wrap(text string, colBreak int) string { 59 | if colBreak < 1 { 60 | return text 61 | } 62 | text = strings.TrimSpace(text) 63 | 64 | var sb strings.Builder 65 | var i int 66 | for i = 0; len(text[i:]) > colBreak; i += colBreak { 67 | sb.WriteString(text[i : i+colBreak]) 68 | sb.WriteString("\n") 69 | 70 | } 71 | sb.WriteString(text[i:]) 72 | 73 | return sb.String() 74 | } 75 | -------------------------------------------------------------------------------- /data/data_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestFetchFromURI(t *testing.T) { 12 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 13 | fmt.Fprint(w, "Hello from scrawl!") 14 | })) 15 | defer ts.Close() 16 | 17 | data, err := FetchFromURI(ts.URL, 1024*10) 18 | if err != nil { 19 | t.Error(err) 20 | } 21 | 22 | want := "Hello from scrawl!" 23 | if got := string(data); got != want { 24 | t.Errorf("got [%v] want [%v]", got, want) 25 | } 26 | } 27 | 28 | func TestFetchFromFile(t *testing.T) { 29 | data, err := FetchFromFile("../testdata/spritesheet.json", 10) 30 | if err != nil { 31 | t.Error(err) 32 | } 33 | 34 | want := `{ "fram` 35 | if got := flatten(string(data)); got != want { 36 | t.Errorf("got [%v] want [%v]", got, want) 37 | } 38 | } 39 | 40 | // remove tabs and newlines and spaces 41 | func flatten(s string) string { 42 | return strings.Replace((strings.Replace(s, "\n", "", -1)), "\t", "", -1) 43 | } 44 | -------------------------------------------------------------------------------- /examples/highlight.css: -------------------------------------------------------------------------------- 1 | /* Style definition file generated by highlight 3.57, http://www.andre-simon.de/ */ 2 | rect { fill:#ffffff; } 3 | g { font-size: 18; font-family: Fira Code; white-space: pre;} 4 | text { fill:#000000; } 5 | tspan.num { fill:#0086b3; } 6 | tspan.esc { fill:#183691; } 7 | tspan.str { fill:#183691; } 8 | tspan.pps { fill:#183691; } 9 | tspan.slc { fill:#969896; } 10 | tspan.com { fill:#969896; } 11 | tspan.ppc { fill:#a71d5d; } 12 | tspan.opt { fill:#000000; } 13 | tspan.ipl { fill:#183691; } 14 | tspan.lin { fill:#b2b2b2; } 15 | tspan.kwa { fill:#a71d5d; } 16 | tspan.kwb { fill:#0086b3; } 17 | tspan.kwc { fill:#0086b3; } 18 | tspan.kwd { fill:#0086b3; } 19 | 20 | -------------------------------------------------------------------------------- /examples/links_tileset.yml: -------------------------------------------------------------------------------- 1 | tiles: 2 | - id: link_tee_down_arrow_left_down 3 | minX: 0 4 | minY: 0 5 | maxX: 128 6 | maxY: 128 7 | - id: link_cross_arrow_up_right_down 8 | minX: 128 9 | minY: 0 10 | maxX: 256 11 | maxY: 128 12 | - id: link_tee_down 13 | minX: 0 14 | minY: 128 15 | maxX: 128 16 | maxY: 256 17 | - id: link_tee_left_arrow_left_up 18 | minX: 128 19 | minY: 128 20 | maxX: 256 21 | maxY: 256 22 | - id: link_horizontal 23 | minX: 256 24 | minY: 0 25 | maxX: 384 26 | maxY: 128 27 | - id: link_tee_right_arrow_right_down 28 | minX: 256 29 | minY: 128 30 | maxX: 384 31 | maxY: 256 32 | - id: link_tee_down_arrow_right_down 33 | minX: 0 34 | minY: 256 35 | maxX: 128 36 | maxY: 384 37 | - id: link_horizontal_arrow_left_right 38 | minX: 128 39 | minY: 256 40 | maxX: 256 41 | maxY: 384 42 | - id: link_cross 43 | minX: 256 44 | minY: 256 45 | maxX: 384 46 | maxY: 384 47 | - id: link_vertical_arrow_up_down 48 | minX: 384 49 | minY: 0 50 | maxX: 512 51 | maxY: 128 52 | - id: link_tee_up_arrow_left 53 | minX: 384 54 | minY: 128 55 | maxX: 512 56 | maxY: 256 57 | - id: link_vertical 58 | minX: 384 59 | minY: 256 60 | maxX: 512 61 | maxY: 384 62 | - id: link_cross_arrow_up 63 | minX: 0 64 | minY: 384 65 | maxX: 128 66 | maxY: 512 67 | - id: link_cross_arrow_up_right 68 | minX: 128 69 | minY: 384 70 | maxX: 256 71 | maxY: 512 72 | - id: link_tee_right_arrow_right 73 | minX: 256 74 | minY: 384 75 | maxX: 384 76 | maxY: 512 77 | - id: link_tee_up_arrow_left_right 78 | minX: 384 79 | minY: 384 80 | maxX: 512 81 | maxY: 512 82 | - id: link_cross_arrow_down 83 | minX: 512 84 | minY: 0 85 | maxX: 640 86 | maxY: 128 87 | - id: link_cross_arrow_left_right 88 | minX: 512 89 | minY: 128 90 | maxX: 640 91 | maxY: 256 92 | - id: link_cross_arrow_left_right_down 93 | minX: 512 94 | minY: 256 95 | maxX: 640 96 | maxY: 384 97 | - id: link_cross_arrow_left_up_right 98 | minX: 512 99 | minY: 384 100 | maxX: 640 101 | maxY: 512 102 | - id: link_horizontal_arrow_right 103 | minX: 0 104 | minY: 512 105 | maxX: 128 106 | maxY: 640 107 | - id: link_tee_right 108 | minX: 128 109 | minY: 512 110 | maxX: 256 111 | maxY: 640 112 | - id: link_cross_arrow_up_down 113 | minX: 256 114 | minY: 512 115 | maxX: 384 116 | maxY: 640 117 | - id: link_tee_up_arrow_up_right 118 | minX: 384 119 | minY: 512 120 | maxX: 512 121 | maxY: 640 122 | - id: link_tee_left_arrow_left 123 | minX: 512 124 | minY: 512 125 | maxX: 640 126 | maxY: 640 127 | - id: link_horizontal_arrow_left 128 | minX: 640 129 | minY: 0 130 | maxX: 768 131 | maxY: 128 132 | - id: link_tee_up 133 | minX: 640 134 | minY: 128 135 | maxX: 768 136 | maxY: 256 137 | - id: link_tee_left 138 | minX: 640 139 | minY: 256 140 | maxX: 768 141 | maxY: 384 142 | - id: link_tee_up_arrow_right 143 | minX: 640 144 | minY: 384 145 | maxX: 768 146 | maxY: 512 147 | - id: link_cross_arrow_left_up_down 148 | minX: 640 149 | minY: 512 150 | maxX: 768 151 | maxY: 640 152 | - id: link_tee_left_arrow_up_down 153 | minX: 0 154 | minY: 640 155 | maxX: 128 156 | maxY: 768 157 | - id: link_tee_down_arrow_down 158 | minX: 128 159 | minY: 640 160 | maxX: 256 161 | maxY: 768 162 | - id: link_tee_down_arrow_left 163 | minX: 256 164 | minY: 640 165 | maxX: 384 166 | maxY: 768 167 | - id: link_tee_up_arrow_up 168 | minX: 384 169 | minY: 640 170 | maxX: 512 171 | maxY: 768 172 | - id: link_tee_right_arrow_up_down 173 | minX: 512 174 | minY: 640 175 | maxX: 640 176 | maxY: 768 177 | - id: link_vertical_arrow_down 178 | minX: 640 179 | minY: 640 180 | maxX: 768 181 | maxY: 768 182 | - id: link_cross_arrow_right 183 | minX: 768 184 | minY: 0 185 | maxX: 896 186 | maxY: 128 187 | - id: link_tee_left_arrow_left_down 188 | minX: 768 189 | minY: 128 190 | maxX: 896 191 | maxY: 256 192 | - id: link_tee_up_arrow_left_up 193 | minX: 768 194 | minY: 256 195 | maxX: 896 196 | maxY: 384 197 | - id: link_tee_right_arrow_up 198 | minX: 768 199 | minY: 384 200 | maxX: 896 201 | maxY: 512 202 | - id: link_cross_arrow_right_down 203 | minX: 768 204 | minY: 512 205 | maxX: 896 206 | maxY: 640 207 | - id: link_tee_down_arrow_left_right 208 | minX: 768 209 | minY: 640 210 | maxX: 896 211 | maxY: 768 212 | - id: link_tee_right_arrow_down 213 | minX: 0 214 | minY: 768 215 | maxX: 128 216 | maxY: 896 217 | - id: link_tee_down_arrow_right 218 | minX: 128 219 | minY: 768 220 | maxX: 256 221 | maxY: 896 222 | - id: link_cross_arrow_left 223 | minX: 256 224 | minY: 768 225 | maxX: 384 226 | maxY: 896 227 | - id: link_tee_left_arrow_up 228 | minX: 384 229 | minY: 768 230 | maxX: 512 231 | maxY: 896 232 | - id: link_tee_left_arrow_down 233 | minX: 512 234 | minY: 768 235 | maxX: 640 236 | maxY: 896 237 | - id: link_tee_right_arrow_up_right 238 | minX: 640 239 | minY: 768 240 | maxX: 768 241 | maxY: 896 242 | - id: link_cross_arrow_left_up 243 | minX: 768 244 | minY: 768 245 | maxX: 896 246 | maxY: 896 247 | - id: link_vertical_arrow_up 248 | minX: 896 249 | minY: 0 250 | maxX: 1024 251 | maxY: 128 252 | width: 1024 253 | height: 896 254 | data: |- 255 | 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 466 | -------------------------------------------------------------------------------- /examples/tilemap_demo_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasepe/tiles/f758e9d7a86cbce74d27dd0a3f4fb18488394767/examples/tilemap_demo_1.png -------------------------------------------------------------------------------- /examples/tilemap_demo_1.yml: -------------------------------------------------------------------------------- 1 | # Nr. of Columns 2 | cols: 4 3 | # Nr. of Rows 4 | rows: 7 5 | # Tile size (Grid cell size) 6 | tile_size: 64 7 | # Canvas margin (optional) 8 | margin: 16 9 | # Canvas watermark (optional) 10 | watermark: Draft 11 | # Canvas background color (optional) 12 | bg_color: "#ffffff" 13 | # List of used tileset 14 | atlas_list: 15 | - ../examples/aws_tileset.yml 16 | - ../examples/links_tileset.yml 17 | # Tiles mapping (associate an index to each tile) 18 | mapping: 19 | 1: aws_lambda 20 | 2: aws_elastic_container_service 21 | 3: aws_api_gateway 22 | 4: aws_rds_mysql_instance 23 | 5: aws_simple_storage_service_s3 24 | 6: aws_elasticache_for_redis 25 | 10: link_vertical 26 | 11: link_vertical_arrow_up 27 | 20: link_cross_arrow_left_up_down 28 | 30: link_horizontal 29 | 40: link_tee_right_arrow_up_down 30 | # Static map layout 31 | layout: > 32 | 0,5,0,0 33 | 4,20,30,2 34 | 0,6,0,11 35 | 0,0,0,10 36 | 0,1,0,10 37 | 0,40,30,3 38 | 0,1,0,0,0 -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lucasepe/tiles 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/disintegration/imaging v1.6.2 7 | github.com/fogleman/gg v1.3.0 8 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 9 | github.com/pkg/errors v0.9.1 10 | github.com/spf13/cobra v1.0.0 11 | github.com/stretchr/testify v1.2.2 12 | golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 13 | gopkg.in/yaml.v2 v2.2.2 14 | ) 15 | -------------------------------------------------------------------------------- /grid/grid.go: -------------------------------------------------------------------------------- 1 | package grid 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/png" 7 | "io" 8 | "math" 9 | "os" 10 | "time" 11 | 12 | "github.com/disintegration/imaging" 13 | "github.com/fogleman/gg" 14 | "github.com/golang/freetype/truetype" 15 | "golang.org/x/image/font/gofont/goregular" 16 | ) 17 | 18 | // Grid represents the grid structure 19 | type Grid struct { 20 | cellSize int 21 | rows int 22 | cols int 23 | margin int 24 | lineDashes float64 25 | lineColor string 26 | lineStrokeWidth float64 27 | borderDashes float64 28 | borderColor string 29 | borderStrokeWidth float64 30 | backgroundColor string 31 | 32 | watermark string 33 | 34 | canvasWidth int 35 | canvasHeight int 36 | 37 | imageWidth int 38 | imageHeight int 39 | 40 | font *truetype.Font 41 | ctx *gg.Context 42 | } 43 | 44 | // NewGrid creates a new grid and sets it up with its configuration 45 | func NewGrid(rows, cols int, cellSize int, opts ...func(*Grid)) (*Grid, error) { 46 | if rows == 0 { 47 | return nil, fmt.Errorf("no rows provided") 48 | } 49 | 50 | if cols == 0 { 51 | return nil, fmt.Errorf("no columns provided") 52 | } 53 | 54 | font, err := truetype.Parse(goregular.TTF) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | res := Grid{ 60 | rows: rows, cols: cols, 61 | cellSize: cellSize, 62 | margin: 24, 63 | lineColor: "#b8b8a7", 64 | backgroundColor: "#ffffff", 65 | borderColor: "#161615", 66 | font: font, 67 | } 68 | 69 | for _, opt := range opts { 70 | opt(&res) 71 | } 72 | 73 | res.canvasWidth = res.cellSize * res.cols 74 | res.canvasHeight = res.cellSize * res.rows 75 | res.imageWidth = res.canvasWidth + 2*res.margin 76 | res.imageHeight = res.canvasHeight + 2*res.margin 77 | 78 | max := res.imageWidth 79 | if max < res.imageHeight { 80 | max = res.imageHeight 81 | } 82 | res.borderColor = "#ffffff00" 83 | res.borderStrokeWidth = 0.002 * float64(max) 84 | res.lineStrokeWidth = 0.001 * float64(max) 85 | 86 | res.ctx = gg.NewContext(res.imageWidth, res.imageHeight) 87 | res.ctx.Translate(float64(res.margin), float64(res.margin)) 88 | res.ctx.SetHexColor(res.backgroundColor) 89 | res.ctx.Clear() 90 | 91 | return &res, nil 92 | } 93 | 94 | // Context returns the grid drawing context 95 | func (g *Grid) Context() *gg.Context { 96 | return g.ctx 97 | } 98 | 99 | // EncodePNG encodes the final image as PNG 100 | func (g *Grid) EncodePNG(w io.Writer) error { 101 | // specify compression level 102 | enc := png.Encoder{ 103 | CompressionLevel: png.BestSpeed, 104 | } 105 | if err := enc.Encode(w, g.ctx.Image()); err != nil { 106 | return err 107 | } 108 | 109 | return nil 110 | } 111 | 112 | // SavePNG saves the grid as PNG image. 113 | func (g *Grid) SavePNG(filename string) error { 114 | if filename == "" { 115 | currentTime := time.Now() 116 | ctf := currentTime.Format("200601021504") 117 | filename = fmt.Sprintf("GRID%s.png", ctf) 118 | } 119 | 120 | f, err := os.Create(filename) 121 | if err != nil { 122 | return err 123 | } 124 | defer f.Close() 125 | 126 | return g.EncodePNG(f) 127 | } 128 | 129 | // DrawBorder draws a border around the grid. 130 | func (g *Grid) DrawBorder() { 131 | canvasWidth := float64(g.cellSize * g.cols) 132 | canvasHeight := float64(g.cellSize * g.rows) 133 | 134 | g.ctx.Push() 135 | g.ctx.MoveTo(0, 0) 136 | g.ctx.LineTo(0, canvasHeight) 137 | g.ctx.LineTo(canvasWidth, canvasHeight) 138 | g.ctx.LineTo(canvasWidth, 0) 139 | g.ctx.LineTo(0, 0) 140 | 141 | if g.borderDashes > 0 { 142 | g.ctx.SetDash(g.borderDashes) 143 | } else { 144 | g.ctx.SetDash() 145 | } 146 | 147 | g.ctx.SetLineWidth(g.borderStrokeWidth) 148 | g.ctx.SetHexColor(g.borderColor) 149 | g.ctx.Stroke() 150 | g.ctx.Pop() 151 | } 152 | 153 | // DrawWatermark draws the watermark. 154 | func (g *Grid) DrawWatermark() { 155 | const lineSpacing = 1.2 156 | 157 | w, h := float64(g.ctx.Width()), float64(g.ctx.Height()) 158 | 159 | fontSize := 0.15 * math.Min(w, h) 160 | face := truetype.NewFace(g.font, &truetype.Options{Size: fontSize}) 161 | 162 | g.ctx.Push() 163 | g.ctx.SetHexColor("#c0c0c088") 164 | g.ctx.SetFontFace(face) 165 | 166 | s := math.Min(g.ctx.MeasureMultilineString(g.watermark, lineSpacing)) 167 | x := 0.5 * (w - 2*float64(g.margin)) 168 | y := 0.5 * (h - 2*float64(g.margin)) 169 | g.ctx.DrawStringWrapped(g.watermark, x, y, 0.5, 0.5, s, lineSpacing, gg.AlignCenter) 170 | g.ctx.Pop() 171 | } 172 | 173 | // DrawGrid draws the grid. 174 | func (g *Grid) DrawGrid() { 175 | g.ctx.Push() 176 | for i := 1; i < g.cols; i++ { 177 | x := float64(i * g.cellSize) 178 | g.ctx.MoveTo(x, 0) 179 | g.ctx.LineTo(x, float64(g.canvasHeight)) 180 | } 181 | 182 | for i := 1; i < g.rows; i++ { 183 | y := float64(i * g.cellSize) 184 | g.ctx.MoveTo(0, y) 185 | g.ctx.LineTo(float64(g.canvasWidth), y) 186 | } 187 | 188 | if g.lineDashes > 0 { 189 | g.ctx.SetDash(g.lineDashes) 190 | } else { 191 | g.ctx.SetDash() 192 | } 193 | if g.lineColor != "" { 194 | g.ctx.SetHexColor(g.lineColor) 195 | } 196 | 197 | g.ctx.SetLineWidth(g.lineStrokeWidth) 198 | g.ctx.Stroke() 199 | g.ctx.Pop() 200 | } 201 | 202 | // DrawImage draws the image at row and col. 203 | // If the image size is greater then the gtid cell size 204 | // it will be shrinked. 205 | func (g *Grid) DrawImage(img image.Image, row, col int) error { 206 | if err := g.VerifyInBounds(row, col); err != nil { 207 | return err 208 | } 209 | 210 | b := img.Bounds() 211 | size := b.Max.X 212 | if b.Max.Y > size { 213 | size = b.Max.Y 214 | } 215 | 216 | if g.cellSize < size { 217 | img = imaging.Resize(img, g.cellSize, g.cellSize, imaging.Lanczos) 218 | } 219 | 220 | center := g.CellCenter(row, col) 221 | 222 | dc := g.Context() 223 | dc.Push() 224 | //g.ctx.RotateAbout(gg.Radians(alpha), center.X, center.Y) 225 | dc.DrawImageAnchored(img, int(center.X), int(center.Y), 0.5, 0.5) 226 | dc.Pop() 227 | 228 | return nil 229 | } 230 | 231 | // DrawCoords draws all cells locations 232 | func (g *Grid) DrawCoords() { 233 | cs := g.CellSize() 234 | fontSize := 0.3 * cs 235 | 236 | face := truetype.NewFace(g.font, &truetype.Options{Size: fontSize}) 237 | 238 | g.ctx.Push() 239 | g.ctx.SetFontFace(face) 240 | g.ctx.SetHexColor("#00000099") 241 | for i := 0; i < g.rows; i++ { 242 | for j := 0; j < g.cols; j++ { 243 | txt := fmt.Sprintf("%d,%d", i, j) 244 | center := g.CellCenter(i, j) 245 | sw, sh := g.ctx.MeasureString(txt) 246 | 247 | g.ctx.Push() 248 | g.ctx.SetHexColor("#00000022") 249 | g.ctx.DrawRoundedRectangle(center.X-0.5*sw-4, center.Y-0.5*sh-4, sw+8, sh+8, 4) 250 | g.ctx.Fill() 251 | g.ctx.Pop() 252 | 253 | g.ctx.DrawStringAnchored(txt, center.X, center.Y, 0.5, 0.35) 254 | } 255 | } 256 | g.ctx.Pop() 257 | } 258 | 259 | // CellSize returns the cell dimension 260 | func (g *Grid) CellSize() float64 { 261 | return float64(g.cellSize) 262 | } 263 | 264 | // CellCenter retuns the cell coordinates in the grid 265 | func (g *Grid) CellCenter(row, col int) gg.Point { 266 | size := g.CellSize() 267 | 268 | x := 0.5*size + float64(col)*size 269 | y := 0.5*size + float64(row)*size 270 | 271 | return gg.Point{X: x, Y: y} 272 | } 273 | 274 | // VerifyInBounds verify that the coordinates 275 | // belongs to the grid 276 | func (g *Grid) VerifyInBounds(row, col int) error { 277 | if row < 0 || row >= g.rows || col < 0 || col >= g.cols { 278 | return fmt.Errorf("cell (%d, %d) is out of bounds", row, col) 279 | } 280 | return nil 281 | } 282 | 283 | // Background sets the grid background color 284 | func Background(hex string) func(*Grid) { 285 | return func(g *Grid) { 286 | g.backgroundColor = hex 287 | } 288 | } 289 | 290 | // Margin sets the grid margin in pixels. 291 | func Margin(val int) func(*Grid) { 292 | return func(g *Grid) { 293 | g.margin = val 294 | } 295 | } 296 | 297 | // Watermark sets the grid watermark. 298 | func Watermark(text string) func(*Grid) { 299 | return func(g *Grid) { 300 | g.watermark = text 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /grid/grid_test.go: -------------------------------------------------------------------------------- 1 | package grid 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/fogleman/gg" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestNewGrid(t *testing.T) { 14 | grid, err := NewGrid(12, 10, 64) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | assert.Equal(t, 12, grid.rows) 20 | assert.Equal(t, 10, grid.cols) 21 | assert.Equal(t, 64, grid.cellSize) 22 | 23 | assert.NotNil(t, grid.Context()) 24 | assert.Equal(t, float64(64), grid.CellSize()) 25 | } 26 | 27 | func TestGridCellCenter(t *testing.T) { 28 | tests := []struct { 29 | row int 30 | col int 31 | want gg.Point 32 | }{ 33 | {2, 2, gg.Point{X: 160, Y: 160}}, 34 | {5, 4, gg.Point{X: 288, Y: 352}}, 35 | {6, 7, gg.Point{X: 480, Y: 416}}, 36 | {9, 3, gg.Point{X: 224, Y: 608}}, 37 | } 38 | 39 | grid, err := NewGrid(12, 10, 64) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | for _, tt := range tests { 45 | t.Run("", func(t *testing.T) { 46 | got := grid.CellCenter(tt.row, tt.col) 47 | if got != tt.want { 48 | t.Errorf("got [%v] want [%v]", got, tt.want) 49 | } 50 | }) 51 | } 52 | } 53 | 54 | func TestGridVerifyInBounds(t *testing.T) { 55 | tests := []struct { 56 | row int 57 | col int 58 | want string 59 | }{ 60 | {2, 2, ""}, 61 | {5, 4, "cell (5, 4) is out of bounds"}, 62 | {6, 7, "cell (6, 7) is out of bounds"}, 63 | {1, 3, ""}, 64 | } 65 | 66 | grid, err := NewGrid(5, 5, 64) 67 | if err != nil { 68 | t.Fatal(err) 69 | } 70 | 71 | for _, tt := range tests { 72 | t.Run("", func(t *testing.T) { 73 | got := grid.VerifyInBounds(tt.row, tt.col) 74 | if got != nil && got.Error() != tt.want { 75 | t.Errorf("got [%v] want [%v]", got, tt.want) 76 | } 77 | }) 78 | } 79 | } 80 | 81 | func TestGridLayout(t *testing.T) { 82 | grid, err := NewGrid(4, 4, 12) 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | 87 | grid.DrawGrid() 88 | grid.DrawCoords() 89 | grid.DrawBorder() 90 | 91 | var data bytes.Buffer 92 | if err := grid.EncodePNG(&data); err != nil { 93 | t.Fatal(err) 94 | } 95 | 96 | str := base64.StdEncoding.EncodeToString(data.Bytes()) 97 | //t.Logf(str) 98 | assert.True(t, strings.HasPrefix(str, "iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAIAAABt+uBvAAAErElEQVR4Aeyb3U7qShiG")) 99 | } 100 | -------------------------------------------------------------------------------- /imagelist/imagelist.go: -------------------------------------------------------------------------------- 1 | package imagelist 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "os/user" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | // Load grabs the list off all the images 12 | // in the folder (id uri is a folder) or in the 13 | // text file (if uri starts with the '@' character). 14 | func Load(uri string) ([]string, error) { 15 | if strings.HasPrefix(uri, "@") { 16 | filename, err := resolveTildeEventually(uri[1:]) 17 | if err != nil { 18 | return nil, err 19 | } 20 | return FromFile(filename) 21 | } 22 | 23 | dirname, err := resolveTildeEventually(uri) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | //fmt.Fprintf(os.Stderr, "loading image list from folder <%s>\n", dirname) 29 | return FromFolder(dirname) 30 | } 31 | 32 | // FromFile returns a slice with all PNG 33 | // images path listed in the specified text file. 34 | func FromFile(filename string) ([]string, error) { 35 | file, err := os.Open(filename) 36 | if err != nil { 37 | return nil, err 38 | } 39 | defer file.Close() 40 | 41 | res := []string{} 42 | 43 | scanner := bufio.NewScanner(file) 44 | for scanner.Scan() { 45 | if line := strings.TrimSpace(scanner.Text()); len(line) > 0 { 46 | res = append(res, line) 47 | } 48 | } 49 | 50 | if err := scanner.Err(); err != nil { 51 | return nil, err 52 | } 53 | 54 | return res, nil 55 | } 56 | 57 | // FromFolder returns a slice with all PNG images 58 | // located in 'dirname'. 59 | func FromFolder(dirname string) ([]string, error) { 60 | fp, err := os.Open(dirname) 61 | if err != nil { 62 | return nil, err 63 | } 64 | defer fp.Close() 65 | 66 | files, err := fp.Readdir(-1) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | res := []string{} 72 | for _, el := range files { 73 | if el.IsDir() { 74 | continue 75 | } 76 | 77 | if strings.EqualFold(filepath.Ext(el.Name()), ".png") { 78 | res = append(res, filepath.Join(dirname, el.Name())) 79 | } 80 | } 81 | 82 | return res, nil 83 | } 84 | 85 | // resolveTildeEventually expand the `~` character 86 | // as the user home directory. 87 | func resolveTildeEventually(uri string) (string, error) { 88 | if strings.HasPrefix(uri, "~") { 89 | usr, err := user.Current() 90 | if err != nil { 91 | return "", err 92 | } 93 | 94 | return filepath.Join(usr.HomeDir, uri[1:]), nil 95 | } 96 | 97 | return uri, nil 98 | } 99 | -------------------------------------------------------------------------------- /tilemap/tilemap.go: -------------------------------------------------------------------------------- 1 | package tilemap 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "io" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/lucasepe/tiles/data" 11 | "github.com/lucasepe/tiles/grid" 12 | "github.com/lucasepe/tiles/tileset" 13 | "gopkg.in/yaml.v2" 14 | ) 15 | 16 | type TileMap struct { 17 | cols int 18 | rows int 19 | tileSize int 20 | layout []int 21 | margin int 22 | watermark string 23 | bgColor string 24 | mapping map[int]string 25 | atlasList []string 26 | } 27 | 28 | func (tm *TileMap) Render(wr io.Writer) error { 29 | repo, err := tileset.Load(tm.atlasList...) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | gr, err := grid.NewGrid(tm.rows, tm.cols, tm.tileSize, 35 | grid.Background(tm.bgColor), 36 | grid.Margin(tm.margin), 37 | grid.Watermark(tm.watermark)) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | gr.DrawBorder() 43 | 44 | for c := 0; c < tm.cols; c++ { 45 | for r := 0; r < tm.rows; r++ { 46 | // Grab the tile index 47 | pos := r*tm.cols + c 48 | if pos >= len(tm.layout) { 49 | return fmt.Errorf("invalid index [%d] with a grid length of %d", pos, len(tm.layout)) 50 | } 51 | 52 | idx := tm.layout[pos] 53 | if idx <= 0 { 54 | continue 55 | } 56 | 57 | // Find the image for the tile id 58 | id, ok := tm.mapping[idx] 59 | if !ok { 60 | return fmt.Errorf("tile with index: %d not found in mapping", idx) 61 | } 62 | 63 | img, err := findImageByID(repo, id) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | gr.DrawImage(img, r, c) 69 | } 70 | } 71 | 72 | gr.DrawWatermark() 73 | 74 | return gr.EncodePNG(wr) 75 | } 76 | 77 | // UnmarshalYAML implements the Unmarshaler interface of the yaml pkg. 78 | func (tm *TileMap) UnmarshalYAML(unmarshal func(interface{}) error) error { 79 | aux := struct { 80 | Cols int `yaml:"cols"` 81 | Rows int `yaml:"rows"` 82 | TileSize int `yaml:"tile_size"` 83 | Margin int `yaml:"margin"` 84 | BgColor string `yaml:"bg_color"` 85 | Layout string `yaml:"layout"` 86 | Watermark string `yaml:"watermark"` 87 | Mapping map[int]string `yaml:"mapping"` 88 | AtlasList []string `yaml:"atlas_list"` 89 | }{} 90 | 91 | err := unmarshal(&aux) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | tm.cols = aux.Cols 97 | tm.rows = aux.Rows 98 | tm.tileSize = aux.TileSize 99 | tm.margin = aux.Margin 100 | tm.bgColor = aux.BgColor 101 | tm.watermark = aux.Watermark 102 | tm.mapping = make(map[int]string) 103 | for k, v := range aux.Mapping { 104 | tm.mapping[k] = v 105 | } 106 | 107 | tm.atlasList = make([]string, len(aux.AtlasList)) 108 | for i, uri := range aux.AtlasList { 109 | tm.atlasList[i] = uri 110 | } 111 | 112 | layout := strings.Split(strings.Replace(aux.Layout, " ", ",", -1), ",") 113 | 114 | tm.layout = make([]int, len(layout)) 115 | for i := 0; i < len(layout); i++ { 116 | num, err := strconv.Atoi(strings.TrimSpace(layout[i])) 117 | if err != nil { 118 | return err 119 | } 120 | tm.layout[i] = num 121 | } 122 | 123 | return nil 124 | } 125 | 126 | func Load(uri string) (TileMap, error) { 127 | dat, err := data.Fetch(uri, -1) 128 | if err != nil { 129 | return TileMap{}, err 130 | } 131 | 132 | res := TileMap{} 133 | if err := yaml.Unmarshal(dat, &res); err != nil { 134 | return TileMap{}, err 135 | } 136 | 137 | return res, nil 138 | } 139 | 140 | func findImageByID(repo []*tileset.Tileset, id string) (image.Image, error) { 141 | for _, el := range repo { 142 | if tile, ok := el.Get(id); ok { 143 | return el.Image(tile) 144 | } 145 | } 146 | 147 | return nil, fmt.Errorf("tile with id: %s not found", id) 148 | } 149 | -------------------------------------------------------------------------------- /tilemap/tilemap_test.go: -------------------------------------------------------------------------------- 1 | package tilemap 2 | 3 | import ( 4 | "image" 5 | "image/png" 6 | "os" 7 | "testing" 8 | 9 | "github.com/lucasepe/tiles/tileset" 10 | ) 11 | 12 | func TestFetchFromURI(t *testing.T) { 13 | tm, err := Load("../examples/demo.yml") 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | 18 | ts, err := tileset.Load(tm.atlasList...) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | img, err := tm.Render(ts) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | savePNG(img, "delme.png") 29 | } 30 | 31 | func savePNG(im image.Image, filename string) error { 32 | fp, err := os.Create(filename) 33 | if err != nil { 34 | return err 35 | } 36 | defer fp.Close() 37 | 38 | enc := png.Encoder{ 39 | CompressionLevel: png.BestSpeed, 40 | } 41 | 42 | return enc.Encode(fp, im) 43 | } 44 | -------------------------------------------------------------------------------- /tileset/tileset.go: -------------------------------------------------------------------------------- 1 | package tileset 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "fmt" 7 | "image" 8 | _ "image/png" // load the PNG driver 9 | "io" 10 | "strings" 11 | "time" 12 | 13 | "github.com/lucasepe/tiles/cache" 14 | "github.com/lucasepe/tiles/data" 15 | "gopkg.in/yaml.v2" 16 | ) 17 | 18 | // Tile describes a tile in the tiles set. 19 | type Tile struct { 20 | ID string `yaml:"id"` 21 | MinX int `yaml:"minX"` 22 | MinY int `yaml:"minY"` 23 | MaxX int `yaml:"maxX"` 24 | MaxY int `yaml:"maxY"` 25 | } 26 | 27 | // Rect returns the image rectangle fot the tile. 28 | func (t *Tile) Rect() image.Rectangle { 29 | return image.Rect(t.MinX, t.MinY, t.MaxX, t.MaxY) 30 | } 31 | 32 | // Tileset describes a tile set. 33 | type Tileset struct { 34 | Tiles []*Tile `yaml:"tiles,omitempty"` 35 | Width int `yaml:"width"` 36 | Height int `yaml:"height"` 37 | Data string `yaml:"data"` 38 | 39 | uri string 40 | } 41 | 42 | // Load fetches an array of tileset(s). 43 | func Load(uri ...string) ([]*Tileset, error) { 44 | res := make([]*Tileset, len(uri)) 45 | for i, u := range uri { 46 | el, err := loadOne(u) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | res[i] = el 52 | } 53 | 54 | return res, nil 55 | } 56 | 57 | // loadOne fetches a single tile set from the specified uri. 58 | func loadOne(uri string) (*Tileset, error) { 59 | dat, err := data.Fetch(uri, -1) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | res := &Tileset{uri: uri} 65 | if err := yaml.Unmarshal(dat, &res); err != nil { 66 | return nil, err 67 | } 68 | res.Data = strings.Replace(res.Data, "\n", "", -1) 69 | 70 | return res, err 71 | } 72 | 73 | // Get returns the tile with the specified id. 74 | func (ts *Tileset) Get(id string) (Tile, bool) { 75 | for _, el := range ts.Tiles { 76 | if strings.EqualFold(el.ID, id) { 77 | return Tile{ 78 | ID: el.ID, 79 | MinX: el.MinX, MinY: el.MinY, 80 | MaxX: el.MaxX, MaxY: el.MaxY, 81 | }, true 82 | } 83 | } 84 | 85 | return Tile{}, false 86 | } 87 | 88 | // List writers all the tiles id on the specified writer. 89 | func (ts *Tileset) List(wr io.Writer) error { 90 | for _, el := range ts.Tiles { 91 | if _, err := fmt.Fprintln(wr, el.ID); err != nil { 92 | return err 93 | } 94 | } 95 | 96 | return nil 97 | } 98 | 99 | // Image returns the tile image. 100 | func (ts *Tileset) Image(tile Tile) (image.Image, error) { 101 | img, err := ts.cachedImage() 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | return img.(subImager).SubImage(tile.Rect()), nil 107 | } 108 | 109 | func (ts *Tileset) cachedImage() (image.Image, error) { 110 | var res image.Image 111 | if el, found := storage.Get(ts.uri); found { 112 | res = el.(image.Image) 113 | return res, nil 114 | } 115 | 116 | data, err := base64.StdEncoding.DecodeString(ts.Data) 117 | if err != nil { 118 | return nil, err 119 | } 120 | 121 | img, _, err := image.Decode(bytes.NewReader(data)) 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | storage.Set(ts.uri, img, cache.DefaultExpiration) 127 | 128 | return img, nil 129 | } 130 | 131 | // subImager interface to return 132 | // an image representing the portion of 133 | // the tileset image visible through r. 134 | type subImager interface { 135 | SubImage(r image.Rectangle) image.Image 136 | } 137 | 138 | var storage *cache.Cache 139 | 140 | func init() { 141 | storage = cache.New(5*time.Minute, 10*time.Minute) 142 | } 143 | --------------------------------------------------------------------------------