Skip to content

Commit 30f1fbc

Browse files
glassbead0vnugent
authored andcommitted
feat: allow null for tick fields & add markdown for tick logic
1 parent 902560f commit 30f1fbc

5 files changed

Lines changed: 79 additions & 77 deletions

File tree

documentation/tick_logic.md

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,29 @@ Let's get to the technical details. There are 3 layers to this architecturally i
1616
* `Tick.style`
1717
* `Tick.attemptType`
1818

19-
(Here)[https://github.com/OpenBeta/openbeta-graphql/blob/develop/src/graphql/schema/Climb.gql#L115] are all the possible values for `Climb.type`, as defined in the [limb Schema](https://github.com/OpenBeta/openbeta-graphql/blob/develop/src/graphql/schema/Climb.gql#L115), and style and attempts types defined in the [TickSchema](https://github.com/OpenBeta/openbeta-graphql/blob/develop/src/db/TickTypes.ts).
19+
Here are all the possible values for `Climb.type` (also called discipline), as defined in the [Climb Schema](https://github.com/OpenBeta/openbeta-graphql/blob/develop/src/graphql/schema/Climb.gql#L115), and Tick style and attemptsTypes defined in the [TickSchema](https://github.com/OpenBeta/openbeta-graphql/blob/develop/src/db/TickTypes.ts).
2020

2121
| Climb.type | Tick.style | Tick.attemptType |
2222
|---------------|------------|------------------|
23-
| trad | Lead | Onsight |
24-
| sport | Follow | Flash |
25-
| bouldering | TR | Redpoint |
26-
| deepwatersolo | Solo | Pinkpoint |
27-
| snow | Aid | Send |
28-
| ice | Boulder | Attempt |
29-
| aid | Frenchfree | |
30-
| tr | |
31-
| alpine | |
32-
| mixed | |
23+
| trad | Lead | Onsight |
24+
| sport | Follow | Flash |
25+
| bouldering | TR | Redpoint |
26+
| deepwatersolo | Solo | Pinkpoint |
27+
| snow | Aid | Send |
28+
| ice | Boulder | Attempt |
29+
| aid | | Frenchfree |
30+
| tr | | |
31+
| alpine | | |
32+
| mixed | | |
3333

3434

3535
See the [Wikipedia Glossary of Climbing Terms](https://en.wikipedia.org/wiki/Glossary_of_climbing_terms) for common definitions of all these terms.
3636

37-
Given the 10 climb types, 6 styles, and 7 attempt types, there are 10*6*7=**420** diffent ways to "tick" a route.
37+
Given the 10 climb types, 6 styles, and 7 attempt types, there are `10*6*7=`**420** diffent ways to "tick" a route. *(Thats not even accounting for the fact that a route can be multiple disciplines, eg: boulder & TR, or sport & deepwatersolo. If you really want to get nerdy: with the `2^10=1024` possible discipline combinations, there are a whopping `1024*6*7=`**43,008** ways to tick a route!)*
3838

39-
Here's a Hierarchical way to restrict values:
39+
## Here's a Hierarchical way to restrict values:
4040

41-
## Climb type -> Tick Style
41+
### Climb type -> Tick Style
4242

4343
| Climb Type | logical description | Tick Style Options |
4444
|-------------------|---------------------|-------------------- |
@@ -48,15 +48,17 @@ Here's a Hierarchical way to restrict values:
4848
| 'deepwatersolo' or leadable or aidable or topropeable | soloable | Solo |
4949
| bouldering | boulderable | Boulder |
5050

51-
## Tick Style -> Tick Attempt Type
51+
52+
Since a route can have multiple disciplines, these options are composable. eg: a route marked as 'trad, aid', is both 'leadable' and 'aidable'. A route that is 'boulder, tr', is both 'boulderable' and 'topropeable'
53+
54+
### Tick Style -> Tick Attempt Type
5255

5356
| Tick Style | Attempt Type options |
5457
|------------|----------------------|
5558
| 'Lead' | 'Onsight', 'Flash', 'Redpoint', 'Pinkpoint', 'Attempt', 'Frenchfree' |
56-
| 'Follow' or 'TR' | 'Send', 'Attempt', 'Frenchfree' |
59+
| 'Follow', 'TR' or 'Aid | 'Send', 'Attempt' |
5760
| 'Solo' | 'Onsight', 'Flash', 'Redpoint', 'Attempt' |
5861
| 'Boulder' | 'Flash', 'Send', 'Attempt' |
59-
| 'Aid' | 'Send', 'Attempt' |
6062

6163
## A few justifications
6264

@@ -65,7 +67,7 @@ Here's a Hierarchical way to restrict values:
6567
* OB does not use the term "Fell/Hung" for roped climbs, and instead normalizes it to "Attempt", just like boulders. Importing routes from MP will convert "Fell/Hung" to "Attempt"
6668
* While 'Frenchfree' and 'Aid' could be considered synomonous, some climbers may want to distinguish, for example, a multipitch route where one pitch was intentionally 'French freed' (Time Wave Zero being a common example), which is distinctly different in character than, eg: aiding the Nose on El Cap.
6769
* Eventually, it might be cool to allow ticks for individual pitches, but that is not supported right now.
68-
* Given the 420 possible combination, no simple logical system will perfectly capture every edge case.
70+
* Given the 43,008 possible combinations, no simple logical system will perfectly capture every edge case.
6971

7072

7173
## Importing from Mountain Project

src/db/TickSchema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ export const TickSchema = new Schema<TickType>({
1616
notes: { type: Schema.Types.String, required: false },
1717
climbId: { type: Schema.Types.String, required: true, index: true },
1818
userId: { type: Schema.Types.String, required: true, index: true },
19-
style: { type: Schema.Types.String, enum: ['Lead', 'Solo', 'TR', 'Follow', 'Aid', 'Boulder', null], required: false },
20-
attemptType: { type: Schema.Types.String, enum: ['Onsight', 'Flash', 'Pinkpoint', 'Frenchfree', 'Redpoint', 'Send', 'Attempt', null], required: false, index: true },
19+
style: { type: Schema.Types.String, enum: ['Lead', 'Solo', 'TR', 'Follow', 'Aid', 'Boulder'], required: false },
20+
attemptType: { type: Schema.Types.String, enum: ['Onsight', 'Flash', 'Pinkpoint', 'Frenchfree', 'Redpoint', 'Send', 'Attempt'], required: false, index: true },
2121
dateClimbed: { type: Schema.Types.Date },
2222
grade: { type: Schema.Types.String, required: false, index: true },
2323
// Bear in mind that these enum types must be kept in sync with the TickSource enum

src/graphql/schema/Tick.gql

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,6 @@ enum TickAttemptType {
136136
Send
137137
"Redpoint"
138138
Redpoint
139-
140139
}
141140

142141
enum TickStyle {

src/model/TickDataSource.ts

Lines changed: 52 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -86,51 +86,59 @@ export default class TickDataSource extends MongoDataSource<TickType> {
8686
if (climb == null) {
8787
throw new Error('Climb not found')
8888
}
89-
// Getting the climb singular type to verifiy, some climbs have multiple types such as the heart route on elcap (13b/v10)
90-
const isDWSOnly =
91-
(climb.type.deepwatersolo === true) &&
92-
Object.keys(climb.type).every(
93-
key => key === 'deepwatersolo' || climb.type[key] === false)
94-
95-
const isBoulderingOnly =
96-
(climb.type.bouldering === true) &&
97-
Object.keys(climb.type).every(
98-
key => key === 'bouldering' || climb.type[key] === false)
99-
100-
const isTROnly =
101-
(climb.type.tr === true) &&
102-
Object.keys(climb.type).every(
103-
key => key === 'tr' || climb.type[key] === false)
104-
105-
const isAidOnly =
106-
(climb.type.aid === true) &&
107-
Object.keys(climb.type).every(
108-
key => key === 'aid' || climb.type[key] === false)
109-
110-
const isTradSportAlpineIceMixedAid =
111-
['trad', 'sport', 'alpine', 'ice', 'mixed', 'aid'].some(
112-
type => climb.type[type] === true)
113-
114-
const tickStyle = tick.style ?? 'null' // Provide a default value if tick.style is undefined
89+
90+
// Tick validation logic is complicated. see [tick_logic.md](https://github.com/OpenBeta/openbeta-graphql/blob/develop/documentation/tick_logic.md).
91+
92+
const tickStyle = tick.style ?? 'null' // Provide a default value if tick.style is undefined. This 'null' string is not saved in the db, but used for easy validation.
11593
const attemptType = tick.attemptType ?? 'null' // Provide a default value if tick.attempy is undefined
116-
if (isDWSOnly || isBoulderingOnly) { // bouldering and dws can only have attempt types: 'Send', 'Flash', 'Attempt', 'Onsight' and should have no sytle
117-
if ((['Lead', 'Solo', 'Tr', 'Follow', 'Aid'].includes(tickStyle)) || ['Pinkpoint', 'Frenchfree', 'Redpoint'].includes(attemptType)) {
118-
throw new Error('Invalid attempt type or style for DWS/Bouldering')
119-
}
120-
} else if (isTROnly) { // TopRope can only have attempt types: 'Send', 'Flash', 'Attempt', 'Onsight' and styles: 'TR'
121-
if (!['TR', 'null'].includes(tickStyle) || ['Pinkpoint', 'Frenchfree', 'Redpoint'].includes(attemptType)) {
122-
throw new Error('Invalid attempt type or style for TR only')
123-
}
124-
} else if (isAidOnly) { // Aid can only have attempt types: 'Send', 'Attempt' and styles: 'Aid', 'Follow'
125-
if (!['Aid', 'Follow', 'null'].includes(tickStyle) || ['Onsight', 'Flash', 'Pinkpoint', 'Frenchfree'].includes(attemptType)) {
126-
throw new Error('Invalid attempt type or style for Aid only')
127-
}
128-
} else if (isTradSportAlpineIceMixedAid) { // roped climbs that aren't lead must have attempt types: 'Send', 'Flash', 'Attempt', 'Onsight'
129-
if (['Solo', 'TR', 'Follow'].includes(tickStyle) && ['Pinkpoint', 'Frenchfree', 'Redpoint'].includes(attemptType)) {
130-
throw new Error('Invalid attempt type for Solo/TR/Follow style')
131-
}
132-
} else {
133-
throw new Error('Invalid climb type')
94+
95+
const leadable = ['trad', 'sport', 'snow', 'ice', 'mixed', 'alpine'].some(type => climb.type[type] === true)
96+
const topropeable = (climb.type.tr === true) || leadable
97+
const aidable = climb.type.aid === true
98+
const boulderable = climb.type.bouldering === true
99+
const soloable = (climb.type.deepwatersolo === true) || leadable || aidable || (topropeable && !boulderable)
100+
101+
// Validate tick style for each climb type
102+
if (!leadable && (['Lead', 'Follow'].includes(tickStyle))) {
103+
throw new Error(`Invalid style ${tickStyle} for climb type`)
104+
}
105+
if (!topropeable && (tickStyle === 'TR')) {
106+
throw new Error(`Invalid style ${tickStyle} for climb type`)
107+
}
108+
if (!aidable && (tickStyle === 'Aid')) {
109+
throw new Error(`Invalid style ${tickStyle} for climb type`)
110+
}
111+
if (!boulderable && (tickStyle === 'Boulder')) {
112+
throw new Error(`Invalid style ${tickStyle} for climb type`)
113+
}
114+
if (!soloable && (tickStyle === 'Solo')) {
115+
throw new Error(`Invalid style ${tickStyle} for climb type`)
116+
}
117+
118+
// validate attempt type for each tick style
119+
switch (tickStyle) {
120+
case 'Lead':
121+
if (!['Onsight', 'Flash', 'Redpoint', 'Pinkpoint', 'Attempt', 'Frenchfree', 'null'].includes(attemptType)) {
122+
throw new Error(`Invalid attempt type ${attemptType} for Lead style`)
123+
}
124+
break
125+
case 'Solo':
126+
if (!['Onsight', 'Flash', 'Redpoint', 'Attempt', 'null'].includes(attemptType)) {
127+
throw new Error(`Invalid attempt type ${attemptType} for Solo style`)
128+
}
129+
break
130+
case 'Boulder':
131+
if (!['Flash', 'Send', 'Attempt', 'null'].includes(attemptType)) {
132+
throw new Error(`Invalid attempt type ${attemptType} for Boulder style`)
133+
}
134+
break
135+
case 'TR':
136+
case 'Follow':
137+
case 'Aid':
138+
if (!['Send', 'Attempt', 'null'].includes(attemptType)) {
139+
throw new Error(`Invalid attempt type ${attemptType} for TR/Follow/Aid style`)
140+
}
141+
break
134142
}
135143
}
136144

src/model/__tests__/tickValidation.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ const toTestBoulder: TickInput = {
7373
notes: 'wet!',
7474
climbId: 'tbd',
7575
userId: userId.toUUID().toString(),
76+
style: 'Boulder',
7677
attemptType: 'Flash',
7778
dateClimbed: new Date('2012-10-15'),
7879
grade: 'v4',
@@ -168,21 +169,13 @@ describe('Tick Validation', () => {
168169
await expect(ticks.addTick(dwsTick)).resolves.not.toThrow()
169170
})
170171

171-
it('should throw error for invalid attempt type for deep water solo climb', async () => {
172-
const invalidDwsTick: TickInput = {
173-
...toTestDWS,
174-
attemptType: 'Pinkpoint'
175-
}
176-
await expect(ticks.addTick(invalidDwsTick)).rejects.toThrow('Invalid attempt type or style for DWS/Bouldering')
177-
})
178-
179172
it('should throw error for invalid style for deep water solo climb', async () => {
180173
const invalidDwsTick: TickInput = {
181174
...toTestDWS,
182175
style: 'Lead',
183176
attemptType: 'Send'
184177
}
185-
await expect(ticks.addTick(invalidDwsTick)).rejects.toThrow('Invalid attempt type or style for DWS/Bouldering')
178+
await expect(ticks.addTick(invalidDwsTick)).rejects.toThrow('Invalid style Lead for climb type')
186179
})
187180

188181
it('should validate tick for top rope climb', async () => {
@@ -194,7 +187,7 @@ describe('Tick Validation', () => {
194187
...toTestTR,
195188
attemptType: 'Pinkpoint'
196189
}
197-
await expect(ticks.addTick(invalidTrTick)).rejects.toThrow('Invalid attempt type or style for TR only')
190+
await expect(ticks.addTick(invalidTrTick)).rejects.toThrow('Invalid attempt type Pinkpoint for TR/Follow/Aid style')
198191
})
199192

200193
it('should validate tick for aid climb', async () => {
@@ -206,7 +199,7 @@ describe('Tick Validation', () => {
206199
...toTestAid,
207200
attemptType: 'Flash'
208201
}
209-
await expect(ticks.addTick(invalidAidTick)).rejects.toThrow('Invalid attempt type or style for Aid only')
202+
await expect(ticks.addTick(invalidAidTick)).rejects.toThrow('Invalid attempt type Flash for TR/Follow/Aid style')
210203
})
211204

212205
it('should throw error for invalid style for aid climb', async () => {
@@ -215,7 +208,7 @@ describe('Tick Validation', () => {
215208
style: 'Lead',
216209
attemptType: 'Send'
217210
}
218-
await expect(ticks.addTick(invalidAidTick)).rejects.toThrow('Invalid attempt type or style for Aid only')
211+
await expect(ticks.addTick(invalidAidTick)).rejects.toThrow('Invalid style Lead for climb type')
219212
})
220213

221214
it('should validate tick with no attempt type', async () => {

0 commit comments

Comments
 (0)