Skip to content

Commit 092ace9

Browse files
committed
less code
1 parent db52516 commit 092ace9

5 files changed

Lines changed: 92 additions & 94 deletions

File tree

docs/data-source.rst

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -272,9 +272,10 @@ a map with no ``marker_styles`` still renders, just with plain pins.
272272
"marker_styles": {
273273
"icon_field": "type_of_place",
274274
"color_field": "transparency",
275+
"icon_provider": "phosphor",
275276
"icons": {
276-
"big bridge": "https://cdn.example.com/bridge.svg",
277-
"container": {"provider": "phosphor", "value": "shipping-container"}
277+
"big bridge": "bridge",
278+
"container": "shipping-container"
278279
},
279280
"colors": {
280281
"lacking": "#c62828",
@@ -292,17 +293,24 @@ a map with no ``marker_styles`` still renders, just with plain pins.
292293
only some of your points carry. A name that isn't declared there is ignored, and
293294
pins get no icon/color from it.
294295

296+
``icon_provider``
297+
Where your icons come from. One provider serves the whole ``icons`` table:
298+
299+
``phosphor``
300+
Entries are `Phosphor <https://phosphoricons.com/>`_ icon names in kebab-case
301+
(e.g. ``"shipping-container"``), so you need not host SVGs yourself.
302+
``url``
303+
Entries are URLs of SVGs you host.
304+
305+
Required whenever ``icons`` has anything in it. Naming a provider GoodMap does not
306+
know stops the app from starting, so the mistake surfaces on deploy rather than as a
307+
silently unstyled pin.
308+
295309
``icons``
296-
Maps a value of ``icon_field`` to either a plain URL string, or
297-
``{"provider": "phosphor", "value": "<icon-name>"}`` to use a `Phosphor
298-
<https://phosphoricons.com/>`_ icon by name instead of hosting your own SVG.
299-
``{"provider": "url", "value": "..."}`` is the plain string spelled out explicitly.
300-
301-
``phosphor`` and ``url`` are the providers GoodMap knows; each one's URL is built
302-
server-side, so the browser only ever receives finished URLs and a new provider needs
303-
no frontend release. An entry GoodMap cannot make sense of — an unknown ``provider``,
304-
a missing ``value`` — stops the app from starting, so the mistake surfaces on deploy
305-
rather than as a silently unstyled pin.
310+
Maps a value of ``icon_field`` to whatever ``icon_provider`` takes — an icon name for
311+
``phosphor``, a URL for ``url``. GoodMap turns these into finished URLs when the app
312+
starts, so the browser never sees the provider and adding a new one needs no frontend
313+
release.
306314

307315
``colors``
308316
Maps a value of ``color_field`` to a CSS color.

e2e-tests/e2e_test_data_initial.json

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -276,15 +276,10 @@
276276
"marker_styles": {
277277
"icon_field": "type_of_place",
278278
"color_field": "speed_limit",
279+
"icon_provider": "phosphor",
279280
"icons": {
280-
"big bridge": {
281-
"provider": "phosphor",
282-
"value": "bridge"
283-
},
284-
"small bridge": {
285-
"provider": "phosphor",
286-
"value": "footprints"
287-
}
281+
"big bridge": "bridge",
282+
"small bridge": "footprints"
288283
},
289284
"colors": {
290285
"10": "#2e7d32",

goodmap/marker_styles.py

Lines changed: 18 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -56,47 +56,33 @@ def resolve(self, value: str) -> str:
5656
}
5757

5858

59-
def _icon_url(entry: Any) -> str:
60-
"""The URL one marker_styles.icons entry stands for.
61-
62-
Args:
63-
entry: A plain URL string - shorthand for the "url" provider - or a tagged
64-
{"provider": <name in ICON_PROVIDERS>, "value": str} dict.
65-
66-
Returns:
67-
The resolved URL.
68-
69-
Raises:
70-
KeyError, TypeError: The entry is malformed. Deliberately not caught: bad
71-
marker_styles config stops the app from starting, the same way a category
72-
with no allowed values does (see data_models.location.create_location_model).
73-
"""
74-
if isinstance(entry, str):
75-
return ICON_PROVIDERS["url"].resolve(entry)
76-
return ICON_PROVIDERS[entry["provider"]].resolve(entry["value"])
77-
78-
7959
def resolve_marker_styles(marker_styles: dict[str, Any]) -> dict[str, Any]:
8060
"""Resolve marker_styles.icons into a flat {value: url} lookup table.
8161
62+
One "icon_provider" serves the whole table, so every entry is a plain value that
63+
provider understands - a Phosphor icon name, a URL - rather than each one restating
64+
which provider it came from.
65+
8266
Args:
8367
marker_styles: Raw marker_styles config as returned by
84-
goodmap.db.get_marker_styles(). May be empty or lack an "icons" key. Never
85-
mutated - for the json backend this is the db's live in-memory config, so
86-
resolving in place would rewrite what the deployment has stored.
68+
goodmap.db.get_marker_styles(). Carries "icon_provider" (a name in
69+
ICON_PROVIDERS) whenever it carries "icons". May be empty or lack both.
70+
Never mutated - for the json backend this is the db's live in-memory config,
71+
so resolving in place would rewrite what is stored.
8772
8873
Returns:
8974
A new dict. "icons", if present, is replaced by a flat {value: url} map; every
9075
other key (icon_field, color_field, colors) is carried through untouched.
91-
"colors" needs no resolving - it maps straight to CSS colors and never had a
92-
tagged form.
76+
"colors" needs no resolving - it maps straight to CSS colors, with no provider.
9377
9478
Raises:
95-
AttributeError, KeyError, TypeError: marker_styles.icons is malformed; see
96-
_icon_url. Uncaught by design, so the app refuses to start.
79+
KeyError, TypeError: "icon_provider" is missing or names a provider that does
80+
not exist. Uncaught by design: bad config stops the app from starting, the
81+
same way a category with no allowed values does (see
82+
data_models.location.create_location_model).
9783
"""
98-
icons = marker_styles.get("icons")
99-
if icons is None:
100-
return dict(marker_styles)
101-
102-
return {**marker_styles, "icons": {key: _icon_url(entry) for key, entry in icons.items()}}
84+
resolved = dict(marker_styles)
85+
if icons := marker_styles.get("icons"):
86+
provider = ICON_PROVIDERS[marker_styles["icon_provider"]]
87+
resolved["icons"] = {key: provider.resolve(value) for key, value in icons.items()}
88+
return resolved

tests/unit_tests/test_goodmap.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import importlib.metadata
22
import io
3+
import json
34
import os
5+
import re
46
import sys
57
import tempfile
68
import types
@@ -132,9 +134,8 @@ def test_map_route_marker_styles():
132134
"categories": {"type_of_place": ["parcel_locker", "container"]},
133135
"marker_styles": {
134136
"icon_field": "type_of_place",
135-
"icons": {
136-
"parcel_locker": "https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2/assets/fill/package-fill.svg"
137-
},
137+
"icon_provider": "phosphor",
138+
"icons": {"parcel_locker": "package"},
138139
"colors": {},
139140
},
140141
},
@@ -170,6 +171,7 @@ def test_map_route_marker_styles_stay_in_step_with_the_api():
170171
"location_obligatory_fields": [["type_of_place", "str"]],
171172
"marker_styles": {
172173
"icon_field": "type_of_place",
174+
"icon_provider": "url",
173175
"icons": {"parcel_locker": "https://cdn.example.com/package.svg"},
174176
"colors": {},
175177
},
@@ -193,15 +195,16 @@ def test_map_route_marker_styles_stay_in_step_with_the_api():
193195

194196

195197
def test_map_route_serves_icons_already_resolved_to_urls():
196-
"""window.MARKER_STYLES.icons is a flat {value: url} table: the tagged
197-
{provider, value} form a data source may use is resolved at startup, so supporting a
198-
new provider never needs a frontend release."""
198+
"""window.MARKER_STYLES.icons is a flat {value: url} table: the provider-specific
199+
values a data source configures are resolved at startup, so supporting a new provider
200+
never needs a frontend release."""
199201
data = {
200202
"site_content": {"pages": []},
201203
"location_obligatory_fields": [["type_of_place", "str"]],
202204
"marker_styles": {
203205
"icon_field": "type_of_place",
204-
"icons": {"big bridge": {"provider": "phosphor", "value": "bridge"}},
206+
"icon_provider": "phosphor",
207+
"icons": {"big bridge": "bridge"},
205208
"colors": {},
206209
},
207210
}
@@ -218,11 +221,12 @@ def test_map_route_serves_icons_already_resolved_to_urls():
218221

219222
response_text = app.test_client().get("/map").data.decode("utf-8")
220223

221-
assert (
222-
"https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2/assets/fill/bridge-fill.svg"
223-
in response_text
224-
)
225-
assert "provider" not in response_text
224+
match = re.search(r"window\.MARKER_STYLES\s*=\s*(.*?);", response_text)
225+
assert match is not None
226+
served = json.loads(match.group(1))
227+
assert served["icons"] == {
228+
"big bridge": "https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2/assets/fill/bridge-fill.svg"
229+
}
226230

227231

228232
def test_map_route_includes_photo_constraints():

tests/unit_tests/test_marker_styles.py

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -26,66 +26,74 @@ class SpriteProvider:
2626
def resolve(self, value):
2727
return f"https://sprites.example/{value}.svg"
2828

29-
styles = {"icons": {"big bridge": {"provider": "sprite", "value": "bridge"}}}
29+
styles = {"icon_provider": "sprite", "icons": {"big bridge": "bridge"}}
3030

3131
with mock.patch.dict(ICON_PROVIDERS, {"sprite": SpriteProvider()}):
3232
resolved = resolve_marker_styles(styles)
3333

3434
assert resolved["icons"] == {"big bridge": "https://sprites.example/bridge.svg"}
3535

3636

37-
def test_resolves_phosphor_entry_to_cdn_url():
38-
styles = {"icons": {"big bridge": {"provider": "phosphor", "value": "bridge"}}}
39-
40-
assert resolve_marker_styles(styles)["icons"] == {"big bridge": PHOSPHOR_BRIDGE_URL}
41-
42-
43-
def test_resolves_url_provider_entry_to_its_value():
44-
styles = {"icons": {"container": {"provider": "url", "value": "https://e.example/c.svg"}}}
37+
def test_phosphor_provider_resolves_every_entry_in_the_table():
38+
styles = {
39+
"icon_provider": "phosphor",
40+
"icons": {"big bridge": "bridge", "small bridge": "footprints"},
41+
}
4542

46-
assert resolve_marker_styles(styles)["icons"] == {"container": "https://e.example/c.svg"}
43+
assert resolve_marker_styles(styles)["icons"] == {
44+
"big bridge": PHOSPHOR_BRIDGE_URL,
45+
"small bridge": (
46+
"https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2/assets/fill/footprints-fill.svg"
47+
),
48+
}
4749

4850

49-
def test_passes_plain_string_entry_through_unchanged():
50-
styles = {"icons": {"container": "https://e.example/c.svg"}}
51+
def test_url_provider_serves_entries_the_deployment_hosts_itself():
52+
styles = {"icon_provider": "url", "icons": {"container": "https://e.example/c.svg"}}
5153

5254
assert resolve_marker_styles(styles)["icons"] == {"container": "https://e.example/c.svg"}
5355

5456

5557
@pytest.mark.parametrize(
56-
"entry",
58+
"styles",
5759
[
58-
{"provider": "phosphorr", "value": "bridge"},
59-
{"value": "bridge"},
60-
{"provider": "phosphor"},
61-
7,
62-
None,
60+
{"icon_provider": "phosphorr", "icons": {"big bridge": "bridge"}},
61+
{"icons": {"big bridge": "bridge"}},
62+
{"icon_provider": None, "icons": {"big bridge": "bridge"}},
6363
],
64-
ids=["unknown-provider", "no-provider", "no-value", "number", "null"],
64+
ids=["unknown-provider", "no-provider", "null-provider"],
6565
)
66-
def test_malformed_entry_stops_the_app_from_starting(entry):
66+
def test_malformed_config_stops_the_app_from_starting(styles):
6767
"""Bad marker_styles config is a deploy-time mistake, so it raises rather than
6868
quietly costing a pin its icon - the same stance create_location_model takes on a
6969
category with no allowed values."""
70-
with pytest.raises((KeyError, TypeError, AttributeError)):
71-
resolve_marker_styles({"icons": {"big bridge": entry}})
70+
with pytest.raises((KeyError, TypeError)):
71+
resolve_marker_styles(styles)
7272

7373

7474
def test_empty_marker_styles_stays_empty():
7575
assert resolve_marker_styles({}) == {}
7676

7777

78-
def test_missing_icons_key_is_not_invented():
79-
assert resolve_marker_styles({"icon_field": "type_of_place"}) == {"icon_field": "type_of_place"}
78+
@pytest.mark.parametrize("icons", [{}, None], ids=["empty-table", "no-icons-key"])
79+
def test_nothing_to_resolve_needs_no_provider(icons):
80+
"""A deployment that styles pins by color alone never names an icon provider, so an
81+
absent or empty table must not demand one."""
82+
styles = {"icon_field": "type_of_place", "colors": {"10": "#2e7d32"}}
83+
if icons is not None:
84+
styles["icons"] = icons
85+
86+
assert resolve_marker_styles(styles) == styles
8087

8188

8289
def test_every_other_key_is_carried_through_untouched():
83-
"""colors maps straight to CSS colors and never had a tagged form, so it - like the
84-
two field names - must survive resolution unchanged."""
90+
"""colors maps straight to CSS colors and has no provider, so it - like the two field
91+
names - must survive resolution unchanged."""
8592
styles = {
8693
"icon_field": "type_of_place",
8794
"color_field": "speed_limit",
8895
"colors": {"10": "#2e7d32", "50": "#c62828"},
96+
"icon_provider": "url",
8997
"icons": {"plain": "https://e.example/c.svg"},
9098
}
9199

@@ -99,10 +107,7 @@ def test_every_other_key_is_carried_through_untouched():
99107
def test_does_not_mutate_the_config_it_was_given():
100108
"""For the json backend this dict is the db's live in-memory config, so resolving in
101109
place would rewrite what the deployment has stored."""
102-
styles = {
103-
"icon_field": "type_of_place",
104-
"icons": {"big bridge": {"provider": "phosphor", "value": "bridge"}},
105-
}
110+
styles = {"icon_provider": "phosphor", "icons": {"big bridge": "bridge"}}
106111
before = copy.deepcopy(styles)
107112
icons_before = styles["icons"]
108113

0 commit comments

Comments
 (0)