Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add requirement section specification #1211

Closed
wants to merge 17 commits into from
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,10 @@ bin

# Misspell binary
internal/tools/bin
.tools

# Pytest cache
__pycache__

# JSON files generated by the specification parser
*.json
10 changes: 9 additions & 1 deletion .markdownlint.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Default state for all rules
default: true

# heading-increment"
MD001: false

# ul-style
MD004: false

Expand All @@ -15,9 +18,14 @@ MD024:
MD029:
style: ordered

# no-blanks-blockquote
MD028: false

# no-inline-html
MD033: false

# no-space-in-code
MD038: false

# fenced-code-language
MD040: false

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Technical committee holds regular meetings, notes are held
## Table of Contents

- [Overview](specification/overview.md)
- [Requirements](specification/requirement-sections.md)
- [Glossary](specification/glossary.md)
- [Versioning and stability for OpenTelemetry clients](specification/versioning-and-stability.md)
- [Library Guidelines](specification/library-guidelines.md)
Expand Down
129 changes: 129 additions & 0 deletions internal/tools/specification_parser/specification_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
from re import finditer, findall
from json import dumps
from os.path import curdir, abspath, join, splitext
from os import walk


def find_markdown_file_paths(root):
markdown_file_paths = []

for root_path, _, file_paths, in walk(root):
for file_path in file_paths:
absolute_file_path = join(root_path, file_path)

_, file_extension = splitext(absolute_file_path)

if file_extension == ".md":
markdown_file_paths.append(absolute_file_path)

return markdown_file_paths


def parse_requirements(markdown_file_paths):
requirements = {}

for markdown_file_path in markdown_file_paths:

with open(markdown_file_path, "r") as markdown_file:

text = markdown_file.read()

requirement_matches = [
requirement_match.groupdict() for requirement_match in (
finditer(
r"###### requirement:\s(?P<key>[_\w]+)\n\n"
r"(?P<description>(>.*\n)+)",
text,
)
)
]

if not requirement_matches:
continue

md_file_path = "".join([splitext(markdown_file_path)[0], ".md"])

requirements[md_file_path] = {}

for requirement in requirement_matches:

requirement_key = requirement["key"]

assert (
requirement_key not in
requirements[md_file_path].keys()
), "Repeated requirement key {} found in {}".format(
requirement_key, markdown_file_path
)

requirement_description = requirement["description"].strip()

BCP_14_keyword_matches = []

for BCP_14_keyword_regex in [
# 2. MUST NOT
r"MUST NOT",
r"SHALL NOT",
# 1. MUST
r"MUST(?! NOT)",
r"REQUIRED",
r"SHALL(?! NOT)",
# 4. SHOULD NOT
r"SHOULD NOT",
r"NOT RECOMMENDED",
# 3. SHOULD
r"SHOULD(?! NOT)",
r"(?<!NOT )RECOMMENDED",
# 5. MAY
r"MAY",
r"OPTIONAL",

]:
BCP_14_keyword_matches.extend(
findall(
BCP_14_keyword_regex,
requirement_description
)
)

requirement_key_path = "{}:{}".format(
markdown_file_path, requirement_key
)

assert (
len(BCP_14_keyword_matches) != 0
), "No BCP 14 keywords were found in {}".format(
requirement_key_path
)

requirements[md_file_path][requirement_key] = {}

requirements[md_file_path][requirement_key]["description"] = (
requirement_description
)

BCP_14_keyword_matches.reverse()

requirements[md_file_path][requirement_key][
"BCP 14 Keywords"
] = BCP_14_keyword_matches

return requirements


def write_json_specifications(requirements):
for md_absolute_file_path, requirement_sections in requirements.items():

with open(
"".join([splitext(md_absolute_file_path)[0], ".json"]), "w"
) as json_file:
json_file.write(dumps(requirement_sections, indent=4))

if __name__ == "__main__":
write_json_specifications(
parse_requirements(
find_markdown_file_paths(
join(abspath(curdir), "specification")
)
)
)
104 changes: 104 additions & 0 deletions internal/tools/specification_parser/specification_parser_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from unittest import TestCase
from os.path import abspath, curdir, join

from specification_parser import (
find_markdown_file_paths,
parse_requirements,
)


class TestSpecificationParser(TestCase):
"""
Tests for the specification parser
"""

@classmethod
def setUpClass(cls):
cls.current_directory_path = abspath(curdir)
cls.test_specification_md_path = join(
cls.current_directory_path, "test_specification.md"
)
cls.parsed_requirements = parse_requirements(
[cls.test_specification_md_path]
)[cls.test_specification_md_path]

def test_find_markdown_file_paths(self):
self.assertIn(
self.test_specification_md_path,
find_markdown_file_paths(self.current_directory_path)
)

