You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

176 lines
6.2 KiB

  1. #!/usr/bin/env python3
  2. import re
  3. import sys
  4. anchor = '###'
  5. min_entries_per_section = 3
  6. auth_keys = ['apiKey', 'OAuth', 'X-Mashape-Key', 'No']
  7. punctuation = ['.', '?', '!']
  8. https_keys = ['Yes', 'No']
  9. cors_keys = ['Yes', 'No', 'Unknown']
  10. index_title = 0
  11. index_desc = 1
  12. index_auth = 2
  13. index_https = 3
  14. index_cors = 4
  15. index_link = 5
  16. num_segments = 5
  17. errors = []
  18. title_links = []
  19. previous_links = []
  20. anchor_re = re.compile(anchor + '\s(.+)')
  21. section_title_re = re.compile('\*\s\[(.*)\]')
  22. link_re = re.compile('\[(.+)\]\((http.*)\)')
  23. def add_error(line_num, message):
  24. """adds an error to the dynamic error list"""
  25. err = '(L{:03d}) {}'.format(line_num + 1, message)
  26. errors.append(err)
  27. def check_alphabetical(lines):
  28. """
  29. checks if all entries per section are in alphabetical order based in entry title
  30. """
  31. sections = {}
  32. section_line_num = {}
  33. for line_num, line in enumerate(lines):
  34. if line.startswith(anchor):
  35. category = line.split(anchor)[1].strip()
  36. sections[category] = []
  37. section_line_num[category] = line_num
  38. continue
  39. if not line.startswith('|') or line.startswith('|---'):
  40. continue
  41. raw_title = [x.strip() for x in line.split('|')[1:-1]][0]
  42. title_re_match = link_re.match(raw_title)
  43. if title_re_match:
  44. sections[category].append(title_re_match.group(1).upper())
  45. for category, entries in sections.items():
  46. if sorted(entries) != entries:
  47. add_error(section_line_num[category], "{} section is not in alphabetical order".format(category))
  48. def check_entry(line_num, segments):
  49. # START Title
  50. raw_title = segments[index_title]
  51. title_re_match = link_re.match(raw_title)
  52. # url should be wrapped in '[TITLE](LINK)' Markdown syntax
  53. if not title_re_match:
  54. add_error(line_num, 'Title syntax should be "[TITLE](LINK)"')
  55. else:
  56. # do not allow "... API" in the entry title
  57. title = title_re_match.group(1)
  58. if title.upper().endswith(' API'):
  59. add_error(line_num, 'Title should not end with "... API". Every entry is an API here!')
  60. # do not allow duplicate links
  61. link = title_re_match.group(2)
  62. if link in previous_links:
  63. add_error(line_num, 'Duplicate link - entries should only be included in one section')
  64. else:
  65. previous_links.append(link)
  66. # END Title
  67. # START Description
  68. # first character should be capitalized
  69. char = segments[index_desc][0]
  70. if char.upper() != char:
  71. add_error(line_num, "first character of description is not capitalized")
  72. # last character should not punctuation
  73. char = segments[index_desc][-1]
  74. if char in punctuation:
  75. add_error(line_num, "description should not end with {}".format(char))
  76. desc_length = len(segments[index_desc])
  77. if desc_length > 100:
  78. add_error(line_num, "description should not exceed 100 characters (currently {})".format(desc_length))
  79. # END Description
  80. # START Auth
  81. # values should conform to valid options only
  82. auth = segments[index_auth]
  83. if auth != 'No' and (not auth.startswith('`') or not auth.endswith('`')):
  84. add_error(line_num, "auth value is not enclosed with `backticks`")
  85. if auth.replace('`', '') not in auth_keys:
  86. add_error(line_num, "{} is not a valid Auth option".format(auth))
  87. # END Auth
  88. # START HTTPS
  89. # values should conform to valid options only
  90. https = segments[index_https]
  91. if https not in https_keys:
  92. add_error(line_num, "{} is not a valid HTTPS option".format(https))
  93. # END HTTPS
  94. # START CORS
  95. # values should conform to valid options only
  96. cors = segments[index_cors]
  97. if cors not in cors_keys:
  98. add_error(line_num, "{} is not a valid CORS option".format(cors))
  99. # END CORS
  100. def check_format(filename):
  101. """
  102. validates that each line is formatted correctly,
  103. appending to error list as needed
  104. """
  105. with open(filename) as fp:
  106. lines = list(line.rstrip() for line in fp)
  107. check_alphabetical(lines)
  108. # START Check Entries
  109. num_in_category = min_entries_per_section + 1
  110. category = ""
  111. category_line = 0
  112. for line_num, line in enumerate(lines):
  113. if section_title_re.match(line):
  114. title_links.append(section_title_re.match(line).group(1))
  115. # check each section for the minimum number of entries
  116. if line.startswith(anchor):
  117. match = anchor_re.match(line)
  118. if match:
  119. if match.group(1) not in title_links:
  120. add_error(line_num, "section header ({}) not added as a title link".format(match.group(1)))
  121. else:
  122. add_error(line_num, "section header is not formatted correctly")
  123. if num_in_category < min_entries_per_section:
  124. add_error(category_line, "{} section does not have the minimum {} entries (only has {})".format(
  125. category, min_entries_per_section, num_in_category))
  126. category = line.split(' ')[1]
  127. category_line = line_num
  128. num_in_category = 0
  129. continue
  130. # skips lines that we do not care about
  131. if not line.startswith('|') or line.startswith('|---'):
  132. continue
  133. num_in_category += 1
  134. segments = line.split('|')[1:-1]
  135. if len(segments) < num_segments:
  136. add_error(line_num, "entry does not have all the required sections (have {}, need {})".format(
  137. len(segments), num_segments))
  138. continue
  139. # START Global
  140. for segment in segments:
  141. # every line segment should start and end with exactly 1 space
  142. if len(segment) - len(segment.lstrip()) != 1 or len(segment) - len(segment.rstrip()) != 1:
  143. add_error(line_num, "each segment must start and end with exactly 1 space")
  144. # END Global
  145. segments = [seg.strip() for seg in segments]
  146. check_entry(line_num, segments)
  147. # END Check Entries
  148. def main():
  149. if len(sys.argv) < 2:
  150. print("No file passed (file should contain Markdown table syntax)")
  151. sys.exit(1)
  152. check_format(sys.argv[1])
  153. if len(errors) > 0:
  154. for err in errors:
  155. print(err)
  156. sys.exit(1)
  157. if __name__ == "__main__":
  158. main()