generate_module_docs.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. # This file generates the modules.md content
  2. import os, json, sys, time, fnmatch, re
  3. class ModuleDocGenerator(object):
  4. PATH_GENERATED_MARKDOWN = "../docs/modules.md"
  5. PATH_MODULES_DOCS = "../docs/modules/"
  6. # Contains the output until it should be written to disk
  7. markdown_string = ""
  8. def execute(self, data):
  9. # Validate that we got some methods back. 155 is an arbitrary large number.
  10. if len(data) < 1:
  11. print "ERR: Invalid data"
  12. exit()
  13. functions_parsed = self.parse_function_list(data)
  14. self.output_markdown(functions_parsed)
  15. print "Markdown doc created successfully at " + self.PATH_GENERATED_MARKDOWN
  16. def parse_function_list(self, functions):
  17. data = {}
  18. for elem in functions:
  19. module = elem["module"]
  20. # TODO: What about the pvx module?
  21. if module == "":
  22. module = "core"
  23. if module not in data:
  24. data[module] = []
  25. data[module].append({"name": elem["name"], "return": elem["ret"], "params": elem["params"]})
  26. return data
  27. def output_markdown(self, data):
  28. self.markdown_header()
  29. for key in sorted(data):
  30. methods = data[key]
  31. # Sort the functions by name alphabetically
  32. methods = sorted(methods, key = lambda k: k['name'])
  33. self.markdown_section_heading(key)
  34. self.markdown_section_content(key, methods)
  35. self.markdown_write()
  36. return True
  37. def markdown_header(self):
  38. self.markdown_string += self.read_file_to_string("header.md")
  39. return True
  40. def markdown_section_heading(self, heading):
  41. self.markdown_string += "## " + heading + " ##\n\n"
  42. self.markdown_string += self.read_file_to_string(heading + "/" + heading + ".header.md")
  43. return True
  44. def markdown_section_content(self, module, methods):
  45. if module == "core":
  46. module_prefix = ""
  47. else:
  48. module_prefix = module + "."
  49. for value in methods:
  50. self.markdown_string += "#### KSR." + module_prefix + value["name"] + "() ####\n\n"
  51. # Sanitize the return values
  52. if value["return"] == "none":
  53. return_value = "void"
  54. else:
  55. return_value = value["return"]
  56. # Sanitize the parameter values
  57. if value["params"] == "none":
  58. params_value = ""
  59. else:
  60. params_value = value["params"]
  61. # Generate the output string for the markdown page
  62. self.markdown_string += "<a target='_blank' href='/docs/modules/devel/modules/" + module + ".html#" \
  63. + module + ".f." + value["name"] + "'> `" + return_value + " " + value["name"] \
  64. + "(" + params_value + ")` </a>\n\n"
  65. self.markdown_string += self.read_file_to_string(module + "/" + module + "." + value["name"] + ".md")
  66. return True
  67. def markdown_write(self):
  68. f = open(self.PATH_GENERATED_MARKDOWN, "w")
  69. f.write(self.markdown_string)
  70. f.close()
  71. return True
  72. def read_file_to_string(self, filename):
  73. path = self.PATH_MODULES_DOCS + filename
  74. if os.path.isfile(path):
  75. with open(path, 'r') as myfile:
  76. return myfile.read() + "\n"
  77. return ""
  78. class KemiFileExportParser(object):
  79. # These functions are created by a macro so makes the parsing somewhat tricky,
  80. # for now they are statically defined
  81. macro_functions = {
  82. "t_set_auto_inv_100": "int state",
  83. "t_set_disable_6xx": "int state",
  84. "t_set_disable_failover": "int state",
  85. "t_set_no_e2e_cancel_reason": "int state",
  86. "t_set_disable_internal_reply": "int state"
  87. }
  88. # These files export the KEMI functions in a special way so we map them manually
  89. # TODO: Discuss with @miconda if core/HDR/pv/x should be added as well or not
  90. special_exports = [
  91. #{"filename": "kemi.c", "export": "_sr_kemi_core", "folder": "/core/"},
  92. #{"filename": "kemi.c", "export": "_sr_kemi_hdr", "folder": "/core/"},
  93. #{"filename": "app_lua_mod.c", "export": "sr_kemi_app_lua_rpc_exports", "folder": "/modules/app_lua/"}
  94. ]
  95. def generate_kemi_export_list(self, source_path):
  96. files = self.list_c_files_in_directory(source_path)
  97. lists = []
  98. for file in files:
  99. with open(file) as f:
  100. lines = f.readlines()
  101. export_name = self.find_c_file_kemi_export(file, lines)
  102. if export_name:
  103. export_functions = self.extract_c_file_kemi_export_lines(file, lines, export_name)
  104. lists = lists + export_functions
  105. print "Found ", len(export_functions), "functions", "Total:", len(lists)
  106. # Handle some special files separately
  107. for elem in self.special_exports:
  108. file = source_path + elem["folder"] + elem["filename"]
  109. with open(file) as f:
  110. lines = f.readlines()
  111. lists = lists + self.extract_c_file_kemi_export_lines(file, lines, elem["export"])
  112. return lists
  113. def find_c_file_kemi_export(self, filename, lines):
  114. param = None
  115. for line in lines:
  116. if line.find("sr_kemi_modules_add") >= 0:
  117. line = line.lstrip(" ")
  118. line = line.lstrip("\t")
  119. if line.find("sr_kemi_modules_add") == 0:
  120. param = line[line.find("(") + 1:line.find(")")]
  121. print "INFO: ---- Found export", filename, param
  122. break
  123. else:
  124. if line != "int sr_kemi_modules_add(sr_kemi_t *klist)\n":
  125. print "ERR: Possible error at line: ", filename, line
  126. exit()
  127. return param
  128. def extract_c_file_kemi_export_lines(self, filename, lines, export_name):
  129. list_functions = []
  130. find_start = True
  131. for line in lines:
  132. if find_start and line.find("static sr_kemi_t " + export_name + "[]") >= 0:
  133. find_start = False
  134. elif not find_start:
  135. if line.find("};") >= 0:
  136. break
  137. line = line.lstrip(" \t")
  138. line = line.rstrip()
  139. list_functions.append(line)
  140. if len(list_functions) < 1:
  141. print "ERR: Couldn't parse file for exported functions: ", export_name
  142. exit()
  143. parsed_list = self.parse_kemi_export_c_lines(filename, list_functions)
  144. return parsed_list
  145. def parse_kemi_export_c_lines(self, filename, lines):
  146. elements = []
  147. function_lines = []
  148. temp_function = ""
  149. # We look for str_init which would be the start of each exported function
  150. for line in lines:
  151. if line.find("str_init") >= 0:
  152. if temp_function != "":
  153. function_lines.append(temp_function)
  154. temp_function = ""
  155. temp_function += line
  156. if temp_function != "":
  157. function_lines.append(temp_function)
  158. # Now we parse each exported function to extract its declaration
  159. for func in function_lines:
  160. function_lines_split = func.split(",{")
  161. if len(function_lines_split) < 2:
  162. print "ERR: Incorrect function line", func
  163. exit()
  164. declarations = function_lines_split[0].split(",")
  165. params = function_lines_split[1]
  166. # Extract the content from the definitions
  167. val_module = declarations[0]
  168. val_module = val_module[val_module.find('("') + 2:val_module.find('")')]
  169. val_function = declarations[1]
  170. val_function = val_function[val_function.find('("') + 2:val_function.find('")')]
  171. if declarations[2] == "SR_KEMIP_INT":
  172. val_return = "int"
  173. elif declarations[2] == "SR_KEMIP_STR":
  174. val_return = "string"
  175. elif declarations[2] == "SR_KEMIP_NONE":
  176. val_return = "void"
  177. elif declarations[2] == "SR_KEMIP_BOOL":
  178. val_return = "bool"
  179. else:
  180. print "ERR: Invalid return value", declarations[2], func
  181. exit()
  182. val_c_function = declarations[3].strip()
  183. # Count how many parameters the KEMI C function expects
  184. val_params = []
  185. itr = 0
  186. for val in params.rstrip("},").split(","):
  187. itr += 1
  188. # KEMI function has a maximum of 6 params
  189. if itr > 6:
  190. break
  191. pm = val.strip()
  192. if pm == "SR_KEMIP_INT":
  193. val_params.append("int")
  194. elif pm == "SR_KEMIP_STR":
  195. val_params.append("str")
  196. elif pm == "SR_KEMIP_NONE":
  197. continue
  198. else:
  199. print "Invalid return value", declarations[2], func
  200. exit()
  201. if itr != 6:
  202. print "ERR: Couldn't iterate the params: ", params
  203. exit()
  204. param_string = self.find_c_function_params(filename, val_c_function, val_return)
  205. param_string = self.prettify_params_list(val_function, param_string, val_params)
  206. elements.append({"module": val_module, "name": val_function, "ret": val_return, "params": param_string})
  207. return elements
  208. def prettify_params_list(self, function_name, function_declaration, kemi_types):
  209. # Validate the quantity and types of declaration vs export
  210. if function_declaration == "" and len(kemi_types) == 0:
  211. return ""
  212. params = function_declaration.split(",")
  213. if params[0].find("sip_msg_t") >= 0 or params[0].find("struct sip_msg") >= 0:
  214. params.pop(0)
  215. if len(params) != len(kemi_types):
  216. print "ERR: Mismatching quantity of params. Declaration for", function_name, ":", function_declaration, "KEMI:", kemi_types
  217. exit()
  218. for declared, type in zip(params, kemi_types):
  219. declared = declared.replace("*", "")
  220. declared = declared.strip().split(" ")[0]
  221. if declared != type:
  222. print "ERR: Mismatching type of params for", function_name, ":", function_declaration, " | ", kemi_types, " | Declared: ", declared, " Type: ", type
  223. exit()
  224. param_string = ""
  225. for param in params:
  226. param = param.strip()
  227. param = param.replace("*", "")
  228. if param[:3] == "str":
  229. temp = param.split(" ")
  230. param = "str" + ' "' + temp[1] + '"'
  231. param_string += param + ", "
  232. # Clean up the presentation of the params
  233. param_string = param_string.rstrip(", ")
  234. return param_string
  235. def find_c_function_params(self, filename, function_name, return_type):
  236. # First we try with the same file to find the declaration
  237. param_string = self.search_file_for_function_declaration(filename, function_name, return_type)
  238. # If we couldn't find it, let's try all files in the same folder as the first file
  239. if param_string:
  240. return param_string
  241. else:
  242. files = self.list_c_files_in_directory(os.path.dirname(filename))
  243. for file in files:
  244. param_string = self.search_file_for_function_declaration(file, function_name, return_type)
  245. if param_string:
  246. return param_string
  247. if function_name in self.macro_functions:
  248. return self.macro_functions[function_name]
  249. print "ERR: Couldn't find the function declaration", filename, function_name, return_type
  250. exit()
  251. def search_file_for_function_declaration(self, filename, function_name, return_type):
  252. # print "Searching file", filename, "for", function_name
  253. with open(filename) as f:
  254. lines = f.readlines()
  255. param_string = None
  256. found = False
  257. temp_string = ""
  258. # KEMI has some magic where most functions actually return INTs but KEMI maps them to void/bool
  259. if return_type == "void" or return_type == "bool":
  260. return_type = "int"
  261. # Look for declarations in format: static? return_type function_name(
  262. r = re.compile("^(?:static )?" + return_type + "[ \t]*(" + function_name + ")\(")
  263. for line in lines:
  264. m = r.match(line)
  265. if m:
  266. found = True
  267. if found:
  268. temp_string += line
  269. if line.find("{") >= 0:
  270. param_string = temp_string[temp_string.find('(') + 1:temp_string.find(')')]
  271. break
  272. return param_string
  273. def list_c_files_in_directory(self, path):
  274. matches = []
  275. for root, dirnames, filenames in os.walk(path):
  276. for filename in fnmatch.filter(filenames, '*.c'):
  277. matches.append(os.path.join(root, filename))
  278. return matches
  279. if __name__ == "__main__":
  280. reload(sys)
  281. sys.setdefaultencoding('utf8')
  282. try:
  283. if not os.path.isdir(sys.argv[1]):
  284. raise Exception('Not a valid directory')
  285. except:
  286. print "Please provide the path to the Kamailio src folder as the first argument"
  287. exit()
  288. print "Parsing the source"
  289. parser = KemiFileExportParser()
  290. data = parser.generate_kemi_export_list(sys.argv[1].rstrip("/"))
  291. fgen = ModuleDocGenerator()
  292. fgen.execute(data)
  293. print "Done"