Skip to content

Commit

Permalink
feat: abstract doesn't directly state it updates or obsoletes each do…
Browse files Browse the repository at this point in the history
…cument so affected (#71)

* feat: added validation for obsoletes and updates

- added validation for update and obsolete metadata
- added validation for obsolete and updated rfc in abstract
- tested app
covered new validation with tests
- modified abstract section validation

* feat: added tests for obsolete and update metadata parsing

* fix: corrected test header

Co-authored-by: Robert Sparks <rjsparks@nostrum.com>

---------

Co-authored-by: Robert Sparks <rjsparks@nostrum.com>
  • Loading branch information
Dmutre and rjsparks authored Feb 21, 2025
1 parent 7e58021 commit f5fb8ec
Show file tree
Hide file tree
Showing 6 changed files with 398 additions and 9 deletions.
62 changes: 60 additions & 2 deletions lib/modules/metadata.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ValidationWarning } from '../helpers/error.mjs'
import { ValidationComment, ValidationWarning } from '../helpers/error.mjs'
import { traverseAllValues } from '../helpers/traversal.mjs'
import { fetchRemoteDocInfo, fetchRemoteRfcInfo } from '../helpers/remote.mjs'
import { MODES } from '../config/modes.mjs'
Expand Down Expand Up @@ -115,7 +115,65 @@ export async function validateObsoleteUpdateRef (doc, { mode = MODES.NORMAL, off

switch (doc.type) {
case 'txt': {
// TODO: Text type validation
const abstract = doc.data.content.abstract.join(' ') || ''
const obsoletesRfc = doc.data.extractedElements.obsoletesRfc
const updatesRfc = doc.data.extractedElements.updatesRfc

const mentionedObsoletesRfcs = [...abstract.matchAll(OBSOLETES_RE)]
.flatMap(match => (match[0].match(RFC_NUM_RE) || []))
const mentionedUpdatesRfcs = [...abstract.matchAll(UPDATES_RE)]
.flatMap(match => (match[0].match(RFC_NUM_RE) || []))

// RFCs in obsoletes/updates but not mentioned in abstract
const obsoletesNotInAbstract = obsoletesRfc.filter(rfc => !mentionedObsoletesRfcs.includes(rfc))
const updatesNotInAbstract = updatesRfc.filter(rfc => !mentionedUpdatesRfcs.includes(rfc))

obsoletesNotInAbstract.forEach(rfc => {
result.push(new ValidationComment(
'OBSOLETES_NOT_IN_ABSTRACT',
`RFC ${rfc} is listed as "obsoleted" in metadata but is not mentioned in the abstract.`,
{
ref: 'https://authors.ietf.org/en/required-content#abstract',
path: 'data.content.abstract'
}
))
})

updatesNotInAbstract.forEach(rfc => {
result.push(new ValidationComment(
'UPDATES_NOT_IN_ABSTRACT',
`RFC ${rfc} is listed as "updated" in metadata but is not mentioned in the abstract.`,
{
ref: 'https://authors.ietf.org/en/required-content#abstract',
path: 'data.content.abstract'
}
))
})

const mentionedButNotObsoletes = mentionedObsoletesRfcs.filter(rfc => !obsoletesRfc.includes(rfc))
const mentionedButNotUpdates = mentionedUpdatesRfcs.filter(rfc => !updatesRfc.includes(rfc))

mentionedButNotObsoletes.forEach(rfc => {
result.push(new ValidationComment(
'MENTIONED_NOT_IN_OBSOLETES',
`RFC ${rfc} is mentioned as "obsoleted" or "replaced" in the abstract but not listed in metadata.`,
{
ref: 'https://authors.ietf.org/en/required-content#abstract',
path: 'data.content.abstract'
}
))
})

mentionedButNotUpdates.forEach(rfc => {
result.push(new ValidationComment(
'MENTIONED_NOT_IN_UPDATES',
`RFC ${rfc} is mentioned as "updated" in the abstract but not listed in metadata.`,
{
ref: 'https://authors.ietf.org/en/required-content#abstract',
path: 'data.content.abstract'
}
))
})
break
}
case 'xml': {
Expand Down
13 changes: 9 additions & 4 deletions lib/modules/sections.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,18 @@ export async function validateAbstractSection (doc, { mode = MODES.NORMAL } = {}
ref: 'https://authors.ietf.org/required-content#abstract'
}))
} else {
const abstractContent = doc.data.content.abstract.join(' ')
const abstractContent = doc.data.content.abstract.join(' ') || ''
const updatesRfc = doc.data.extractedElements?.updatesRfc || []
const obsoletesRfc = doc.data.extractedElements?.obsoletesRfc || []

const rfcReferencePattern = /\[RFC\d+\]/i
const rfcReferencePattern = /\[RFC\d+\]/ig
const urlPattern = /https?:\/\/[^\s]+|www\.[^\s]+/i
const sectionReferencePattern = /\bSection\s\d+(\.\d+)?\b|\bAppendix\s\w+\b/i
const internetDraftReferencePattern = /\[I-D\.[^\]]+\]/i
const customReferencePattern = /\[[A-Za-z0-9-]+\]/i
const customReferencePattern = /\[(?!RFC\d+\b)[A-Za-z0-9-]+\]/i

const rfcMatches = [...abstractContent.matchAll(rfcReferencePattern)].map(match => match[0].match(/\d+/)[0])
const notAllowedRfcReferences = rfcMatches.filter(rfc => !updatesRfc.includes(rfc) && !obsoletesRfc.includes(rfc))

const validateReference = (pattern, errorCode, errorMessage) => {
if (pattern.test(abstractContent)) {
Expand All @@ -53,7 +58,7 @@ export async function validateAbstractSection (doc, { mode = MODES.NORMAL } = {}
}
}

validateReference(rfcReferencePattern, 'INVALID_ABSTRACT_SECTION_REF', 'The abstract section should not contain references to RFCs.')
if (notAllowedRfcReferences.length > 0) validateReference(rfcReferencePattern, 'INVALID_ABSTRACT_SECTION_REF', 'The abstract section should not contain references to RFCs.')
validateReference(urlPattern, 'INVALID_ABSTRACT_SECTION_URL', 'The abstract section should not contain URLs.')
validateReference(sectionReferencePattern, 'INVALID_ABSTRACT_SECTION_REF', 'The abstract section should not contain references to sections or appendices.')
validateReference(internetDraftReferencePattern, 'INVALID_ABSTRACT_SECTION_ID_REF', 'The abstract section should not contain references to Internet-Drafts.')
Expand Down
48 changes: 47 additions & 1 deletion lib/parsers/txt.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ const KEYWORDS_PATTERN = /((NOT)\s)?(MUST|REQUIRED|SHALL|SHOULD|RECOMMENDED|OPTI
// Invalid combinations regex pattern
const INVALID_COMBINATIONS_PATTERN = /(MUST not|SHALL not|SHOULD not|not RECOMMENDED|MAY NOT|NOT REQUIRED|NOT OPTIONAL)/g

// Obsolete and updates regex patterns
const OBSOLETES_RE = /(?:obsoletes|replaces)\s*:\s*((?:rfc\s*)?[0-9]+(?:,|\s|and)*\s*)+/gi
const UPDATES_RE = /updates\s*:\s*((?:rfc\s*)?[0-9]+(?:,|\s|and)*\s*)+/gi

/**
* @typedef {Object} TXTDocObject
* @property {Object} data Parsed TXT tree
Expand Down Expand Up @@ -124,7 +128,9 @@ export async function parse (rawText, filename) {
ipv4: [],
ipv6: [],
keywords2119: [],
boilerplate2119Keywords: []
boilerplate2119Keywords: [],
obsoletesRfc: [],
updatesRfc: []
},
boilerplate: {
rfc2119: BOILERPLATE_PATTERNS.rfc2119.test(normalizedText) || BOILERPLATE_PATTERNS.rfc2119_alt.test(normalizedText),
Expand Down Expand Up @@ -169,6 +175,12 @@ export async function parse (rawText, filename) {
data.boilerplate.similar2119boilerplate =
hasBoilerplateMatch(normalizedText, BOILERPLATE_PARTS.rfc2119, BOILERPLATE_PARTS.rfc2119_alt, BOILERPLATE_PARTS.rfc8174) && !(data.boilerplate.rfc2119 || data.boilerplate.rfc8174)

const obsoletesRfcs = extractRfcNumbers(normalizedText, OBSOLETES_RE)
data.extractedElements.obsoletesRfc.push(...obsoletesRfcs.plainNumbers)

const updatesRfcs = extractRfcNumbers(normalizedText, UPDATES_RE)
data.extractedElements.updatesRfc.push(...updatesRfcs.plainNumbers)

for (const line of rawText.split('\n')) {
const trimmedLine = line.trim()
lineIdx++
Expand Down Expand Up @@ -459,3 +471,37 @@ function hasBoilerplateMatch (text, ...regexGroups) {
}
return false
}

/**
* Extract RFC numbers from the text
*
* @param {string} text Text to extract RFC numbers from
* @param {RegExp} regex Regular expression to extract RFC numbers
* @returns {Array<string>} Extracted RFC numbers
*/
function extractRfcNumbers (text, regex) {
const matches = {
rfcWithPrefix: [],
plainNumbers: []
}

let match
while ((match = regex.exec(text)) !== null) {
const rfcList = match[0]
if (rfcList) {
const numbers = rfcList
.match(/\b(RFC\s*[0-9]+|[0-9]+)\b/gi)
?.map(num => num.trim()) || []
numbers.forEach(num => {
if (/^RFC\s*[0-9]+$/i.test(num)) {
matches.rfcWithPrefix.push(num)
matches.plainNumbers.push(num.replace(/^RFC\s*/i, ''))
} else {
matches.plainNumbers.push(num)
}
})
}
}

return matches
}
17 changes: 17 additions & 0 deletions tests/fixtures/txt-blocks/section-blocks.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,23 @@ Updates: 6789, 7890, 8901, 9012 (if approved)
draft-ietf-idr-rt-derived-community-05
`

export const metaWithoutObsoleteAndUpdatesTXTBlock = `
idr Z. Zhang
Internet-Draft J. Haas
Intended status: Standards Track Juniper Networks
Expires: 8 September 2023 K. Patel
Arrcus
21 January 2025
Extended Communities Derived from Route Targets
draft-ietf-idr-rt-derived-community-05
`

export const authorAddressTXTBlock = `
Authors' Addresses
Expand Down
Loading

0 comments on commit f5fb8ec

Please sign in to comment.