69ee021469
also included the generated docs also changed Travis build script to not run the formatting check on OSX
277 lines
No EOL
9.9 KiB
Python
Executable file
277 lines
No EOL
9.9 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
|
|
import re
|
|
import subprocess as sp
|
|
import sys
|
|
import json
|
|
import urllib.request as req
|
|
import jsonschema
|
|
|
|
|
|
re_full = re.compile(r'(?P<name>^.*?$)(?P<desc>.*?)(^Argument.*?$(?P<args>.*?))?(^Result[^\n,]*?:\s*$(?P<resl>.*?))?(^Exampl.*?$(?P<exmp>.*))?', re.DOTALL | re.MULTILINE)
|
|
re_argline = re.compile(r'^("?)(?P<name>\w.*?)\1(\s*:.+?,?\s*)?\s+\((?P<type>.*?)\)\s*(?P<desc>.*?)\s*$', re.DOTALL)
|
|
|
|
|
|
def get_obj_from_dirty_text(full_object: str):
|
|
lines = full_object.splitlines()
|
|
lefts = []
|
|
i = 0
|
|
while i < len(lines):
|
|
idx = lines[i].find('(')
|
|
left = lines[i][0:idx].strip() if idx >= 0 else lines[i]
|
|
left = left.rstrip('.') # handling , ...
|
|
left = left.strip()
|
|
left = left.rstrip(',')
|
|
lefts.append(left)
|
|
while idx >= 0 and i < len(lines) - 1:
|
|
idx2 = len(re.match(r'^\s*', lines[i + 1]).group())
|
|
if idx2 > idx:
|
|
lines[i] += lines.pop(i + 1)[idx2 - 1:]
|
|
else:
|
|
break
|
|
i += 1
|
|
|
|
ret = None
|
|
try:
|
|
property_stack = []
|
|
object_stack = []
|
|
name_stack = []
|
|
|
|
last_name = None
|
|
for i in range(0, len(lines)):
|
|
left = lefts[i]
|
|
if not left:
|
|
continue
|
|
line = lines[i].strip()
|
|
|
|
arg_parsed = re_argline.fullmatch(line)
|
|
property_refined_type = 'object'
|
|
if arg_parsed is not None:
|
|
property_name, property_type, property_desc = arg_parsed.group('name', 'type', 'desc')
|
|
property_refined_type, property_required, property_child = get_type(property_type, None)
|
|
|
|
if property_refined_type is not 'array' and property_refined_type is not 'object':
|
|
property_stack[-1][property_name] = {
|
|
'type': property_refined_type,
|
|
'description': property_desc
|
|
}
|
|
else:
|
|
last_name = property_name
|
|
elif len(left) > 1:
|
|
match = re.match(r'^(\[)?"(?P<name>\w.*?)"(\])?.*', left)
|
|
last_name = match.group('name')
|
|
if match.group(1) is not None and match.group(3) is not None:
|
|
left = '['
|
|
property_refined_type = 'string'
|
|
if 'string' not in line:
|
|
raise NotImplementedError('Not implemented: ' + line)
|
|
|
|
if left.endswith('['):
|
|
object_stack.append({'type': 'array', 'items': {'type': property_refined_type}})
|
|
property_stack.append({})
|
|
name_stack.append(last_name)
|
|
elif left.endswith('{'):
|
|
object_stack.append({'type': 'object'})
|
|
property_stack.append({})
|
|
name_stack.append(last_name)
|
|
elif (left.endswith(']') and '[' not in left) or (left.endswith('}') and '{' not in left):
|
|
obj = object_stack.pop()
|
|
prop = property_stack.pop()
|
|
name = name_stack.pop()
|
|
if len(prop) > 0:
|
|
if 'items' in obj:
|
|
obj['items']['properties'] = prop
|
|
else:
|
|
obj['properties'] = prop
|
|
if len(property_stack) > 0:
|
|
if 'items' in object_stack[-1]:
|
|
object_stack[-1]['items']['type'] = obj['type']
|
|
if len(prop) > 0:
|
|
object_stack[-1]['items']['properties'] = prop
|
|
else:
|
|
if name is None:
|
|
raise RuntimeError('Not expected')
|
|
property_stack[-1][name] = obj
|
|
else:
|
|
ret = obj
|
|
if ret is not None:
|
|
if i + 1 < len(lines) - 1:
|
|
print('Ignoring this data (below the parsed object): ' + "\n".join(lines[i+1:]), file=sys.stderr)
|
|
return ret
|
|
except Exception as e:
|
|
print('Exception: ' + str(e), file=sys.stderr)
|
|
print('Unable to cope with: ' + '\n'.join(lines), file=sys.stderr)
|
|
return None
|
|
|
|
|
|
def get_type(arg_type: str, full_line: str):
|
|
if arg_type is None:
|
|
return 'string', True, None
|
|
|
|
required = 'required' in arg_type or 'optional' not in arg_type
|
|
|
|
arg_type = arg_type.lower()
|
|
if 'array' in arg_type:
|
|
return 'array', required, None
|
|
if 'numeric' in arg_type:
|
|
return 'number', required, None
|
|
if 'bool' in arg_type:
|
|
return 'boolean', required, None
|
|
if 'string' in arg_type:
|
|
return 'string', required, None
|
|
if 'object' in arg_type:
|
|
properties = get_obj_from_dirty_text(full_line) if full_line is not None else None
|
|
return 'object', required, properties
|
|
|
|
print('Unable to derive type from: ' + arg_type, file=sys.stderr)
|
|
return None, False, None
|
|
|
|
|
|
def get_default(arg_refined_type: str, arg_type: str):
|
|
if 'default=' in arg_type:
|
|
if 'number' in arg_refined_type:
|
|
return int(re.match('.*default=([^,)]+)', arg_type).group(1))
|
|
if 'string' in arg_refined_type:
|
|
return re.match('.*default=([^,)]+)', arg_type).group(1)
|
|
if 'boolean' in arg_refined_type:
|
|
if 'default=true' in arg_type:
|
|
return True
|
|
if 'default=false' in arg_type:
|
|
return False
|
|
raise NotImplementedError('Not implemented: ' + arg_type)
|
|
if 'array' in arg_type:
|
|
raise NotImplementedError('Not implemented: ' + arg_type)
|
|
return None
|
|
|
|
|
|
def parse_single_argument(line: str):
|
|
if line:
|
|
line = line.strip()
|
|
if not line or line.startswith('None'):
|
|
return None, None, False
|
|
|
|
arg_parsed = re_argline.fullmatch(line)
|
|
if arg_parsed is None:
|
|
if line.startswith('{') or line.startswith('['):
|
|
return get_obj_from_dirty_text(line), None, True
|
|
else:
|
|
print("Unparsable argument: " + line, file=sys.stderr)
|
|
descriptor = {
|
|
'type': 'array' if line.startswith('[') else 'object',
|
|
'description': line,
|
|
}
|
|
return descriptor, None, True
|
|
|
|
arg_name, arg_type, arg_desc = arg_parsed.group('name', 'type', 'desc')
|
|
if not arg_type:
|
|
raise NotImplementedError('Not implemented: ' + arg_type)
|
|
arg_refined_type, arg_required, arg_properties = get_type(arg_type, arg_desc)
|
|
|
|
if arg_properties is not None:
|
|
return arg_properties, arg_name, arg_required
|
|
|
|
arg_refined_default = get_default(arg_refined_type, arg_type)
|
|
arg_desc = re.sub('\s+', ' ', arg_desc.strip()) \
|
|
if arg_desc and arg_refined_type is not 'object' and arg_refined_type is not 'array' \
|
|
else arg_desc.strip() if arg_desc else ''
|
|
|
|
descriptor = {
|
|
'type': arg_refined_type,
|
|
'description': arg_desc,
|
|
}
|
|
if arg_refined_default is not None:
|
|
descriptor['default'] = arg_refined_default
|
|
return descriptor, arg_name, arg_required
|
|
|
|
|
|
def parse_params(args: str):
|
|
arguments = {}
|
|
requireds = []
|
|
if args:
|
|
for line in re.split('\s*\d+\.\s+', args, re.DOTALL):
|
|
descriptor, name, required = parse_single_argument(line)
|
|
if descriptor is None:
|
|
continue
|
|
if required:
|
|
requireds.append(name)
|
|
arguments[name] = descriptor
|
|
return arguments, requireds
|
|
|
|
|
|
def get_api(section_name: str, command: str, command_help: str):
|
|
|
|
parsed = re_full.fullmatch(command_help)
|
|
if parsed is None:
|
|
raise RuntimeError('Unable to resolve help format for ' + command)
|
|
|
|
name, desc, args, resl, exmp = parsed.group('name', 'desc', 'args', 'resl', 'exmp')
|
|
|
|
properties, required = parse_params(args)
|
|
result_descriptor, result_name, result_required = parse_single_argument(resl)
|
|
|
|
desc = re.sub('\s+', ' ', desc.strip()) if desc else name
|
|
example_array = exmp.splitlines() if exmp else []
|
|
|
|
ret = {
|
|
'summary': desc,
|
|
'description': example_array,
|
|
'tags': [section_name],
|
|
'params': {
|
|
'type': 'object',
|
|
'properties': properties,
|
|
'required': required
|
|
},
|
|
}
|
|
if result_descriptor is not None:
|
|
ret['result'] = result_descriptor
|
|
return ret
|
|
|
|
|
|
def write_api():
|
|
if len(sys.argv) < 2:
|
|
print("Missing required argument: <path to CLI tool>", file=sys.stderr)
|
|
sys.exit(1)
|
|
cli_tool = sys.argv[1]
|
|
result = sp.run([cli_tool, "help"], stdout=sp.PIPE, universal_newlines=True)
|
|
commands = result.stdout
|
|
sections = re.split('^==\s*(.*?)\s*==$', commands, flags=re.MULTILINE)
|
|
methods = {}
|
|
for section in sections:
|
|
if not section:
|
|
continue
|
|
lines = section.splitlines()
|
|
if len(lines) == 1:
|
|
section_name = lines[0]
|
|
continue
|
|
for command in sorted(lines[1:]):
|
|
if not command:
|
|
continue
|
|
command = command.split(' ')[0]
|
|
result = sp.run([cli_tool, "help", command], stdout=sp.PIPE, universal_newlines=True)
|
|
methods[command] = get_api(section_name, command, result.stdout)
|
|
|
|
version = sp.run([cli_tool, "--version"], stdout=sp.PIPE, universal_newlines=True)
|
|
wrapper = {
|
|
'$schema': 'https://rawgit.com/mzernetsch/jrgen/master/jrgen-spec.schema.json',
|
|
'jrgen': '1.1',
|
|
'jsonrpc': '1.0', # see https://github.com/bitcoin/bitcoin/pull/12435
|
|
'info': {
|
|
'title': 'lbrycrd RPC API',
|
|
'version': version.stdout.strip(),
|
|
'description': []
|
|
},
|
|
'definitions': {}, # for items used in $ref further down
|
|
'methods': methods,
|
|
}
|
|
|
|
schema = req.urlopen(wrapper['$schema']).read().decode('utf-8')
|
|
try:
|
|
jsonschema.validate(wrapper, schema)
|
|
except Exception as e:
|
|
print('From schema validation: ' + str(e), file=sys.stderr)
|
|
|
|
print(json.dumps(wrapper, indent=4))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
write_api() |