#!/usr/bin/env python3 import subprocess import msgpack from string import Template import re import textwrap import os import io void_func_template = Template('''\ public func ${func_name}(${args} expectsReturnValue: Bool = false ) -> Completable { let params: [RxNeovimApi.Value] = [ ${params} ] if expectsReturnValue { return self .checkBlocked( self.rpc(method: "${nvim_func_name}", params: params, expectsReturnValue: expectsReturnValue) ) .asCompletable() } return self .rpc(method: "${nvim_func_name}", params: params, expectsReturnValue: expectsReturnValue) .asCompletable() } ''') get_mode_func_template = Template('''\ public func ${func_name}(${args} ) -> Single<${result_type}> { let params: [RxNeovimApi.Value] = [ ${params} ] return self .rpc(method: "${nvim_func_name}", params: params, expectsReturnValue: true) .map { value in guard let result = (${return_value}) else { throw RxNeovimApi.Error.conversion(type: ${result_type}.self) } return result } } ''') func_template = Template('''\ public func ${func_name}(${args} errWhenBlocked: Bool = true ) -> Single<${result_type}> { let params: [RxNeovimApi.Value] = [ ${params} ] func transform(_ value: Value) throws -> ${result_type} { guard let result = (${return_value}) else { throw RxNeovimApi.Error.conversion(type: ${result_type}.self) } return result } if errWhenBlocked { return self .checkBlocked( self.rpc(method: "${nvim_func_name}", params: params, expectsReturnValue: true) ) .map(transform) } return self .rpc(method: "${nvim_func_name}", params: params, expectsReturnValue: true) .map(transform) } ''') extension_template = Template('''\ // Auto generated for nvim version ${version}. // See bin/generate_api_methods.py import Foundation import MessagePack import RxSwift extension RxNeovimApi { public enum Error: Swift.Error { ${error_types} case exception(message: String) case validation(message: String) case blocked case conversion(type: Any.Type) case unknown init(_ value: RxNeovimApi.Value?) { let array = value?.arrayValue guard array?.count == 2 else { self = .unknown return } guard let rawValue = array?[0].uint64Value, let message = array?[1].stringValue else { self = .unknown return } switch rawValue { ${error_cases} default: self = .unknown } } } } extension RxNeovimApi { $body } extension RxNeovimApi.Buffer { public init?(_ value: RxNeovimApi.Value) { guard let (type, data) = value.extendedValue else { return nil } guard type == ${buffer_type} else { return nil } guard let handle = (try? unpack(data))?.value.int64Value else { return nil } self.handle = Int(handle) } } extension RxNeovimApi.Window { public init?(_ value: RxNeovimApi.Value) { guard let (type, data) = value.extendedValue else { return nil } guard type == ${window_type} else { return nil } guard let handle = (try? unpack(data))?.value.int64Value else { return nil } self.handle = Int(handle) } } extension RxNeovimApi.Tabpage { public init?(_ value: RxNeovimApi.Value) { guard let (type, data) = value.extendedValue else { return nil } guard type == ${tabpage_type} else { return nil } guard let handle = (try? unpack(data))?.value.int64Value else { return nil } self.handle = Int(handle) } } fileprivate func msgPackDictToSwift(_ dict: Dictionary?) -> Dictionary? { return dict?.compactMapToDict { k, v in guard let strKey = k.stringValue else { return nil } return (strKey, v) } } fileprivate func msgPackArrayDictToSwift(_ array: [RxNeovimApi.Value]?) -> [Dictionary]? { return array? .compactMap { v in v.dictionaryValue } .compactMap { d in msgPackDictToSwift(d) } } extension Dictionary { fileprivate func mapToDict(_ transform: ((key: Key, value: Value)) throws -> (K, V)) rethrows -> Dictionary { let array = try self.map(transform) return tuplesToDict(array) } fileprivate func compactMapToDict(_ transform: ((key: Key, value: Value)) throws -> (K, V)?) rethrows -> Dictionary { let array = try self.compactMap(transform) return tuplesToDict(array) } fileprivate func tuplesToDict(_ sequence: S) -> Dictionary where S.Iterator.Element == (K, V) { var result = Dictionary(minimumCapacity: sequence.underestimatedCount) for (key, value) in sequence { result[key] = value } return result } } ''') def snake_to_camel(snake_str): components = snake_str.split('_') return components[0] + "".join(x.title() for x in components[1:]) def nvim_type_to_swift(nvim_type): if nvim_type == 'Boolean': return 'Bool' if nvim_type == 'Integer': return 'Int' if nvim_type == 'Float': return nvim_type if nvim_type == 'void': return 'Void' if nvim_type == 'String': return 'String' if nvim_type == 'Array': return 'RxNeovimApi.Value' if nvim_type == 'Dictionary': return 'Dictionary' if nvim_type == 'Buffer': return 'RxNeovimApi.Buffer' if nvim_type == 'Window': return 'RxNeovimApi.Window' if nvim_type == 'Tabpage': return 'RxNeovimApi.Tabpage' if nvim_type == 'Object': return 'RxNeovimApi.Value' if nvim_type.startswith('ArrayOf('): match = re.match(r'ArrayOf\((.*?)(?:, \d+)*\)', nvim_type) return '[{}]'.format(nvim_type_to_swift(match.group(1))) return 'RxNeovimApi.Value' def msgpack_to_swift(msgpack_value_name, type): if type == 'Bool': return f'{msgpack_value_name}.boolValue' if type == 'Int': return f'({msgpack_value_name}.int64Value == nil ? nil : Int({msgpack_value_name}.int64Value!))' if type == 'Float': return f'{msgpack_value_name}.floatValue' if type == 'Void': return f'()' if type == 'String': return f'{msgpack_value_name}.stringValue' if type == 'RxNeovimApi.Value': return f'Optional({msgpack_value_name})' if type in 'RxNeovimApi.Buffer': return f'RxNeovimApi.Buffer({msgpack_value_name})' if type in 'RxNeovimApi.Window': return f'RxNeovimApi.Window({msgpack_value_name})' if type in 'RxNeovimApi.Tabpage': return f'RxNeovimApi.Tabpage({msgpack_value_name})' if type.startswith('Dictionary<'): return f'msgPackDictToSwift({msgpack_value_name}.dictionaryValue)' if type.startswith('[Dictionary<'): return f'msgPackArrayDictToSwift({msgpack_value_name}.arrayValue)' if type.startswith('['): element_type = re.match(r'\[(.*)\]', type).group(1) return f'{msgpack_value_name}.arrayValue?.compactMap({{ v in {msgpack_to_swift("v", element_type)} }})' return 'RxNeovimApi.Value' def swift_to_msgpack_value(name, type): if type == 'Bool': return f'.bool({name})' if type == 'Int': return f'.int(Int64({name}))' if type == 'Float': return f'.float({name})' if type == 'Void': return f'.nil()' if type == 'String': return f'.string({name})' if type == 'Dictionary': return f'.map({name}.mapToDict({{ (Value.string($0), $1) }}))' if type == 'RxNeovimApi.Value': return name if type in ['RxNeovimApi.Buffer', 'RxNeovimApi.Window', 'RxNeovimApi.Tabpage']: return f'.int(Int64({name}.handle))' if type.startswith('['): match = re.match(r'\[(.*)\]', type) test = '$0' return f'.array({name}.map {{ {swift_to_msgpack_value(test, match.group(1))} }})' def parse_args(raw_params): types = [nvim_type_to_swift(p[0]) for p in raw_params] names = [p[1] for p in raw_params] params = dict(zip(names, types)) result = '\n'.join([n + ': ' + t + ',' for n, t in params.items()]) if not result: return '' return '\n' + textwrap.indent(result, ' ') def parse_params(raw_params): types = [nvim_type_to_swift(p[0]) for p in raw_params] names = [p[1] for p in raw_params] params = dict(zip(names, types)) result = '\n'.join([swift_to_msgpack_value(n, t) + ',' for n, t in params.items()]) return textwrap.indent(result, ' ').strip() def parse_function(f): args = parse_args(f['parameters']) template = void_func_template if f['return_type'] == 'void' else func_template template = get_mode_func_template if f['name'] == 'nvim_get_mode' else template result = template.substitute( func_name=snake_to_camel(f['name'][5:]), nvim_func_name=f['name'], args=args, params=parse_params(f['parameters']), result_type=nvim_type_to_swift(f['return_type']), return_value=msgpack_to_swift('value', nvim_type_to_swift(f['return_type'])) ) return result def parse_version(version): return '.'.join([str(v) for v in [version['major'], version['minor'], version['patch']]]) def parse_error_types(error_types): return textwrap.indent( '\n'.join( [f'public static let {t.lower()}RawValue = UInt64({v["id"]})' for t, v in error_types.items()] ), ' ' ).lstrip() def parse_error_cases(error_types): return textwrap.indent( '\n'.join( [f'case Error.{t.lower()}RawValue: self = .{t.lower()}(message: message)' for t, v in error_types.items()] ), ' ' ).lstrip() if __name__ == '__main__': result_file_path = './Sources/RxNeovim/RxNeovimApi.generated.swift' nvim_path = os.environ['NVIM_PATH'] if 'NVIM_PATH' in os.environ else 'nvim' nvim_output = subprocess.run([nvim_path, '--api-info'], stdout=subprocess.PIPE) api = msgpack.unpackb(nvim_output.stdout) version = parse_version(api['version']) functions = [f for f in api['functions'] if 'deprecated_since' not in f] body = '\n'.join([parse_function(f) for f in functions]) result = extension_template.substitute( body=body, version=version, error_types=parse_error_types(api['error_types']), error_cases=parse_error_cases(api['error_types']), buffer_type=api['types']['Buffer']['id'], window_type=api['types']['Window']['id'], tabpage_type=api['types']['Tabpage']['id'] ) with io.open(result_file_path, 'w') as api_methods_file: api_methods_file.write(result)