Skip to content

Commit d97b3e9

Browse files
Jonas Saabelclaude
andcommitted
feat: add place property type support with convenience accessors
Implement structured support for Notion's place property type, providing type-safe access to location data including coordinates, names, addresses, and place IDs. Changes: - Add PlaceValue data class with lat/lon, name, address, and place IDs - Add PageProperty.Place with formattedLocation convenience property - Update PagePropertySerializer to handle place type serialization - Add comprehensive unit tests (6 tests covering various scenarios) - Add convenience accessors: getPlaceProperty(), getPlaceAsString() - Add missing convenience accessors for unique_id property - Update getPlainTextForProperty() to support both place and unique_id Tests: All 514 unit tests passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 02807bc commit d97b3e9

File tree

6 files changed

+575
-2
lines changed

6 files changed

+575
-2
lines changed
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Session: Place Property Implementation
2+
3+
**Date:** 2025-11-04
4+
**Focus:** Implement proper support for place property type
5+
6+
## Context
7+
8+
Following the unique_id property implementation completed earlier today, the `place` property type was identified as the next candidate for proper implementation. The place property provides structured location data with coordinates, names, addresses, and place IDs from AWS/Google services.
9+
10+
## Implementation
11+
12+
### Changes Made
13+
14+
1. **PageRequests.kt**: Added `PlaceValue` data class (lines 397-411)
15+
- `lat: Double?` - Latitude coordinate
16+
- `lon: Double?` - Longitude coordinate
17+
- `name: String?` - Location name (e.g., "Oslo Airport")
18+
- `address: String?` - Full address string
19+
- `awsPlaceId: String?` - AWS place identifier
20+
- `googlePlaceId: String?` - Google place identifier
21+
22+
2. **PageProperty.kt**: Added `PageProperty.Place` (lines 133-157)
23+
- Structured access to place data
24+
- Convenience property `formattedLocation` that returns:
25+
- "Name (lat, lon)" when both name and coordinates are available
26+
- "(lat, lon)" when only coordinates are available
27+
- "Name" when only name is available
28+
- `null` when all fields are empty
29+
30+
3. **PagePropertySerializer.kt**: Updated serializer (lines 28, 64, 108)
31+
- Added "place" to deserialization when block
32+
- Added serialization support
33+
- Updated documentation to include place in supported types
34+
35+
4. **PagePropertyPlaceTest.kt**: Created comprehensive unit tests (6 tests)
36+
- Full location data with AWS place ID
37+
- Coordinates only (no name)
38+
- Name only (no coordinates)
39+
- Place alongside other property types
40+
- Null place value
41+
- Empty place value (all fields null)
42+
43+
5. **PageExtensions.kt**: Added convenience accessor methods (lines 55-73, 163-164)
44+
- `getUniqueIdProperty()` - Returns full UniqueIdValue object
45+
- `getUniqueIdAsString()` - Returns formatted string (e.g., "TASK-123")
46+
- `getPlaceProperty()` - Returns full PlaceValue object
47+
- `getPlaceAsString()` - Returns formatted location string
48+
- Updated `getPlainTextForProperty()` to handle both UniqueId and Place types
49+
50+
### Example Payloads
51+
52+
**Full location data:**
53+
```json
54+
{
55+
"id": "%3FJG%7D",
56+
"type": "place",
57+
"place": {
58+
"lat": 60.19116,
59+
"lon": 11.10242,
60+
"name": "Oslo Airport",
61+
"address": "Oslo Airport, E16, 2060 Gardermoen, Norway",
62+
"aws_place_id": "AQAAAFUAJOZ89r-mb1SYL7-SoMdRt07f78RSAwxxWdEftbKanfZs-NqGy40xt67lWhjfJzRfiogmMr75O8PZ3b4T0PKbYS3OTBLMB8cgTubHqwS7sTFnIVYYShVzNMhVJtBKJPu03EeEWbfslnPMluRM9eImLnrMM_bz",
63+
"google_place_id": null
64+
}
65+
}
66+
```
67+
68+
**Coordinates only:**
69+
```json
70+
{
71+
"id": "place-id",
72+
"type": "place",
73+
"place": {
74+
"lat": 40.7128,
75+
"lon": -74.0060,
76+
"name": null,
77+
"address": null,
78+
"aws_place_id": null,
79+
"google_place_id": null
80+
}
81+
}
82+
```
83+
84+
### Usage Examples
85+
86+
```kotlin
87+
// Direct property access
88+
val page = client.pages.retrieve("page-id")
89+
val locationProp = page.properties["Location"] as? PageProperty.Place
90+
val lat = locationProp?.place?.lat
91+
val name = locationProp?.place?.name
92+
93+
// Convenience accessors
94+
val location = page.getPlaceProperty("Location") // PlaceValue object
95+
val locationStr = page.getPlaceAsString("Location") // "Oslo Airport (60.19116, 11.10242)"
96+
val locationPlain = page.getPlainTextForProperty("Location") // Same formatted string
97+
98+
// Unique ID accessors (added in this session)
99+
val taskId = page.getUniqueIdProperty("TaskID") // UniqueIdValue object
100+
val taskIdStr = page.getUniqueIdAsString("TaskID") // "TASK-123"
101+
```
102+
103+
### Test Results
104+
105+
- Total tests: 514 (up from 508)
106+
- New tests: 6 place property tests
107+
- Status: ✅ All passing, no regressions
108+
- Build: ✅ Successful
109+
110+
## Additional Improvements
111+
112+
During code review, identified that convenience accessor methods were missing for both `unique_id` and `place` properties in PageExtensions.kt. Added:
113+
114+
- `getUniqueIdProperty()` and `getUniqueIdAsString()`
115+
- `getPlaceProperty()` and `getPlaceAsString()`
116+
- Updated `getPlainTextForProperty()` to handle both new property types
117+
118+
This ensures consistency with existing property types and provides the same level of convenience access.
119+
120+
## Outcome
121+
122+
**Complete**: Place property type now fully supported with type-safe access and convenience methods
123+
124+
## Pattern Consistency
125+
126+
Both `unique_id` and `place` implementations follow the established pattern:
127+
1. Value data class in PageRequests.kt
128+
2. PageProperty subclass with convenience properties
129+
3. Serializer updates for deserialization and serialization
130+
4. Comprehensive unit tests
131+
5. Convenience accessor methods in PageExtensions.kt
132+
6. Updated catch-all `getPlainTextForProperty()` method
133+
134+
## Next Steps
135+
136+
Remaining unsupported property types (as Unknown):
137+
- **Button**: No data to retrieve, just triggers actions
138+
- **Verification**: Limited utility, can remain as Unknown
139+
140+
Both `unique_id` and `place` were high-value additions due to their utility in tracking systems and location-based applications respectively.

