generate_module_docs.py 13 KB

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