def test_parseable_section_0(self):
assert self.parsed_requirements["parseable_section_0"] == {
"description": "> This MUST be done.",
"BCP 14 Keywords": ["MUST"]
}

def test_parseable_section_1(self):
assert self.parsed_requirements["parseable_section_1"] == {
"description": "> This MUST NOT be done.",
"BCP 14 Keywords": ["MUST NOT"]
}

def test_parseable_section_2(self):
assert self.parsed_requirements["parseable_section_2"] == {
"description": "> This SHOULD be done.",
"BCP 14 Keywords": ["SHOULD"]
}

def test_parseable_section_3(self):
assert self.parsed_requirements["parseable_section_3"] == {
"description": "> This SHOULD NOT be done.",
"BCP 14 Keywords": ["SHOULD NOT"]
}

def test_parseable_section_4(self):
assert self.parsed_requirements["parseable_section_4"] == {
"description": "> This MAY be done.",
"BCP 14 Keywords": ["MAY"]
}

def test_parseable_section_5(self):
assert self.parsed_requirements["parseable_section_5"] == {
"description": "> This **MAY** be done 5.",
"BCP 14 Keywords": ["MAY"]
}

def test_parseable_section_6(self):
assert self.parsed_requirements["parseable_section_6"] == {
"description": "> This *MAY* be done 6.",
"BCP 14 Keywords": ["MAY"]
}

def test_parseable_section_7(self):
assert self.parsed_requirements["parseable_section_7"] == {
"description": (
"> This *MAY* be done 7.\n> This is section 7."
),
"BCP 14 Keywords": ["MAY"]
}

def test_parseable_section_8(self):
assert self.parsed_requirements["parseable_section_8"] == {
"description": "> This *MAY* be done 8.\n>\n> This is section 8.",
"BCP 14 Keywords": ["MAY"]
}

def test_parseable_section_9(self):
assert self.parsed_requirements["parseable_section_9"] == {
"description": (
"> This *MAY* be done 9.\n>\n> This is section 9.\n>\n"
"> 1. Item 1\n> 2. Item 2\n> 1. Item 2.1\n"
"> 2. Item 2.2"
),
"BCP 14 Keywords": ["MAY"]
}

def test_parseable_section_10(self):
assert self.parsed_requirements["parseable_section_10"] == {
"description": (
"> This *MAY* be done 10\n>\n> If this is done, then this "
"MUST be that and MUST NOT be\n> something else."
),
"BCP 14 Keywords": ["MAY", "MUST", "MUST NOT"]
}
62 changes: 62 additions & 0 deletions internal/tools/specification_parser/test_specification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Test Specification

## Title

Some content.

###### requirement: parseable_section_0

> This MUST be done.

###### requirement: parseable_section_1

> This MUST NOT be done.

###### requirement: parseable_section_2

> This SHOULD be done.

###### requirement: parseable_section_3

> This SHOULD NOT be done.

###### requirement: parseable_section_4

> This MAY be done.

###### requirement: parseable_section_5

> This **MAY** be done 5.

###### requirement: parseable_section_6

> This *MAY* be done 6.

###### requirement: parseable_section_7

> This *MAY* be done 7.
> This is section 7.

###### requirement: parseable_section_8

> This *MAY* be done 8.
>
> This is section 8.

###### requirement: parseable_section_9

> This *MAY* be done 9.
>
> This is section 9.
>
> 1. Item 1
> 2. Item 2
> 1. Item 2.1
> 2. Item 2.2

###### requirement: parseable_section_10

> This *MAY* be done 10
>
> If this is done, then this MUST be that and MUST NOT be
> something else.
23 changes: 17 additions & 6 deletions specification/metrics/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,23 @@ can be configured at run time.

### Behavior of the API in the absence of an installed SDK

In the absence of an installed Metrics SDK, the Metrics API MUST consist only
of no-ops. None of the calls on any part of the API can have any side effects
or do anything meaningful. Meters MUST return no-op implementations of any
instruments. From a user's perspective, calls to these should be ignored without raising errors
(i.e., *no* `null` references MUST be returned in languages where accessing these results in errors).
The API MUST NOT throw exceptions on any calls made to it.
###### requirement: only_no_ops

> The metrics API MUST consist only of no-ops. None of the calls on any part of the API can have any side effects or do anything meaningful.

###### requirement: meters_return_no_ops

> Meters MUST return no-op implementations of any instruments.

From a user's perspective, calls to these should be ignored without raising errors.

###### requirement: no_null_references

> Null references MUST NOT be returned in languages where accessing these results in errors.

###### requirement: no_api_exceptions

> The API MUST NOT throw exceptions on any calls made to it.

### Measurements

Expand Down
Loading