src/main/kotlin/it/saabel/kotlinnotionclient/models/pages/PageExtensions.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,26 @@ fun Page.getEmailProperty(name: String): String? = getProperty<PageProperty.Emai
5252
*/
5353
fun Page.getPhoneNumberProperty(name: String): String? = getProperty<PageProperty.PhoneNumber>(name)?.phoneNumber
5454

55+
/**
56+
* Get a unique_id property value (returns the full UniqueIdValue object).
57+
*/
58+
fun Page.getUniqueIdProperty(name: String): UniqueIdValue? = getProperty<PageProperty.UniqueId>(name)?.uniqueId
59+
60+
/**
61+
* Get a unique_id property as formatted string (e.g., "TASK-123" or "42").
62+
*/
63+
fun Page.getUniqueIdAsString(name: String): String? = getProperty<PageProperty.UniqueId>(name)?.formattedId
64+
65+
/**
66+
* Get a place property value (returns the full PlaceValue object).
67+
*/
68+
fun Page.getPlaceProperty(name: String): PlaceValue? = getProperty<PageProperty.Place>(name)?.place
69+
70+
/**
71+
* Get a place property as formatted location string (e.g., "Oslo Airport (60.19, 11.10)").
72+
*/
73+
fun Page.getPlaceAsString(name: String): String? = getProperty<PageProperty.Place>(name)?.formattedLocation
74+
5575
/**
5676
* Get a select property option (returns the full SelectOption object).
5777
*/
@@ -140,6 +160,8 @@ fun Page.getPlainTextForProperty(name: String): String? {
140160
is PageProperty.Url -> property.url
141161
is PageProperty.Email -> property.email
142162
is PageProperty.PhoneNumber -> property.phoneNumber
163+
is PageProperty.UniqueId -> property.formattedId
164+
is PageProperty.Place -> property.formattedLocation
143165
is PageProperty.Select -> property.select?.name
144166
is PageProperty.MultiSelect -> property.multiSelect.joinToString(", ") { it.name }
145167
is PageProperty.Status -> property.status?.name

src/main/kotlin/it/saabel/kotlinnotionclient/models/pages/PageProperty.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,32 @@ sealed class PageProperty {
130130
}
131131
}
132132

