From 41167a994324a1ad21679042a9bb5d1b447bc38f Mon Sep 17 00:00:00 2001 From: Alexander Rolek Date: Sun, 14 Jul 2024 18:11:57 -0600 Subject: [PATCH] Added TileURLTemplate type Introduced the type TileURLTemplate for handling forming up tile template URLs. This adds more structure and type safety when handling the creation of tile template URLs. closes #994 --- cmd/tegola/cmd/server.go | 11 +- cmd/tegola_lambda/main.go | 9 +- config/config_test.go | 65 +++--- server/errors.go | 11 + server/handle_capabilities.go | 50 ++-- server/handle_capabilities_test.go | 301 +++++++++++++++++++------ server/handle_map_capabilities.go | 21 +- server/handle_map_capabilities_test.go | 245 ++++++++++++++++++-- server/handle_map_layer_zxy.go | 12 +- server/handle_map_layer_zxy_test.go | 9 +- server/handle_map_style.go | 18 +- server/handle_map_style_test.go | 63 +++--- server/middleware_tile_cache_test.go | 4 +- server/server.go | 48 ++-- server/server_cors_test.go | 5 +- server/server_internal_test.go | 135 ++--------- server/server_test.go | 61 +++-- server/tile_url_template.go | 126 +++++++++++ server/tile_url_template_test.go | 229 +++++++++++++++++++ server/viewer_disabled.go | 1 + server/viewer_embed.go | 1 + 21 files changed, 1041 insertions(+), 384 deletions(-) create mode 100644 server/errors.go create mode 100644 server/tile_url_template.go create mode 100644 server/tile_url_template_test.go diff --git a/cmd/tegola/cmd/server.go b/cmd/tegola/cmd/server.go index cb4a53526..9d504872b 100644 --- a/cmd/tegola/cmd/server.go +++ b/cmd/tegola/cmd/server.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "net/url" "time" "github.com/go-spatial/cobra" @@ -38,9 +39,17 @@ var serverCmd = &cobra.Command{ serverPort = string(conf.Webserver.Port) } + if conf.Webserver.HostName != "" { + hostname, err := url.Parse(string(conf.Webserver.HostName)) + if err != nil { + log.Fatalf("unable to parse webserver.hostname: %s", err) + } + + server.HostName = hostname + } + // set our server version server.Version = build.Version - server.HostName = string(conf.Webserver.HostName) build.Commands = append(build.Commands, cmd.Name()) atlas.StartSubProcesses() diff --git a/cmd/tegola_lambda/main.go b/cmd/tegola_lambda/main.go index 5e987da8c..f6c9f3af7 100644 --- a/cmd/tegola_lambda/main.go +++ b/cmd/tegola_lambda/main.go @@ -8,8 +8,8 @@ import ( "github.com/akrylysov/algnhsa" "github.com/dimfeld/httptreemux" - "github.com/go-spatial/geom/encoding/mvt" + "github.com/go-spatial/tegola/atlas" "github.com/go-spatial/tegola/cmd/internal/register" "github.com/go-spatial/tegola/config" @@ -91,7 +91,12 @@ func init() { // set our server version server.Version = build.Version if conf.Webserver.HostName != "" { - server.HostName = string(conf.Webserver.HostName) + hostname, err := url.Parse(string(conf.Webserver.HostName)) + if err != nil { + log.Fatalf("unable to parse webserver.hostname: %s", err) + } + + server.HostName = hostname } // set user defined response headers diff --git a/config/config_test.go b/config/config_test.go index 5be0d8186..1a6b80c71 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -113,7 +113,7 @@ func TestParse(t *testing.T) { } tests := map[string]tcase{ - "1": { + "happy path": { config: ` tile_buffer = 12 @@ -256,7 +256,7 @@ func TestParse(t *testing.T) { }, }, }, - "2 test env": { + "test env": { config: ` [webserver] hostname = "${ENV_TEST_HOST_1}.${ENV_TEST_HOST_2}.${ENV_TEST_HOST_3}" @@ -412,7 +412,7 @@ func TestParse(t *testing.T) { }, }, }, - "3 missing env": { + "missing env": { config: ` [webserver] hostname = "${ENV_TEST_HOST_1}.${ENV_TEST_HOST_2}.${ENV_TEST_HOST_3}" @@ -479,7 +479,7 @@ func TestParse(t *testing.T) { expected: config.Config{}, expectedErr: env.ErrEnvVar("I_AM_MISSING"), }, - "4 test empty proxy_protocol": { + "test empty proxy_protocol": { config: ` [webserver] hostname = "${ENV_TEST_HOST_1}.${ENV_TEST_HOST_2}.${ENV_TEST_HOST_3}" @@ -647,7 +647,6 @@ func TestValidateMutateZoom(t *testing.T) { type tcase struct { config *config.Config - layerName string expectedMinZoom int expectedMaxZoom int } @@ -675,7 +674,7 @@ func TestValidateMutateZoom(t *testing.T) { } tests := map[string]tcase{ - "1 - default max zoom": { + "default max zoom": { expectedMinZoom: 0, expectedMaxZoom: 22, config: &config.Config{ @@ -719,7 +718,7 @@ func TestValidateMutateZoom(t *testing.T) { }, }, }, - "2 - max zoom 0, default to 1": { + "max zoom 0, default to 1": { expectedMinZoom: 0, expectedMaxZoom: 1, config: &config.Config{ @@ -790,7 +789,7 @@ func TestValidate(t *testing.T) { } tests := map[string]tcase{ - "1": { + "happy path 1": { config: config.Config{ LocationName: "", Webserver: config.Webserver{ @@ -858,7 +857,7 @@ func TestValidate(t *testing.T) { ProviderLayer2: "provider2.water", }, }, - "2": { + "happy path 2": { config: config.Config{ Providers: []env.Dict{ { @@ -924,7 +923,7 @@ func TestValidate(t *testing.T) { ProviderLayer2: "provider2.water_5_10", }, }, - "3": { + "happy path 3": { config: config.Config{ LocationName: "", Webserver: config.Webserver{ @@ -1007,7 +1006,7 @@ func TestValidate(t *testing.T) { }, expectedErr: nil, }, - "4 default zooms": { + "default zooms": { config: config.Config{ LocationName: "", Webserver: config.Webserver{ @@ -1076,7 +1075,7 @@ func TestValidate(t *testing.T) { }, expectedErr: nil, }, - "5 default zooms fail": { + "default zooms fail": { config: config.Config{ LocationName: "", Webserver: config.Webserver{ @@ -1140,7 +1139,7 @@ func TestValidate(t *testing.T) { ProviderLayer2: "provider2.water_default_z", }, }, - "6 blocked headers": { + "blocked headers": { config: config.Config{ LocationName: "", Webserver: config.Webserver{ @@ -1154,7 +1153,7 @@ func TestValidate(t *testing.T) { Header: "Content-Encoding", }, }, - "7 non-existant provider type": { + "non-existant provider type": { expectedErr: config.ErrUnknownProviderType{Type: "nonexistant", Name: "provider1", KnownProviders: []string{"..."}}, config: config.Config{ Providers: []env.Dict{ @@ -1165,7 +1164,7 @@ func TestValidate(t *testing.T) { }, }, }, - "8 missing name field": { + "missing name field": { expectedErr: config.ErrProviderNameRequired{Pos: 0}, config: config.Config{ Providers: []env.Dict{ @@ -1175,7 +1174,7 @@ func TestValidate(t *testing.T) { }, }, }, - "8 duplicate name field": { + "duplicate name field": { expectedErr: config.ErrProviderNameDuplicate{Pos: 1}, config: config.Config{ Providers: []env.Dict{ @@ -1190,7 +1189,7 @@ func TestValidate(t *testing.T) { }, }, }, - "8 missing name field at pos 1": { + "missing name field at pos 1": { expectedErr: config.ErrProviderNameRequired{Pos: 1}, config: config.Config{ Providers: []env.Dict{ @@ -1204,7 +1203,7 @@ func TestValidate(t *testing.T) { }, }, }, - "9 missing type field": { + "missing type field": { expectedErr: config.ErrProviderTypeRequired{Pos: 0}, config: config.Config{ Providers: []env.Dict{ @@ -1214,7 +1213,7 @@ func TestValidate(t *testing.T) { }, }, }, - "9 missing type field at pos 1": { + "missing type field at pos 1": { expectedErr: config.ErrProviderTypeRequired{Pos: 1}, config: config.Config{ Providers: []env.Dict{ @@ -1228,7 +1227,7 @@ func TestValidate(t *testing.T) { }, }, }, - "10 happy 1 mvt provider only 1 layer": { + "happy 1 mvt provider only 1 layer": { config: config.Config{ Providers: []env.Dict{ { @@ -1249,7 +1248,7 @@ func TestValidate(t *testing.T) { }, }, }, - "10 happy 1 mvt provider only 2 layer": { + "happy 1 mvt provider only 2 layer": { config: config.Config{ Providers: []env.Dict{ { @@ -1273,7 +1272,7 @@ func TestValidate(t *testing.T) { }, }, }, - "10 happy 1 mvt, 1 std provider only 1 layer": { + "happy 1 mvt, 1 std provider only 1 layer": { config: config.Config{ Providers: []env.Dict{ { @@ -1301,7 +1300,7 @@ func TestValidate(t *testing.T) { }, }, }, - "11 invalid provider referenced in map": { + "invalid provider referenced in map": { expectedErr: config.ErrInvalidProviderForMap{ MapName: "happy", ProviderName: "bad", @@ -1326,7 +1325,7 @@ func TestValidate(t *testing.T) { }, }, }, - "12 mvt_provider comingle": { + "mvt_provider comingle": { expectedErr: config.ErrMVTDifferentProviders{ Original: "provider1", Current: "stdprovider1", @@ -1358,7 +1357,7 @@ func TestValidate(t *testing.T) { }, }, }, - "13 mvt_provider comingle; flip": { + "mvt_provider comingle; flip": { expectedErr: config.ErrMVTDifferentProviders{ Original: "stdprovider1", Current: "provider1", @@ -1390,7 +1389,7 @@ func TestValidate(t *testing.T) { }, }, }, - "14 reserved token name": { + "reserved token name": { config: config.Config{ Maps: []provider.Map{ { @@ -1414,7 +1413,7 @@ func TestValidate(t *testing.T) { }, }, }, - "15 duplicate parameter name": { + "duplicate parameter name": { config: config.Config{ Maps: []provider.Map{ { @@ -1443,7 +1442,7 @@ func TestValidate(t *testing.T) { }, }, }, - "16 duplicate token name": { + "duplicate token name": { config: config.Config{ Maps: []provider.Map{ { @@ -1472,7 +1471,7 @@ func TestValidate(t *testing.T) { }, }, }, - "17 parameter unknown type": { + "parameter unknown type": { config: config.Config{ Maps: []provider.Map{ { @@ -1496,7 +1495,7 @@ func TestValidate(t *testing.T) { }, }, }, - "18 parameter two defaults": { + "parameter two defaults": { config: config.Config{ Maps: []provider.Map{ { @@ -1524,7 +1523,7 @@ func TestValidate(t *testing.T) { }, }, }, - "19 parameter invalid default": { + "parameter invalid default": { config: config.Config{ Maps: []provider.Map{ { @@ -1551,7 +1550,7 @@ func TestValidate(t *testing.T) { }, }, }, - "20 invalid token name": { + "invalid token name": { config: config.Config{ Maps: []provider.Map{ { @@ -1575,7 +1574,7 @@ func TestValidate(t *testing.T) { }, }, }, - "21 invalid webserver hostname": { + "invalid webserver hostname": { config: config.Config{ Webserver: config.Webserver{ HostName: ":\\malformed.host", diff --git a/server/errors.go b/server/errors.go new file mode 100644 index 000000000..a17d0b508 --- /dev/null +++ b/server/errors.go @@ -0,0 +1,11 @@ +package server + +import "fmt" + +type ErrMalformedTileTemplateURL struct { + Got string +} + +func (e ErrMalformedTileTemplateURL) Error() string { + return fmt.Sprintf("Expected URL in the format scheme://domain.tld/maps/:map_name/{z}/{x}/{y}.pbf. Got %s", e.Got) +} diff --git a/server/handle_capabilities.go b/server/handle_capabilities.go index 42f1404de..6690c11b2 100644 --- a/server/handle_capabilities.go +++ b/server/handle_capabilities.go @@ -4,9 +4,12 @@ import ( "encoding/json" "net/http" "net/url" + "path" "github.com/go-spatial/geom" + "github.com/go-spatial/tegola/atlas" + "github.com/go-spatial/tegola/internal/log" ) type Capabilities struct { @@ -19,16 +22,16 @@ type CapabilitiesMap struct { Attribution string `json:"attribution"` Bounds *geom.Extent `json:"bounds"` Center [3]float64 `json:"center"` - Tiles []string `json:"tiles"` + Tiles []TileURLTemplate `json:"tiles"` Capabilities string `json:"capabilities"` Layers []CapabilitiesLayer `json:"layers"` } type CapabilitiesLayer struct { - Name string `json:"name"` - Tiles []string `json:"tiles"` - MinZoom uint `json:"minzoom"` - MaxZoom uint `json:"maxzoom"` + Name string `json:"name"` + Tiles []TileURLTemplate `json:"tiles"` + MinZoom uint `json:"minzoom"` + MaxZoom uint `json:"maxzoom"` } type HandleCapabilities struct{} @@ -39,16 +42,13 @@ func (req HandleCapabilities) ServeHTTP(w http.ResponseWriter, r *http.Request) Version: Version, } - // parse our query string - var query = r.URL.Query() - // iterate our registered maps for _, m := range atlas.AllMaps() { debugQuery := url.Values{} // if we have a debug param add it to our URLs - if query.Get("debug") == "true" { - debugQuery.Set("debug", "true") + if r.URL.Query().Get(QueryKeyDebug) == "true" { + debugQuery.Set(QueryKeyDebug, "true") // update our map to include the debug layers m = m.AddDebugLayers() @@ -60,10 +60,20 @@ func (req HandleCapabilities) ServeHTTP(w http.ResponseWriter, r *http.Request) Attribution: m.Attribution, Bounds: m.Bounds, Center: m.Center, - Tiles: []string{ - buildCapabilitiesURL(r, []string{"maps", m.Name, "{z}/{x}/{y}.pbf"}, debugQuery), + Tiles: []TileURLTemplate{ + { + Scheme: scheme(r), + Host: hostName(r).Host, + MapName: m.Name, + Query: debugQuery, + }, }, - Capabilities: buildCapabilitiesURL(r, []string{"capabilities", m.Name + ".json"}, debugQuery), + Capabilities: (&url.URL{ + Scheme: scheme(r), + Host: hostName(r).Host, + Path: path.Join(URIPrefix, "capabilities", m.Name+".json"), + RawQuery: debugQuery.Encode(), + }).String(), } for i := range m.Layers { @@ -93,8 +103,14 @@ func (req HandleCapabilities) ServeHTTP(w http.ResponseWriter, r *http.Request) // build the layer details cLayer := CapabilitiesLayer{ Name: m.Layers[i].MVTName(), - Tiles: []string{ - buildCapabilitiesURL(r, []string{"maps", m.Name, m.Layers[i].MVTName(), "{z}/{x}/{y}.pbf"}, debugQuery), + Tiles: []TileURLTemplate{ + { + Host: hostName(r).Host, + Scheme: scheme(r), + MapName: m.Name, + LayerName: m.Layers[i].MVTName(), + Query: debugQuery, + }, }, MinZoom: m.Layers[i].MinZoom, MaxZoom: m.Layers[i].MaxZoom, @@ -117,5 +133,7 @@ func (req HandleCapabilities) ServeHTTP(w http.ResponseWriter, r *http.Request) } // setup a new json encoder and encode our capabilities - json.NewEncoder(w).Encode(capabilities) + if err := json.NewEncoder(w).Encode(capabilities); err != nil { + log.Errorf("error trying to encode capabilities response (%s)", err) + } } diff --git a/server/handle_capabilities_test.go b/server/handle_capabilities_test.go index 6bd80b12b..f51f267a9 100644 --- a/server/handle_capabilities_test.go +++ b/server/handle_capabilities_test.go @@ -2,12 +2,12 @@ package server_test import ( "encoding/json" - "fmt" - "io/ioutil" "net/http" - "reflect" + "net/url" "testing" + "github.com/go-test/deep" + "github.com/go-spatial/tegola" "github.com/go-spatial/tegola/atlas" "github.com/go-spatial/tegola/server" @@ -16,49 +16,38 @@ import ( func TestHandleCapabilities(t *testing.T) { type tcase struct { - hostname string + hostname *url.URL port string uri string - method string expected server.Capabilities } fn := func(tc tcase) func(*testing.T) { return func(t *testing.T) { - var err error - server.HostName = tc.hostname server.Port = tc.port + a := newTestMapWithLayers(testLayer1, testLayer2, testLayer3) - w, _, err := doRequest(a, tc.method, tc.uri, nil) + resp, _, err := doRequest(t, a, http.MethodGet, tc.uri, nil) if err != nil { t.Fatal(err) } - - if w.Code != http.StatusOK { - t.Errorf("status code, expected %v got %v", http.StatusOK, w.Code) - return - } - - bytes, err := ioutil.ReadAll(w.Body) - if err != nil { - t.Errorf("error response body, expected nil got %v", err) + if resp.Code != http.StatusOK { + t.Errorf("status code, expected %v got %v", http.StatusOK, resp.Code) return } + // read the response body var capabilities server.Capabilities - - // read the respons body - if err := json.Unmarshal(bytes, &capabilities); err != nil { - t.Errorf("error unmarshal JSON, expected nil got %v", err) + if err := json.NewDecoder(resp.Body).Decode(&capabilities); err != nil { + t.Errorf("unexpected error decoding response body: %s", err) return } - if !reflect.DeepEqual(tc.expected, capabilities) { - t.Errorf("response body, \n expected %+v\n got %+v", tc.expected, capabilities) + if diff := deep.Equal(tc.expected, capabilities); diff != nil { + t.Errorf("expected does not match output. diff: %v", diff) } - } } @@ -75,22 +64,36 @@ func TestHandleCapabilities(t *testing.T) { Center: [3]float64{1.0, 2.0, 3.0}, Bounds: tegola.WGS84Bounds, Capabilities: "http://localhost:8080/capabilities/test-map.json", - Tiles: []string{ - "http://localhost:8080/maps/test-map/{z}/{x}/{y}.pbf", + Tiles: []server.TileURLTemplate{ + { + Scheme: "http", + Host: "localhost:8080", + MapName: "test-map", + }, }, Layers: []server.CapabilitiesLayer{ { Name: testLayer1.MVTName(), - Tiles: []string{ - fmt.Sprintf("http://localhost:8080/maps/test-map/%v/{z}/{x}/{y}.pbf", testLayer1.MVTName()), + Tiles: []server.TileURLTemplate{ + { + Scheme: "http", + Host: "localhost:8080", + MapName: "test-map", + LayerName: testLayer1.MVTName(), + }, }, MinZoom: testLayer1.MinZoom, MaxZoom: testLayer3.MaxZoom, // layer 1 and layer 3 share a name in our test so the zoom range includes the entire zoom range }, { Name: testLayer2.MVTName(), - Tiles: []string{ - fmt.Sprintf("http://localhost:8080/maps/test-map/%v/{z}/{x}/{y}.pbf", testLayer2.MVTName()), + Tiles: []server.TileURLTemplate{ + { + Scheme: "http", + Host: "localhost:8080", + MapName: "test-map", + LayerName: testLayer2.MVTName(), + }, }, MinZoom: testLayer2.MinZoom, MaxZoom: testLayer2.MaxZoom, @@ -103,9 +106,11 @@ func TestHandleCapabilities(t *testing.T) { "none port cdn host": { // With hostname set and port set to "none" in config, urls should have host "cdn.tegola.io" // debug layers turned on - hostname: "cdn.tegola.io", - port: "none", // Set to none or port 8080 from uri will be used. - uri: "http://localhost:8080/capabilities?debug=true", + hostname: &url.URL{ + Host: "cdn.tegola.io", + }, + port: "none", // Set to none or port 8080 from uri will be used. + uri: "http://localhost:8080/capabilities?debug=true", expected: server.Capabilities{ Version: serverVersion, Maps: []server.CapabilitiesMap{ @@ -115,38 +120,77 @@ func TestHandleCapabilities(t *testing.T) { Center: [3]float64{1.0, 2.0, 3.0}, Bounds: tegola.WGS84Bounds, Capabilities: "http://cdn.tegola.io/capabilities/test-map.json?debug=true", - Tiles: []string{ - "http://cdn.tegola.io/maps/test-map/{z}/{x}/{y}.pbf?debug=true", + Tiles: []server.TileURLTemplate{ + { + Scheme: "http", + Host: "cdn.tegola.io", + MapName: "test-map", + Query: url.Values{ + server.QueryKeyDebug: []string{"true"}, + }, + }, }, Layers: []server.CapabilitiesLayer{ { Name: testLayer1.MVTName(), - Tiles: []string{ - fmt.Sprintf("http://cdn.tegola.io/maps/test-map/%v/{z}/{x}/{y}.pbf?debug=true", testLayer1.MVTName()), + Tiles: []server.TileURLTemplate{ + { + Scheme: "http", + Host: "cdn.tegola.io", + MapName: "test-map", + LayerName: testLayer1.MVTName(), + Query: url.Values{ + server.QueryKeyDebug: []string{"true"}, + }, + }, }, MinZoom: testLayer1.MinZoom, MaxZoom: testLayer3.MaxZoom, // layer 1 and layer 3 share a name in our test so the zoom range includes the entire zoom range }, { Name: "test-layer-2-name", - Tiles: []string{ - fmt.Sprintf("http://cdn.tegola.io/maps/test-map/%v/{z}/{x}/{y}.pbf?debug=true", testLayer2.MVTName()), + Tiles: []server.TileURLTemplate{ + { + Scheme: "http", + Host: "cdn.tegola.io", + MapName: "test-map", + LayerName: testLayer2.MVTName(), + Query: url.Values{ + server.QueryKeyDebug: []string{"true"}, + }, + }, }, MinZoom: testLayer2.MinZoom, MaxZoom: testLayer2.MaxZoom, }, { Name: "debug-tile-outline", - Tiles: []string{ - "http://cdn.tegola.io/maps/test-map/debug-tile-outline/{z}/{x}/{y}.pbf?debug=true", + Tiles: []server.TileURLTemplate{ + { + Scheme: "http", + Host: "cdn.tegola.io", + MapName: "test-map", + LayerName: "debug-tile-outline", + Query: url.Values{ + server.QueryKeyDebug: []string{"true"}, + }, + }, }, MinZoom: 0, MaxZoom: atlas.MaxZoom, }, { Name: "debug-tile-center", - Tiles: []string{ - "http://cdn.tegola.io/maps/test-map/debug-tile-center/{z}/{x}/{y}.pbf?debug=true", + Tiles: []server.TileURLTemplate{ + { + Scheme: "http", + Host: "cdn.tegola.io", + MapName: "test-map", + LayerName: "debug-tile-center", + Query: url.Values{ + server.QueryKeyDebug: []string{"true"}, + }, + }, }, MinZoom: 0, MaxZoom: atlas.MaxZoom, @@ -167,22 +211,36 @@ func TestHandleCapabilities(t *testing.T) { Center: [3]float64{1.0, 2.0, 3.0}, Bounds: tegola.WGS84Bounds, Capabilities: "http://localhost:8080/capabilities/test-map.json", - Tiles: []string{ - "http://localhost:8080/maps/test-map/{z}/{x}/{y}.pbf", + Tiles: []server.TileURLTemplate{ + { + Scheme: "http", + Host: "localhost:8080", + MapName: "test-map", + }, }, Layers: []server.CapabilitiesLayer{ { Name: testLayer1.MVTName(), - Tiles: []string{ - fmt.Sprintf("http://localhost:8080/maps/test-map/%v/{z}/{x}/{y}.pbf", testLayer1.MVTName()), + Tiles: []server.TileURLTemplate{ + { + Scheme: "http", + Host: "localhost:8080", + MapName: "test-map", + LayerName: testLayer1.MVTName(), + }, }, MinZoom: testLayer1.MinZoom, MaxZoom: testLayer3.MaxZoom, // layer 1 and layer 3 share a name in our test so the zoom range includes the entire zoom range }, { Name: testLayer2.MVTName(), - Tiles: []string{ - fmt.Sprintf("http://localhost:8080/maps/test-map/%v/{z}/{x}/{y}.pbf", testLayer2.MVTName()), + Tiles: []server.TileURLTemplate{ + { + Scheme: "http", + Host: "localhost:8080", + MapName: "test-map", + LayerName: testLayer2.MVTName(), + }, }, MinZoom: testLayer2.MinZoom, MaxZoom: testLayer2.MaxZoom, @@ -195,9 +253,11 @@ func TestHandleCapabilities(t *testing.T) { "unset port set host": { // With hostname set in config, port unset in config, and no port in request uri, // urls should have host from config and no port: "cdn.tegola.io" - hostname: "cdn.tegola.io", - port: "none", // Set to none or port 8080 from uri will be used. - uri: "http://localhost/capabilities?debug=true", + hostname: &url.URL{ + Host: "cdn.tegola.io", + }, + port: "none", // Set to none or port 8080 from uri will be used. + uri: "http://localhost/capabilities?debug=true", expected: server.Capabilities{ Version: serverVersion, Maps: []server.CapabilitiesMap{ @@ -207,38 +267,87 @@ func TestHandleCapabilities(t *testing.T) { Center: [3]float64{1.0, 2.0, 3.0}, Bounds: tegola.WGS84Bounds, Capabilities: "http://cdn.tegola.io/capabilities/test-map.json?debug=true", - Tiles: []string{ - "http://cdn.tegola.io/maps/test-map/{z}/{x}/{y}.pbf?debug=true", + Tiles: []server.TileURLTemplate{ + { + Scheme: "http", + Host: "cdn.tegola.io", + MapName: "test-map", + Query: url.Values{ + server.QueryKeyDebug: []string{ + "true", + }, + }, + }, }, Layers: []server.CapabilitiesLayer{ { Name: testLayer1.MVTName(), - Tiles: []string{ - fmt.Sprintf("http://cdn.tegola.io/maps/test-map/%v/{z}/{x}/{y}.pbf?debug=true", testLayer1.MVTName()), + Tiles: []server.TileURLTemplate{ + { + Scheme: "http", + Host: "cdn.tegola.io", + MapName: "test-map", + LayerName: testLayer1.MVTName(), + Query: url.Values{ + server.QueryKeyDebug: []string{ + "true", + }, + }, + }, }, MinZoom: testLayer1.MinZoom, MaxZoom: testLayer3.MaxZoom, // layer 1 and layer 3 share a name in our test so the zoom range includes the entire zoom range }, { Name: "test-layer-2-name", - Tiles: []string{ - fmt.Sprintf("http://cdn.tegola.io/maps/test-map/%v/{z}/{x}/{y}.pbf?debug=true", testLayer2.MVTName()), + Tiles: []server.TileURLTemplate{ + { + Scheme: "http", + Host: "cdn.tegola.io", + MapName: "test-map", + LayerName: testLayer2.MVTName(), + Query: url.Values{ + server.QueryKeyDebug: []string{ + "true", + }, + }, + }, }, MinZoom: testLayer2.MinZoom, MaxZoom: testLayer2.MaxZoom, }, { Name: "debug-tile-outline", - Tiles: []string{ - "http://cdn.tegola.io/maps/test-map/debug-tile-outline/{z}/{x}/{y}.pbf?debug=true", + Tiles: []server.TileURLTemplate{ + { + Scheme: "http", + Host: "cdn.tegola.io", + MapName: "test-map", + LayerName: "debug-tile-outline", + Query: url.Values{ + server.QueryKeyDebug: []string{ + "true", + }, + }, + }, }, MinZoom: 0, MaxZoom: atlas.MaxZoom, }, { Name: "debug-tile-center", - Tiles: []string{ - "http://cdn.tegola.io/maps/test-map/debug-tile-center/{z}/{x}/{y}.pbf?debug=true", + Tiles: []server.TileURLTemplate{ + { + Scheme: "http", + Host: "cdn.tegola.io", + MapName: "test-map", + LayerName: "debug-tile-center", + Query: url.Values{ + server.QueryKeyDebug: []string{ + "true", + }, + }, + }, }, MinZoom: 0, MaxZoom: atlas.MaxZoom, @@ -251,8 +360,10 @@ func TestHandleCapabilities(t *testing.T) { "config set hostname unset port": { // With hostname set and port unset in config, urls should have host from config and // port from uri: "cdn.tegola.io:8080" - hostname: "cdn.tegola.io", - uri: "http://localhost:8080/capabilities?debug=true", + hostname: &url.URL{ + Host: "cdn.tegola.io", + }, + uri: "http://localhost:8080/capabilities?debug=true", expected: server.Capabilities{ Version: serverVersion, Maps: []server.CapabilitiesMap{ @@ -262,38 +373,77 @@ func TestHandleCapabilities(t *testing.T) { Center: [3]float64{1.0, 2.0, 3.0}, Bounds: tegola.WGS84Bounds, Capabilities: "http://cdn.tegola.io/capabilities/test-map.json?debug=true", - Tiles: []string{ - "http://cdn.tegola.io/maps/test-map/{z}/{x}/{y}.pbf?debug=true", + Tiles: []server.TileURLTemplate{ + { + Scheme: "http", + Host: "cdn.tegola.io", + MapName: "test-map", + Query: url.Values{ + server.QueryKeyDebug: []string{"true"}, + }, + }, }, Layers: []server.CapabilitiesLayer{ { Name: testLayer1.MVTName(), - Tiles: []string{ - fmt.Sprintf("http://cdn.tegola.io/maps/test-map/%v/{z}/{x}/{y}.pbf?debug=true", testLayer1.MVTName()), + Tiles: []server.TileURLTemplate{ + { + Scheme: "http", + Host: "cdn.tegola.io", + MapName: "test-map", + LayerName: testLayer1.MVTName(), + Query: url.Values{ + server.QueryKeyDebug: []string{"true"}, + }, + }, }, MinZoom: testLayer1.MinZoom, MaxZoom: testLayer3.MaxZoom, // layer 1 and layer 3 share a name in our test so the zoom range includes the entire zoom range }, { Name: "test-layer-2-name", - Tiles: []string{ - fmt.Sprintf("http://cdn.tegola.io/maps/test-map/%v/{z}/{x}/{y}.pbf?debug=true", testLayer2.MVTName()), + Tiles: []server.TileURLTemplate{ + { + Scheme: "http", + Host: "cdn.tegola.io", + MapName: "test-map", + LayerName: testLayer2.MVTName(), + Query: url.Values{ + server.QueryKeyDebug: []string{"true"}, + }, + }, }, MinZoom: testLayer2.MinZoom, MaxZoom: testLayer2.MaxZoom, }, { Name: "debug-tile-outline", - Tiles: []string{ - "http://cdn.tegola.io/maps/test-map/debug-tile-outline/{z}/{x}/{y}.pbf?debug=true", + Tiles: []server.TileURLTemplate{ + { + Scheme: "http", + Host: "cdn.tegola.io", + MapName: "test-map", + LayerName: "debug-tile-outline", + Query: url.Values{ + server.QueryKeyDebug: []string{"true"}, + }, + }, }, MinZoom: 0, MaxZoom: atlas.MaxZoom, }, { Name: "debug-tile-center", - Tiles: []string{ - "http://cdn.tegola.io/maps/test-map/debug-tile-center/{z}/{x}/{y}.pbf?debug=true", + Tiles: []server.TileURLTemplate{ + { + Scheme: "http", + Host: "cdn.tegola.io", + MapName: "test-map", + LayerName: "debug-tile-center", + Query: url.Values{ + server.QueryKeyDebug: []string{"true"}, + }, + }, }, MinZoom: 0, MaxZoom: atlas.MaxZoom, @@ -325,3 +475,6 @@ func TestHandleCapabilitiesCORS(t *testing.T) { t.Run(name, CORSTest(tc)) } } + +// expected {Version:0.10.0 Maps:[{Name:test-map Attribution:test attribution Bounds:0x1027ad5e0 Center:[1 2 3] Tiles:[http://localhost:8080/maps/test-map/{z}/{x}/{y}.pbf] Capabilities:http://localhost:8080/capabilities/test-map.json Layers:[{Name:test-layer Tiles:[http://localhost:8080/maps/test-map/test-layer/{z}/{x}/{y}.pbf] MinZoom:4 MaxZoom:20} {Name:test-layer-2-name Tiles:[http://localhost:8080/maps/test-map/test-layer-2-name/{z}/{x}/{y}.pbf] MinZoom:10 MaxZoom:15}]}]} +// got {Version:0.10.0 Maps:[{Name:test-map Attribution:test attribution Bounds:0x1400050c7a0 Center:[1 2 3] Tiles:[http://localhost:8080/maps/test-map/{z}/{x}/{y}.pbf] Capabilities:http://localhost:8080/capabilities/test-map.json Layers:[{Name:test-layer Tiles:[http://localhost:8080/maps/test-map/test-layer/{z}/{x}/{y}.pbf] MinZoom:4 MaxZoom:20} {Name:test-layer-2-name Tiles:[http://localhost:8080/maps/test-map/test-layer-2-name/{z}/{x}/{y}.pbf] MinZoom:10 MaxZoom:15}]}]} diff --git a/server/handle_map_capabilities.go b/server/handle_map_capabilities.go index a27d77acf..77fca757c 100644 --- a/server/handle_map_capabilities.go +++ b/server/handle_map_capabilities.go @@ -54,7 +54,7 @@ func (req HandleMapCapabilities) ServeHTTP(w http.ResponseWriter, r *http.Reques Attribution: &m.Attribution, Bounds: m.Bounds.Extent(), Center: m.Center, - Format: "pbf", + Format: TileURLFileFormat, Name: &m.Name, Scheme: tilejson.SchemeXYZ, TileJSON: tilejson.Version, @@ -68,8 +68,8 @@ func (req HandleMapCapabilities) ServeHTTP(w http.ResponseWriter, r *http.Reques debugQuery := url.Values{} // if we have a debug param add it to our URLs - if query.Get("debug") == "true" { - debugQuery.Set("debug", "true") + if query.Get(QueryKeyDebug) == "true" { + debugQuery.Set(QueryKeyDebug, "true") // update our map to include the debug layers m = m.AddDebugLayers() @@ -125,7 +125,13 @@ func (req HandleMapCapabilities) ServeHTTP(w http.ResponseWriter, r *http.Reques MinZoom: m.Layers[i].MinZoom, MaxZoom: m.Layers[i].MaxZoom, Tiles: []string{ - buildCapabilitiesURL(r, []string{"maps", req.mapName, m.Layers[i].MVTName(), "{z}/{x}/{y}.pbf"}, debugQuery), + TileURLTemplate{ + Scheme: scheme(r), + Host: hostName(r).Host, + MapName: req.mapName, + LayerName: m.Layers[i].MVTName(), + Query: debugQuery, + }.String(), }, } @@ -145,7 +151,12 @@ func (req HandleMapCapabilities) ServeHTTP(w http.ResponseWriter, r *http.Reques tileJSON.VectorLayers = append(tileJSON.VectorLayers, layer) } - tileURL := buildCapabilitiesURL(r, []string{"maps", req.mapName, "{z}/{x}/{y}.pbf"}, debugQuery) + tileURL := TileURLTemplate{ + Scheme: scheme(r), + Host: hostName(r).Host, + MapName: req.mapName, + Query: debugQuery, + }.String() // build our URL scheme for the tile grid tileJSON.Tiles = append(tileJSON.Tiles, tileURL) diff --git a/server/handle_map_capabilities_test.go b/server/handle_map_capabilities_test.go index 18575bf0f..d97f5a993 100644 --- a/server/handle_map_capabilities_test.go +++ b/server/handle_map_capabilities_test.go @@ -2,10 +2,10 @@ package server_test import ( "encoding/json" - "fmt" - "io/ioutil" + "io" "net/http" "net/http/httptest" + "net/url" "reflect" "testing" @@ -17,7 +17,7 @@ import ( func TestHandleMapCapabilities(t *testing.T) { type tcase struct { handler http.Handler - hostName string + hostName *url.URL port string uri string reqMethod string @@ -48,7 +48,7 @@ func TestHandleMapCapabilities(t *testing.T) { return } - bytes, err := ioutil.ReadAll(w.Body) + bytes, err := io.ReadAll(w.Body) if err != nil { t.Errorf("err reading response body: %v", err) return @@ -65,21 +65,20 @@ func TestHandleMapCapabilities(t *testing.T) { t.Errorf("response body and expected do not match \n%+v\n%+v", tc.expected, tileJSON) return } - } } - testcases := []tcase{ - { + tests := map[string]tcase{ + "happy path": { handler: server.HandleCapabilities{}, - hostName: "", + hostName: nil, uri: "http://localhost:8080/capabilities/test-map.json", - reqMethod: "GET", + reqMethod: http.MethodGet, expected: tilejson.TileJSON{ Attribution: &testMapAttribution, Bounds: [4]float64{-180.0, -85.0511, 180.0, 85.0511}, Center: testMapCenter, - Format: "pbf", + Format: server.TileURLFileFormat, MinZoom: testLayer1.MinZoom, MaxZoom: testLayer3.MaxZoom, // the max zoom for the test group is in layer 3 Name: &testMapName, @@ -87,7 +86,11 @@ func TestHandleMapCapabilities(t *testing.T) { Scheme: tilejson.SchemeXYZ, TileJSON: tilejson.Version, Tiles: []string{ - "http://localhost:8080/maps/test-map/{z}/{x}/{y}.pbf", + server.TileURLTemplate{ + Scheme: "http", + Host: "localhost:8080", + MapName: "test-map", + }.String(), }, Grids: []string{}, Data: []string{}, @@ -104,7 +107,12 @@ func TestHandleMapCapabilities(t *testing.T) { MinZoom: testLayer1.MinZoom, MaxZoom: testLayer3.MaxZoom, // layer 1 and layer 3 share a name in our test so the zoom range includes the entire zoom range Tiles: []string{ - fmt.Sprintf("http://localhost:8080/maps/test-map/%v/{z}/{x}/{y}.pbf", testLayer1.MVTName()), + server.TileURLTemplate{ + Scheme: "http", + Host: "localhost:8080", + MapName: "test-map", + LayerName: testLayer1.MVTName(), + }.String(), }, }, { @@ -116,15 +124,155 @@ func TestHandleMapCapabilities(t *testing.T) { MinZoom: testLayer2.MinZoom, MaxZoom: testLayer2.MaxZoom, Tiles: []string{ - fmt.Sprintf("http://localhost:8080/maps/test-map/%v/{z}/{x}/{y}.pbf", testLayer2.MVTName()), + server.TileURLTemplate{ + Scheme: "http", + Host: "localhost:8080", + MapName: "test-map", + LayerName: testLayer2.MVTName(), + }.String(), }, }, }, }, }, - { - handler: server.HandleCapabilities{}, - hostName: "cdn.tegola.io", + "with hostname": { + handler: server.HandleCapabilities{}, + hostName: &url.URL{ + Host: "cdn.tegola.io", + }, + port: "none", + uri: "http://localhost:8080/capabilities/test-map.json?debug=true", + reqMethod: "GET", + expected: tilejson.TileJSON{ + Attribution: &testMapAttribution, + Bounds: [4]float64{-180.0, -85.0511, 180.0, 85.0511}, + Center: testMapCenter, + Format: server.TileURLFileFormat, + MinZoom: 0, + MaxZoom: atlas.MaxZoom, + Name: &testMapName, + Description: nil, + Scheme: tilejson.SchemeXYZ, + TileJSON: tilejson.Version, + Tiles: []string{ + server.TileURLTemplate{ + Scheme: "http", + Host: "cdn.tegola.io", + MapName: "test-map", + Query: url.Values{ + server.QueryKeyDebug: []string{ + "true", + }, + }, + }.String(), + }, + Grids: []string{}, + Data: []string{}, + Version: "1.0.0", + Template: nil, + Legend: nil, + VectorLayers: []tilejson.VectorLayer{ + { + Version: 2, + Extent: 4096, + ID: testLayer1.MVTName(), + Name: testLayer1.MVTName(), + GeometryType: tilejson.GeomTypePoint, + MinZoom: testLayer1.MinZoom, + MaxZoom: testLayer3.MaxZoom, // layer 1 and layer 3 share a name in our test so the zoom range includes the entire zoom range + Tiles: []string{ + server.TileURLTemplate{ + Scheme: "http", + Host: "cdn.tegola.io", + MapName: "test-map", + LayerName: testLayer1.MVTName(), + Query: url.Values{ + server.QueryKeyDebug: []string{ + "true", + }, + }, + }.String(), + }, + }, + { + Version: 2, + Extent: 4096, + ID: testLayer2.MVTName(), + Name: testLayer2.MVTName(), + GeometryType: tilejson.GeomTypeLine, + MinZoom: testLayer2.MinZoom, + MaxZoom: testLayer2.MaxZoom, + Tiles: []string{ + server.TileURLTemplate{ + Scheme: "http", + Host: "cdn.tegola.io", + MapName: "test-map", + LayerName: testLayer2.MVTName(), + Query: url.Values{ + server.QueryKeyDebug: []string{ + "true", + }, + }, + }.String(), + }, + }, + { + Version: 2, + Extent: 4096, + ID: "debug-tile-outline", + Name: "debug-tile-outline", + GeometryType: tilejson.GeomTypeLine, + MinZoom: 0, + MaxZoom: atlas.MaxZoom, + Tiles: []string{ + server.TileURLTemplate{ + Scheme: "http", + Host: "cdn.tegola.io", + MapName: "test-map", + LayerName: "debug-tile-outline", + Query: url.Values{ + server.QueryKeyDebug: []string{ + "true", + }, + }, + }.String(), + }, + }, + { + Version: 2, + Extent: 4096, + ID: "debug-tile-center", + Name: "debug-tile-center", + GeometryType: tilejson.GeomTypePoint, + MinZoom: 0, + MaxZoom: atlas.MaxZoom, + Tiles: []string{ + server.TileURLTemplate{ + Scheme: "http", + Host: "cdn.tegola.io", + MapName: "test-map", + LayerName: "debug-tile-center", + Query: url.Values{ + server.QueryKeyDebug: []string{ + "true", + }, + }, + }.String(), + }, + }, + }, + }, + }, + // https://github.com/go-spatial/tegola/issues/994 + "hostname with scheme": { + handler: server.HandleCapabilities{}, + hostName: &url.URL{ + // The scheme is determined at request time. if the + // user sets the scheme on the hostname, it will be + // ignored + Scheme: "https", + Host: "cdn.tegola.io", + }, port: "none", uri: "http://localhost:8080/capabilities/test-map.json?debug=true", reqMethod: "GET", @@ -132,7 +280,7 @@ func TestHandleMapCapabilities(t *testing.T) { Attribution: &testMapAttribution, Bounds: [4]float64{-180.0, -85.0511, 180.0, 85.0511}, Center: testMapCenter, - Format: "pbf", + Format: server.TileURLFileFormat, MinZoom: 0, MaxZoom: atlas.MaxZoom, Name: &testMapName, @@ -140,7 +288,16 @@ func TestHandleMapCapabilities(t *testing.T) { Scheme: tilejson.SchemeXYZ, TileJSON: tilejson.Version, Tiles: []string{ - "http://cdn.tegola.io/maps/test-map/{z}/{x}/{y}.pbf?debug=true", + server.TileURLTemplate{ + Scheme: "http", + Host: "cdn.tegola.io", + MapName: "test-map", + Query: url.Values{ + server.QueryKeyDebug: []string{ + "true", + }, + }, + }.String(), }, Grids: []string{}, Data: []string{}, @@ -157,7 +314,17 @@ func TestHandleMapCapabilities(t *testing.T) { MinZoom: testLayer1.MinZoom, MaxZoom: testLayer3.MaxZoom, // layer 1 and layer 3 share a name in our test so the zoom range includes the entire zoom range Tiles: []string{ - fmt.Sprintf("http://cdn.tegola.io/maps/test-map/%v/{z}/{x}/{y}.pbf?debug=true", testLayer1.MVTName()), + server.TileURLTemplate{ + Scheme: "http", + Host: "cdn.tegola.io", + MapName: "test-map", + LayerName: testLayer1.MVTName(), + Query: url.Values{ + server.QueryKeyDebug: []string{ + "true", + }, + }, + }.String(), }, }, { @@ -169,7 +336,17 @@ func TestHandleMapCapabilities(t *testing.T) { MinZoom: testLayer2.MinZoom, MaxZoom: testLayer2.MaxZoom, Tiles: []string{ - fmt.Sprintf("http://cdn.tegola.io/maps/test-map/%v/{z}/{x}/{y}.pbf?debug=true", testLayer2.MVTName()), + server.TileURLTemplate{ + Scheme: "http", + Host: "cdn.tegola.io", + MapName: "test-map", + LayerName: testLayer2.MVTName(), + Query: url.Values{ + server.QueryKeyDebug: []string{ + "true", + }, + }, + }.String(), }, }, { @@ -181,7 +358,17 @@ func TestHandleMapCapabilities(t *testing.T) { MinZoom: 0, MaxZoom: atlas.MaxZoom, Tiles: []string{ - "http://cdn.tegola.io/maps/test-map/debug-tile-outline/{z}/{x}/{y}.pbf?debug=true", + server.TileURLTemplate{ + Scheme: "http", + Host: "cdn.tegola.io", + MapName: "test-map", + LayerName: "debug-tile-outline", + Query: url.Values{ + server.QueryKeyDebug: []string{ + "true", + }, + }, + }.String(), }, }, { @@ -193,7 +380,17 @@ func TestHandleMapCapabilities(t *testing.T) { MinZoom: 0, MaxZoom: atlas.MaxZoom, Tiles: []string{ - "http://cdn.tegola.io/maps/test-map/debug-tile-center/{z}/{x}/{y}.pbf?debug=true", + server.TileURLTemplate{ + Scheme: "http", + Host: "cdn.tegola.io", + MapName: "test-map", + LayerName: "debug-tile-center", + Query: url.Values{ + server.QueryKeyDebug: []string{ + "true", + }, + }, + }.String(), }, }, }, @@ -201,8 +398,8 @@ func TestHandleMapCapabilities(t *testing.T) { }, } - for i, tc := range testcases { - t.Run(fmt.Sprintf("%d", i), fn(tc)) + for name, tc := range tests { + t.Run(name, fn(tc)) } } diff --git a/server/handle_map_layer_zxy.go b/server/handle_map_layer_zxy.go index 652116396..093195a45 100644 --- a/server/handle_map_layer_zxy.go +++ b/server/handle_map_layer_zxy.go @@ -8,18 +8,18 @@ import ( "strconv" "strings" - "github.com/go-spatial/geom" - "github.com/go-spatial/proj" - "github.com/go-spatial/tegola/observability" - "github.com/go-spatial/tegola/provider" - "github.com/dimfeld/httptreemux" + "github.com/go-spatial/geom" "github.com/go-spatial/geom/encoding/mvt" "github.com/go-spatial/geom/slippy" + "github.com/go-spatial/proj" + "github.com/go-spatial/tegola" "github.com/go-spatial/tegola/atlas" "github.com/go-spatial/tegola/internal/log" "github.com/go-spatial/tegola/maths" + "github.com/go-spatial/tegola/observability" + "github.com/go-spatial/tegola/provider" ) var ( @@ -103,7 +103,7 @@ func (req *HandleMapLayerZXY) parseURI(r *http.Request) error { } // check for debug request - if r.URL.Query().Get("debug") == "true" { + if r.URL.Query().Get(QueryKeyDebug) == "true" { req.debug = true } diff --git a/server/handle_map_layer_zxy_test.go b/server/handle_map_layer_zxy_test.go index 6528cbf66..212d02046 100644 --- a/server/handle_map_layer_zxy_test.go +++ b/server/handle_map_layer_zxy_test.go @@ -1,15 +1,16 @@ package server_test import ( - "io/ioutil" + "io" "net/http" "reflect" "strings" "testing" + "github.com/golang/protobuf/proto" + vectorTile "github.com/go-spatial/geom/encoding/mvt/vector_tile" "github.com/go-spatial/tegola/atlas" - "github.com/golang/protobuf/proto" ) type MapHandlerTCase struct { @@ -29,7 +30,7 @@ func MapHandlerTester(tc MapHandlerTCase) func(t *testing.T) { if a == nil { a = newTestMapWithLayers(testLayer1, testLayer2, testLayer3) } - w, _, err := doRequest(a, tc.method, tc.uri, nil) + w, _, err := doRequest(t, a, tc.method, tc.uri, nil) if err != nil { t.Fatalf("doRequest: %v", err) } @@ -60,7 +61,7 @@ func MapHandlerTester(tc MapHandlerTCase) func(t *testing.T) { var tile vectorTile.Tile var responseBodyBytes []byte - responseBodyBytes, err = ioutil.ReadAll(w.Body) + responseBodyBytes, err = io.ReadAll(w.Body) if err != nil { t.Errorf("reading response body, expected nil got %v", err) return diff --git a/server/handle_map_style.go b/server/handle_map_style.go index 6b65d4690..ff4a89bf2 100644 --- a/server/handle_map_style.go +++ b/server/handle_map_style.go @@ -2,16 +2,16 @@ package server import ( "encoding/json" - "net/http" "net/url" + "path" "strconv" "strings" "github.com/dimfeld/httptreemux" + "github.com/go-spatial/geom" "gopkg.in/go-playground/colors.v1" - "github.com/go-spatial/geom" "github.com/go-spatial/tegola/atlas" "github.com/go-spatial/tegola/internal/log" "github.com/go-spatial/tegola/mapbox/style" @@ -28,7 +28,8 @@ type HandleMapStyle struct { // tileJSON spec (https://github.com/mapbox/tilejson-spec/tree/master/2.1.0) // // URI scheme: /capabilities/:map_name.json -// map_name - map name in the config file +// +// map_name - map name in the config file func (req HandleMapStyle) ServeHTTP(w http.ResponseWriter, r *http.Request) { var err error @@ -56,8 +57,8 @@ func (req HandleMapStyle) ServeHTTP(w http.ResponseWriter, r *http.Request) { // if we have a debug param add it to our URLs debugQuery := url.Values{} - if r.URL.Query().Get("debug") == "true" { - debugQuery.Set("debug", "true") + if r.URL.Query().Get(QueryKeyDebug) == "true" { + debugQuery.Set(QueryKeyDebug, "true") // update our map to include the debug layers m = m.AddDebugLayers() @@ -71,7 +72,12 @@ func (req HandleMapStyle) ServeHTTP(w http.ResponseWriter, r *http.Request) { Sources: map[string]style.Source{ req.mapName: { Type: style.SourceTypeVector, - URL: buildCapabilitiesURL(r, []string{"capabilities", req.mapName + ".json"}, debugQuery), + URL: (&url.URL{ + Scheme: scheme(r), + Host: hostName(r).Host, + Path: path.Join(URIPrefix, "capabilities", req.mapName+".json"), + RawQuery: debugQuery.Encode(), + }).String(), }, }, Layers: []style.Layer{}, diff --git a/server/handle_map_style_test.go b/server/handle_map_style_test.go index 8cc3aefa3..3c75d5ac8 100644 --- a/server/handle_map_style_test.go +++ b/server/handle_map_style_test.go @@ -2,30 +2,29 @@ package server_test import ( "encoding/json" - "fmt" - "io/ioutil" "net/http" - "reflect" + "net/url" + "path" "testing" "github.com/go-spatial/tegola/mapbox/style" "github.com/go-spatial/tegola/server" + "github.com/go-test/deep" ) func TestHandleMapStyle(t *testing.T) { type tcase struct { handler http.Handler uriPrefix string - hostName string - port string uri string uriPattern string - reqMethod string expected style.Root } // config params this test relies on - server.HostName = serverHostName + server.HostName = &url.URL{ + Host: serverHostName, + } fn := func(tc tcase) func(t *testing.T) { return func(t *testing.T) { @@ -37,34 +36,22 @@ func TestHandleMapStyle(t *testing.T) { server.URIPrefix = "/" } - w, _, err := doRequest(nil, tc.reqMethod, tc.uri, nil) + resp, _, err := doRequest(t, nil, http.MethodGet, tc.uri, nil) if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - if w.Code != http.StatusOK { - t.Errorf("handler returned wrong status code: got (%v) expected (%v)", w.Code, http.StatusOK) - return + t.Fatalf("unexpected error: %s", err) } - - bytes, err := ioutil.ReadAll(w.Body) - if err != nil { - t.Errorf("err reading response body: %v", err) - return + if resp.Code != http.StatusOK { + t.Fatalf("handler returned wrong status code: got (%d) expected (%d)", resp.Code, http.StatusOK) } - var output style.Root // read the response body - if err := json.Unmarshal(bytes, &output); err != nil { - t.Errorf("unable to unmarshal JSON response body: %v", err) - return - + var output style.Root + if err := json.NewDecoder(resp.Body).Decode(&output); err != nil { + t.Fatalf("unable to unmarshal JSON response body: %s", err) } - if !reflect.DeepEqual(output, tc.expected) { - t.Errorf("failed. output \n\n %+v \n\n does not match expected \n\n %+v", output, tc.expected) - return + if diff := deep.Equal(output, tc.expected); diff != nil { + t.Fatalf("output does not match expected. diff %s", diff) } } } @@ -72,9 +59,8 @@ func TestHandleMapStyle(t *testing.T) { tests := map[string]tcase{ "default": { handler: server.HandleMapStyle{}, - uri: fmt.Sprintf("/maps/%v/style.json", testMapName), + uri: path.Join("/maps", testMapName, "style.json"), uriPattern: "/maps/:map_name/style.json", - reqMethod: "GET", expected: style.Root{ Name: testMapName, Version: style.Version, @@ -83,7 +69,11 @@ func TestHandleMapStyle(t *testing.T) { Sources: map[string]style.Source{ testMapName: { Type: style.SourceTypeVector, - URL: fmt.Sprintf("http://%v/capabilities/%v.json", serverHostName, testMapName), + URL: (&url.URL{ + Scheme: "http", + Host: serverHostName, + Path: path.Join(server.URIPrefix, "capabilities", testMapName+".json"), + }).String(), }, }, Layers: []style.Layer{ @@ -118,9 +108,8 @@ func TestHandleMapStyle(t *testing.T) { "uri prefix set": { handler: server.HandleMapStyle{}, uriPrefix: "/tegola", - uri: fmt.Sprintf("/tegola/maps/%v/style.json", testMapName), + uri: path.Join("/tegola", "maps", testMapName, "style.json"), uriPattern: "/tegola/maps/:map_name/style.json", - reqMethod: "GET", expected: style.Root{ Name: testMapName, Version: style.Version, @@ -129,7 +118,11 @@ func TestHandleMapStyle(t *testing.T) { Sources: map[string]style.Source{ testMapName: { Type: style.SourceTypeVector, - URL: fmt.Sprintf("http://%v/tegola/capabilities/%v.json", serverHostName, testMapName), + URL: (&url.URL{ + Scheme: "http", + Host: serverHostName, + Path: path.Join(server.URIPrefix, "tegola", "capabilities", testMapName+".json"), + }).String(), }, }, Layers: []style.Layer{ @@ -171,7 +164,7 @@ func TestHandleMapStyle(t *testing.T) { func TestHandleMapStyleCORS(t *testing.T) { tests := map[string]CORSTestCase{ "1": { - uri: fmt.Sprintf("/maps/%v/style.json", testMapName), + uri: path.Join("maps", testMapName, "style.json"), }, } diff --git a/server/middleware_tile_cache_test.go b/server/middleware_tile_cache_test.go index 7563839fe..6ac378e66 100644 --- a/server/middleware_tile_cache_test.go +++ b/server/middleware_tile_cache_test.go @@ -29,7 +29,7 @@ func TestMiddlewareTileCacheHandler(t *testing.T) { cacher, _ := memory.New(nil) a.SetCache(cacher) - w, router, err := doRequest(a, "GET", tc.uri, nil) + w, router, err := doRequest(t, a, http.MethodGet, tc.uri, nil) if err != nil { t.Errorf("error making request, expected nil got %v", err) return @@ -100,7 +100,7 @@ func TestMiddlewareTileCacheHandlerIgnoreParams(t *testing.T) { cacher, _ := memory.New(nil) a.SetCache(cacher) - w, router, err := doRequest(a, "GET", tc.uri, nil) + w, router, err := doRequest(t, a, http.MethodGet, tc.uri, nil) if err != nil { t.Errorf("error making request, expected nil got %v", err) return diff --git a/server/server.go b/server/server.go index 51aba0e77..3cba13fbb 100644 --- a/server/server.go +++ b/server/server.go @@ -2,25 +2,25 @@ package server import ( - "fmt" "net/http" "net/url" - "path" - - "github.com/go-spatial/tegola/internal/build" - - "github.com/go-spatial/tegola/observability" "github.com/dimfeld/httptreemux" "github.com/go-spatial/tegola/atlas" + "github.com/go-spatial/tegola/internal/build" "github.com/go-spatial/tegola/internal/log" + "github.com/go-spatial/tegola/observability" ) const ( // MaxTileSize is 500k. Currently, just throws a warning when tile // is larger than MaxTileSize MaxTileSize = 500000 + + // QueryKeyDebug is a common query string key used throughout the pacakge + // the value should always be a boolean + QueryKeyDebug = "debug" ) var ( @@ -30,7 +30,7 @@ var ( // HostName is the name of the host to use for construction of URLS. // configurable via the tegola config.toml file (set in main.go) - HostName string + HostName *url.URL // Port is the port the server is listening on, used for construction of URLS. // configurable via the tegola config.toml file (set in main.go) @@ -135,15 +135,15 @@ func Start(a *atlas.Atlas, port string) *http.Server { return srv } -// hostName determines weather to use an user defined HostName +// hostName determines whether to use an user defined HostName // or the host from the incoming request -func hostName(r *http.Request) string { +func hostName(r *http.Request) *url.URL { // if the HostName has been configured, don't mutate it - if HostName != "" { + if HostName != nil { return HostName } - return r.Host + return r.URL } // various checks to determine if the request is http or https. the scheme is needed for the TileURLs @@ -164,37 +164,15 @@ func scheme(r *http.Request) string { // URLRoot builds a string containing the scheme, host and port based on a combination of user defined values, // headers and request parameters. The function is public so it can be overridden for other implementations. var URLRoot = func(r *http.Request) *url.URL { - root := url.URL{ + return &url.URL{ Scheme: scheme(r), - Host: hostName(r), - } - - return &root -} - -// buildCapabilitiesURL is responsible for building the various URLs which are returned by -// the capabilities endpoints using the request, uri parts, and query params the function -// will determine the protocol host:port and URI prefix that need to be included based on -// user defined configurations and request context -func buildCapabilitiesURL(r *http.Request, uriParts []string, query url.Values) string { - uri := path.Join(uriParts...) - q := query.Encode() - if q != "" { - // prepend our query identifier - q = "?" + q + Host: hostName(r).Host, } - - // usually the url.URL package would be used for building the URL, but the - // uri template for the tiles contains characters that the package does not like: - // {z}/{x}/{y}. These values are escaped during the String() call which does not - // work for the capabilities URLs. - return fmt.Sprintf("%v%v%v", URLRoot(r), path.Join(URIPrefix, uri), q) } // corsHandler is used to respond to all OPTIONS requests for registered routes func corsHandler(w http.ResponseWriter, _ *http.Request, _ map[string]string) { setHeaders(w) - return } // setHeaders sets default headers and user defined headers diff --git a/server/server_cors_test.go b/server/server_cors_test.go index 68ed23306..196324284 100644 --- a/server/server_cors_test.go +++ b/server/server_cors_test.go @@ -3,6 +3,7 @@ package server_test import ( "net/http" "net/http/httptest" + "net/url" "reflect" "testing" @@ -24,7 +25,9 @@ func CORSTest(tc CORSTestCase) func(*testing.T) { return func(t *testing.T) { var err error - server.HostName = tc.hostname + server.HostName = &url.URL{ + Host: tc.hostname, + } server.Port = tc.port server.URIPrefix = "/" diff --git a/server/server_internal_test.go b/server/server_internal_test.go index a4a38ad06..ef64dc7ec 100644 --- a/server/server_internal_test.go +++ b/server/server_internal_test.go @@ -17,9 +17,20 @@ func TestHostName(t *testing.T) { fn := func(tc tcase) func(t *testing.T) { return func(t *testing.T) { - // set the package variable - HostName = tc.hostName - Port = tc.port + // reset the server singleton values. this is not ideal + // but is the current design of this package + HostName = nil + Port = "" + + if tc.hostName != "" { + // set the package variable + HostName = &url.URL{ + Host: tc.hostName, + } + } + if tc.port != "" { + Port = tc.port + } url, err := url.Parse(tc.url) if err != nil { @@ -29,12 +40,11 @@ func TestHostName(t *testing.T) { req := http.Request{URL: url, Host: url.Host} - output := hostName(&req) + output := hostName(&req).Host if output != tc.expected { - t.Errorf("hostname, expected %v got %v", tc.expected, output) + t.Errorf("hostname, expected (%s) got (%s)", tc.expected, output) return } - } } @@ -185,116 +195,3 @@ func TestScheme(t *testing.T) { ProxyProtocol = "" } - -func TestBuildCapabilitiesURL(t *testing.T) { - - type tcase struct { - request http.Request - uriParts []string - uriPrefix string - query url.Values - expected string - proxyProtocol string - } - - fn := func(tc tcase) func(t *testing.T) { - return func(t *testing.T) { - - if tc.uriPrefix != "" { - URIPrefix = tc.uriPrefix - } else { - URIPrefix = "/" - } - - if tc.proxyProtocol != "" { - ProxyProtocol = tc.proxyProtocol - } else { - ProxyProtocol = "" - } - - output := buildCapabilitiesURL(&tc.request, tc.uriParts, tc.query) - if output != tc.expected { - t.Errorf("expected (%v) got (%v)", tc.expected, output) - } - } - } - - tests := map[string]tcase{ - "no uri prefix no query": { - request: http.Request{ - Host: "cdn.tegola.io", - }, - uriParts: []string{"foo", "bar"}, - query: url.Values{}, - expected: "http://cdn.tegola.io/foo/bar", - }, - "uri prefix no query": { - request: http.Request{ - Host: "cdn.tegola.io", - }, - uriParts: []string{"foo", "bar"}, - uriPrefix: "/tegola", - query: url.Values{}, - expected: "http://cdn.tegola.io/tegola/foo/bar", - }, - "uri prefix and query": { - request: http.Request{ - Host: "cdn.tegola.io", - }, - uriParts: []string{"foo", "bar"}, - uriPrefix: "/tegola", - query: url.Values{ - "debug": []string{"true"}, - }, - expected: "http://cdn.tegola.io/tegola/foo/bar?debug=true", - }, - "http proxy_protocol": { - request: http.Request{ - Host: "cdn.tegola.io", - }, - uriParts: []string{"foo", "bar"}, - query: url.Values{}, - proxyProtocol: "http", - expected: "http://cdn.tegola.io/foo/bar", - }, - "https proxy_protocol": { - request: http.Request{ - Host: "cdn.tegola.io", - }, - uriParts: []string{"foo", "bar"}, - query: url.Values{}, - proxyProtocol: "https", - expected: "https://cdn.tegola.io/foo/bar", - }, - "http proxy_protocol with https Request": { - request: http.Request{ - TLS: &tls.ConnectionState{}, - Host: "cdn.tegola.io", - }, - uriParts: []string{"foo", "bar"}, - query: url.Values{}, - proxyProtocol: "http", - expected: "http://cdn.tegola.io/foo/bar", - }, - "https proxy_protocol with uri_prefix": { - request: http.Request{ - Host: "cdn.tegola.io", - }, - uriParts: []string{"foo", "bar"}, - uriPrefix: "/tegola", - query: url.Values{}, - proxyProtocol: "https", - expected: "https://cdn.tegola.io/tegola/foo/bar", - }, - } - - for name, tc := range tests { - t.Run(name, fn(tc)) - } - - // reset the URIPrefix. Ideally this would not be necessary but the server package is - // designed as a singleton right now. Eventually this will change so the tests - // don't need to consider each other - URIPrefix = "/" - ProxyProtocol = "" -} diff --git a/server/server_test.go b/server/server_test.go index 0bfef8bfb..b65aee71f 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -3,15 +3,18 @@ package server_test import ( "context" "crypto/tls" - "fmt" "io" "net/http" "net/http/httptest" + "net/url" + "os" + "reflect" "testing" "time" "github.com/dimfeld/httptreemux" "github.com/go-spatial/geom" + "github.com/go-spatial/tegola/atlas" "github.com/go-spatial/tegola/provider/test" "github.com/go-spatial/tegola/server" @@ -39,7 +42,7 @@ var testLayer1 = atlas.Layer{ MaxZoom: 9, Provider: &test.TileProvider{}, GeomType: geom.Point{}, - DefaultTags: map[string]interface{}{ + DefaultTags: map[string]any{ "foo": "bar", }, } @@ -51,7 +54,7 @@ var testLayer2 = atlas.Layer{ MaxZoom: 15, Provider: &test.TileProvider{}, GeomType: geom.Line{}, - DefaultTags: map[string]interface{}{ + DefaultTags: map[string]any{ "foo": "bar", }, } @@ -63,7 +66,7 @@ var testLayer3 = atlas.Layer{ MaxZoom: 20, Provider: &test.TileProvider{}, GeomType: geom.Point{}, - DefaultTags: map[string]interface{}{}, + DefaultTags: map[string]any{}, } func newTestMapWithLayers(layers ...atlas.Layer) *atlas.Atlas { @@ -93,28 +96,32 @@ func newTestMapWithBounds(minx, miny, maxx, maxy float64) *atlas.Atlas { return a } -func doRequest(a *atlas.Atlas, method string, uri string, body io.Reader) (w *httptest.ResponseRecorder, router *httptreemux.TreeMux, err error) { - - router = server.NewRouter(a) +func doRequest(t *testing.T, a *atlas.Atlas, method string, uri string, body io.Reader) (*httptest.ResponseRecorder, *httptreemux.TreeMux, error) { + t.Helper() // Default Method to GET if method == "" { - method = "GET" + method = http.MethodGet } r, err := http.NewRequest(method, uri, body) if err != nil { return nil, nil, err } - w = httptest.NewRecorder() + + router := server.NewRouter(a) + w := httptest.NewRecorder() router.ServeHTTP(w, r) + return w, router, nil } // pre test setup phase -func init() { +func TestMain(m *testing.M) { server.Version = serverVersion - server.HostName = serverHostName + server.HostName = &url.URL{ + Host: serverHostName, + } testMap := atlas.NewWebMercatorMap(testMapName) testMap.Attribution = testMapAttribution @@ -127,13 +134,15 @@ func init() { // register a map with atlas atlas.AddMap(testMap) + + os.Exit(m.Run()) } func TestURLRoot(t *testing.T) { type tcase struct { request http.Request - hostName string - expected string + hostName *url.URL + expected *url.URL } fn := func(tc tcase) func(t *testing.T) { @@ -141,25 +150,35 @@ func TestURLRoot(t *testing.T) { server.HostName = tc.hostName - output := server.URLRoot(&tc.request).String() - if output != tc.expected { - t.Errorf("expected (%v) got (%v)", tc.expected, output) + output := server.URLRoot(&tc.request) + if !reflect.DeepEqual(output, tc.expected) { + t.Errorf("expected (%+v) got (%+v)", tc.expected, output) } } } tests := map[string]tcase{ "http": { - request: http.Request{}, - hostName: serverHostName, - expected: fmt.Sprintf("http://%v", serverHostName), + request: http.Request{}, + hostName: &url.URL{ + Host: serverHostName, + }, + expected: &url.URL{ + Scheme: "http", + Host: serverHostName, + }, }, "https": { request: http.Request{ TLS: &tls.ConnectionState{}, }, - hostName: serverHostName, - expected: fmt.Sprintf("https://%v", serverHostName), + hostName: &url.URL{ + Host: serverHostName, + }, + expected: &url.URL{ + Scheme: "https", + Host: serverHostName, + }, }, } diff --git a/server/tile_url_template.go b/server/tile_url_template.go new file mode 100644 index 000000000..e17229b90 --- /dev/null +++ b/server/tile_url_template.go @@ -0,0 +1,126 @@ +package server + +import ( + "encoding/json" + "fmt" + "net/url" + "path" + "strings" +) + +const ( + TileURLMapsToken = "maps" + TileURLZToken = "{z}" + TileURLXToken = "{x}" + TileURLYToken = "{y}" + TileURLFileFormat = "pbf" +) + +// TileURLTemplate is responsible for forming up tile URLs which +// contain the uri template variables {z}, {x} and {y}.pbf as the suffix +// of the path. +type TileURLTemplate struct { + Scheme string + Host string + Query url.Values + MapName string + LayerName string +} + +func (t *TileURLTemplate) MarshalJSON() ([]byte, error) { + return []byte(`"` + t.String() + `"`), nil +} + +func (t *TileURLTemplate) UnmarshalJSON(data []byte) error { + var urlStr string + if err := json.Unmarshal(data, &urlStr); err != nil { + return err + } + + urlParsed, err := url.Parse(urlStr) + if err != nil { + return err + } + + t.Scheme = urlParsed.Scheme + t.Host = urlParsed.Host + + query := urlParsed.Query() + if len(query) != 0 { + t.Query = query + } + + // split the path into parts + pathParts := strings.Split(urlParsed.Path, "/") + + var foundZToken bool + for i := range pathParts { + // loop the path parts until we find the "maps" URL token + if pathParts[i] == TileURLMapsToken { + // check pathParts length before inspecting further + // ahead in the slice + if len(pathParts) < i+1 { + return ErrMalformedTileTemplateURL{ + Got: urlStr, + } + } + // value after the maps token is the map name + t.MapName = pathParts[i+1] + + continue + } + + if pathParts[i] == TileURLZToken { + foundZToken = true + // value before the z token is either the + // map name or the layer name. + if pathParts[i-1] != t.MapName { + t.LayerName = pathParts[i-1] + } + break + } + } + if !foundZToken { + return ErrMalformedTileTemplateURL{ + Got: urlStr, + } + } + + return nil +} + +func (t TileURLTemplate) String() string { + query := t.Query.Encode() + if query != "" { + // prepend our query identifier + query = "?" + query + } + + // usually the url.URL struct would be used for building the URL, but what's being + // built here is a "uri template" that contains the placeholders: {z}/{x}/{y}, which + // will be encoded when calling String() on url.URL. URI templates are described in detail + // at https://datatracker.ietf.org/doc/html/rfc6570. + return fmt.Sprintf("%s://%s%s%s", t.Scheme, t.Host, t.Path(), query) +} + +// Path will build the path part of the URI, including the template variables +// {z}, {x} and {y}.pbf. The path will start with a forward slash ("/"). +func (t TileURLTemplate) Path() string { + pathParts := []string{} + if URIPrefix != "" { + pathParts = append(pathParts, URIPrefix) + } + // add the maps part of the path + pathParts = append(pathParts, TileURLMapsToken, t.MapName) + + // if we have a layer add layer parts to the path + if t.LayerName != "" { + pathParts = append(pathParts, t.LayerName) + } + + // add the z/x/y uri template to the path + pathParts = append(pathParts, TileURLZToken, TileURLXToken, TileURLYToken+"."+TileURLFileFormat) + + // build the path + return path.Clean("/" + path.Join(pathParts...)) +} diff --git a/server/tile_url_template_test.go b/server/tile_url_template_test.go new file mode 100644 index 000000000..891c14f78 --- /dev/null +++ b/server/tile_url_template_test.go @@ -0,0 +1,229 @@ +package server_test + +import ( + "encoding/json" + "errors" + "net/url" + "reflect" + "testing" + + "github.com/go-spatial/tegola/server" +) + +func TestTileURLTemplateString(t *testing.T) { + type tcase struct { + uriPrefix string + input server.TileURLTemplate + expected string + } + + fn := func(tc tcase) func(t *testing.T) { + return func(t *testing.T) { + currentURIPrefix := server.URIPrefix + if tc.uriPrefix != "" { + server.URIPrefix = tc.uriPrefix + } + + output := tc.input.String() + if output != tc.expected { + t.Errorf("expected (%s) got (%s)", tc.expected, output) + } + + // reset the URIPrefix to what it was before the test + server.URIPrefix = currentURIPrefix + } + } + + tests := map[string]tcase{ + "map": { + input: server.TileURLTemplate{ + Scheme: "https", + Host: "go-spatial.org", + MapName: "osm", + }, + expected: "https://go-spatial.org/maps/osm/{z}/{x}/{y}.pbf", + }, + "map with query params": { + input: server.TileURLTemplate{ + Scheme: "https", + Host: "go-spatial.org", + MapName: "osm", + Query: url.Values{ + server.QueryKeyDebug: []string{ + "true", + }, + }, + }, + expected: "https://go-spatial.org/maps/osm/{z}/{x}/{y}.pbf?debug=true", + }, + "map with uri prefix": { + uriPrefix: "v1", + input: server.TileURLTemplate{ + Scheme: "https", + Host: "go-spatial.org", + MapName: "osm", + }, + expected: "https://go-spatial.org/v1/maps/osm/{z}/{x}/{y}.pbf", + }, + "map layer": { + input: server.TileURLTemplate{ + Scheme: "https", + Host: "go-spatial.org", + MapName: "osm", + LayerName: "water", + }, + expected: "https://go-spatial.org/maps/osm/water/{z}/{x}/{y}.pbf", + }, + "map layer with query params": { + input: server.TileURLTemplate{ + Scheme: "https", + Host: "go-spatial.org", + MapName: "osm", + LayerName: "water", + Query: url.Values{ + server.QueryKeyDebug: []string{ + "true", + }, + "foo": []string{ + "bar", + }, + }, + }, + expected: "https://go-spatial.org/maps/osm/water/{z}/{x}/{y}.pbf?debug=true&foo=bar", + }, + "map layer with uri prefix": { + uriPrefix: "v1", + input: server.TileURLTemplate{ + Scheme: "https", + Host: "go-spatial.org", + MapName: "osm", + LayerName: "water", + }, + expected: "https://go-spatial.org/v1/maps/osm/water/{z}/{x}/{y}.pbf", + }, + } + + for name, tc := range tests { + t.Run(name, fn(tc)) + } +} + +func TestTileURLTemplateUnmarshalJSON(t *testing.T) { + type tcase struct { + uriPrefix string + input []byte + expected server.TileURLTemplate + expectedErr error + } + + fn := func(tc tcase) func(t *testing.T) { + return func(t *testing.T) { + currentURIPrefix := server.URIPrefix + if tc.uriPrefix != "" { + server.URIPrefix = tc.uriPrefix + } + + var output server.TileURLTemplate + err := json.Unmarshal(tc.input, &output) + if err != nil { + if tc.expectedErr != nil { + if !errors.Is(err, tc.expectedErr) { + t.Fatalf("unepxected err: %s", err) + } + return + } + t.Fatalf("unepxected err: %s", err) + } + if tc.expectedErr != nil { + t.Fatalf("expected err of type: %T, but got %T", tc.expectedErr, err) + } + + if !reflect.DeepEqual(output, tc.expected) { + t.Fatalf("expected (%+v) got (%+v)", tc.expected, output) + } + + // reset the URIPrefix to what it was before the test + server.URIPrefix = currentURIPrefix + } + } + + tests := map[string]tcase{ + "map": { + input: []byte(`"https://go-spatial.org/maps/osm/{z}/{x}/{y}.pbf"`), + expected: server.TileURLTemplate{ + Scheme: "https", + Host: "go-spatial.org", + MapName: "osm", + }, + }, + "map with query params": { + input: []byte(`"https://go-spatial.org/maps/osm/{z}/{x}/{y}.pbf?debug=true"`), + expected: server.TileURLTemplate{ + Scheme: "https", + Host: "go-spatial.org", + MapName: "osm", + Query: url.Values{ + server.QueryKeyDebug: []string{ + "true", + }, + }, + }, + }, + "map with URI prefix": { + uriPrefix: "v1", + input: []byte(`"https://go-spatial.org/v1/maps/osm/{z}/{x}/{y}.pbf"`), + expected: server.TileURLTemplate{ + Scheme: "https", + Host: "go-spatial.org", + MapName: "osm", + }, + }, + "map layer": { + input: []byte(`"https://go-spatial.org/maps/osm/water/{z}/{x}/{y}.pbf"`), + expected: server.TileURLTemplate{ + Scheme: "https", + Host: "go-spatial.org", + MapName: "osm", + LayerName: "water", + }, + }, + "map layer with query params": { + input: []byte(`"https://go-spatial.org/maps/osm/water/{z}/{x}/{y}.pbf?debug=true&foo=bar"`), + expected: server.TileURLTemplate{ + Scheme: "https", + Host: "go-spatial.org", + MapName: "osm", + LayerName: "water", + Query: url.Values{ + server.QueryKeyDebug: []string{ + "true", + }, + "foo": []string{ + "bar", + }, + }, + }, + }, + "map layer with uri prefix": { + uriPrefix: "v1", + input: []byte(`"https://go-spatial.org/v1/maps/osm/water/{z}/{x}/{y}.pbf"`), + expected: server.TileURLTemplate{ + Scheme: "https", + Host: "go-spatial.org", + MapName: "osm", + LayerName: "water", + }, + }, + "malformed url 1": { + uriPrefix: "v1", + input: []byte(`"https://go-spatial.org/v1/maps/"`), + expectedErr: server.ErrMalformedTileTemplateURL{ + Got: "https://go-spatial.org/v1/maps/", + }, + }, + } + + for name, tc := range tests { + t.Run(name, fn(tc)) + } +} diff --git a/server/viewer_disabled.go b/server/viewer_disabled.go index 4889d57a4..b86e62fdf 100644 --- a/server/viewer_disabled.go +++ b/server/viewer_disabled.go @@ -5,6 +5,7 @@ package server import ( "github.com/dimfeld/httptreemux" + "github.com/go-spatial/tegola/observability" ) diff --git a/server/viewer_embed.go b/server/viewer_embed.go index 23737ee0a..4a5b870bc 100644 --- a/server/viewer_embed.go +++ b/server/viewer_embed.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/dimfeld/httptreemux" + "github.com/go-spatial/tegola/observability" "github.com/go-spatial/tegola/ui" )