133+
@Serializable
134+
@SerialName("place")
135+
data class Place(
136+
@SerialName("id") override val id: String,
137+
@SerialName("type") override val type: String,
138+
@SerialName("place") val place: PlaceValue?,
139+
) : PageProperty() {
140+
/**
141+
* Returns the formatted location string with coordinates (e.g., "Oslo Airport (60.19116, 11.10242)").
142+
* Returns null if the place value is not set.
143+
*/
144+
val formattedLocation: String?
145+
get() =
146+
place?.let {
147+
buildString {
148+
if (it.name != null) {
149+
append(it.name)
150+
}
151+
if (it.lat != null && it.lon != null) {
152+
if (it.name != null) append(" ")
153+
append("(${it.lat}, ${it.lon})")
154+
}
155+
}.takeIf { it.isNotEmpty() }
156+
}
157+
}
158+
133159
@Serializable
134160
@SerialName("select")
135161
data class Select(

src/main/kotlin/it/saabel/kotlinnotionclient/models/pages/PagePropertySerializer.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ import kotlinx.serialization.json.jsonPrimitive
2222
*
2323
* Unlike other serializers in the codebase, this one gracefully handles unknown property types
2424
* by deserializing them as [PageProperty.Unknown], ensuring forward compatibility as Notion
25-
* adds new property types (e.g., "button", "unique_id", "verification", etc.).
25+
* adds new property types (e.g., "button", "verification", etc.).
2626
*
2727
* ## Supported Property Types
28-
* - title, rich_text, number, checkbox, url, email, phone_number, unique_id
28+
* - title, rich_text, number, checkbox, url, email, phone_number, unique_id, place
2929
* - select, multi_select, status
3030
* - date, people, files
3131
* - relation, formula, rollup
@@ -61,6 +61,7 @@ object PagePropertySerializer : KSerializer<PageProperty> {
6161
"email" -> decoder.json.decodeFromJsonElement(PageProperty.Email.serializer(), element)
6262
"phone_number" -> decoder.json.decodeFromJsonElement(PageProperty.PhoneNumber.serializer(), element)
6363
"unique_id" -> decoder.json.decodeFromJsonElement(PageProperty.UniqueId.serializer(), element)
64+
"place" -> decoder.json.decodeFromJsonElement(PageProperty.Place.serializer(), element)
6465
"select" -> decoder.json.decodeFromJsonElement(PageProperty.Select.serializer(), element)
6566
"multi_select" -> decoder.json.decodeFromJsonElement(PageProperty.MultiSelect.serializer(), element)
6667
"status" -> decoder.json.decodeFromJsonElement(PageProperty.Status.serializer(), element)
@@ -104,6 +105,7 @@ object PagePropertySerializer : KSerializer<PageProperty> {
104105
is PageProperty.Email -> encoder.encodeSerializableValue(PageProperty.Email.serializer(), value)
105106
is PageProperty.PhoneNumber -> encoder.encodeSerializableValue(PageProperty.PhoneNumber.serializer(), value)
106107
is PageProperty.UniqueId -> encoder.encodeSerializableValue(PageProperty.UniqueId.serializer(), value)
108+
is PageProperty.Place -> encoder.encodeSerializableValue(PageProperty.Place.serializer(), value)
107109
is PageProperty.Select -> encoder.encodeSerializableValue(PageProperty.Select.serializer(), value)
108110
is PageProperty.MultiSelect -> encoder.encodeSerializableValue(PageProperty.MultiSelect.serializer(), value)
109111
is PageProperty.Status -> encoder.encodeSerializableValue(PageProperty.Status.serializer(), value)

src/main/kotlin/it/saabel/kotlinnotionclient/models/pages/PageRequests.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,22 @@ data class UniqueIdValue(
394394
val number: Int,
395395
)
396396

397+
@Serializable
398+
data class PlaceValue(
399+
@SerialName("lat")
400+
val lat: Double?,
401+
@SerialName("lon")
402+
val lon: Double?,
403+
@SerialName("name")
404+
val name: String?,
405+
@SerialName("address")
406+
val address: String?,
407+
@SerialName("aws_place_id")
408+
val awsPlaceId: String?,
409+
@SerialName("google_place_id")
410+
val googlePlaceId: String?,
411+
)
412+
397413
@Serializable
398414
sealed class FileObject {
399415
@Serializable

0 commit comments

Comments
 (0)