├── .gitignore ├── Documents ├── json2idl.gif └── xcode_idl.png ├── LICENSE ├── README.md ├── json2idl.py ├── nu-scraper.py ├── protocols ├── APIKitRequest.swift ├── IDLProtocols.swift ├── JSON.swift └── Lensy.swift ├── swift_idl-test.py └── swift_idl.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # Swift-IDL 60 | out/ 61 | IDL/ 62 | -------------------------------------------------------------------------------- /Documents/json2idl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safx/swift-idl/be65c7741d570a4960781d08487801d47beb09b7/Documents/json2idl.gif -------------------------------------------------------------------------------- /Documents/xcode_idl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/safx/swift-idl/be65c7741d570a4960781d08487801d47beb09b7/Documents/xcode_idl.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift-IDL 2 | 3 | Swift-IDL generates Swift source from Swift source. 4 | 5 | Swift-IDL can generate Swift source code adding some functionality from inherited *peseudo* protocols as follows: 6 | 7 | * `Encodable` 8 | * `Decodable` 9 | * `ClassInit` (memberwise initializer for class and struct) 10 | * `Printable` (generates `CustomStringConvertible`) 11 | * `URLRequestHelper` 12 | * `Lensy` (Lensy API for [Lensy](https://github.com/safx/Lensy)) 13 | * `APIKitHelper` (REST API Helper for [APIKit](https://github.com/ishkawa/APIKit)) 14 | * `WSHelper` (WebSocket helper for [Starscream](https://github.com/daltoniam/starscream)) 15 | * `EnumStaticInit` (case-wise initializer for enum) (WIP, maybe dropped) 16 | 17 | All available protocols are declared in IDLProtocols.swift. 18 | 19 | ## Requirements 20 | 21 | * mako (`pip install mako`) 22 | * SourceKitten (required: 0.11.0 or more) 23 | * Xcode 7 (Set as default to use `xcode-select -s`) 24 | 25 | ## Up and Running 26 | 27 | 1. Create target, e.g., "IDL", which can omit some options in command line later, by choosing "Command Line Tool" to your project in Xcode. 28 | ![](Documents/xcode_idl.png) 29 | 1. Add scheme, e.g., "IDL". If you add target in Xcode, this step could be skipped. 30 | 1. Add Empty Swift file, which add target to "IDL" 31 | 1. `python swift-idl.py -o Source/gen YourProject.xcodeproj -f` 32 | 1. Add the output files to your project. 33 | 1. Add the additional Swift files from protocol directory, which depends on protocols you chosen. For example, add `JSONDecodable.swift` to your project, if you use `JSONDecodable`. 34 | 35 | ## Usage 36 | 37 | `swift-idl.py --help` will show the usage text. 38 | 39 | ```sh 40 | usage: swift_idl.py [-h] [-s SOURCEKITTEN] [-o OUTPUT_DIR] [-f] 41 | [project] [scheme] 42 | 43 | swift-idl: Swift source generator from Swift 44 | 45 | positional arguments: 46 | project project to parse 47 | scheme sceheme to parse 48 | 49 | optional arguments: 50 | -h, --help show this help message and exit 51 | -s SOURCEKITTEN, --sourcekitten SOURCEKITTEN 52 | path to sourcekitten 53 | -o OUTPUT_DIR, --output_dir OUTPUT_DIR 54 | directory to output 55 | -f, --force force to output 56 | ``` 57 | 58 | 59 | ## IDL Protocols 60 | 61 | ### Decodable 62 | 63 | ### Encodable 64 | 65 | ### APIKitHelper 66 | 67 | ```swift 68 | class CreateTalk: ClassInit, APIKitHelper, MyRequest { // router:"POST,topics/\(topicId)/talks" 69 | let topicId: Int 70 | let talkName: String 71 | let postIds: [Int] = [] 72 | } 73 | ``` 74 | 75 | You can specify `Response` by using `typealias` for API response type. 76 | If `typealias Response` is not declared, `typealias Response = Response` is inserted. 77 | 78 | You can add custom `Request` protocol e.g., `MyRequest`, for your customizing point. 79 | 80 | ### URLRequestHelper 81 | 82 | ```swift 83 | enum Router: URLRequestHelper { 84 | case GetMessages(id: Int, count: Int?) // router:",message/\(id)" 85 | case PostMessage(id: Int, message: String) // router:"POST,message/\(id)" 86 | } 87 | ``` 88 | 89 | 90 | ## Annotations 91 | 92 | We can customize output code to add annotations as formatted comment in member variables or cases. 93 | 94 | ```swift 95 | struct Blog: JSONDecodable { 96 | let title : String 97 | let authorName : String // JSON:"author-name" 98 | let homepageURL: NSURL? // JSON:"homepage-url" 99 | let faviconURL : NSURL? // JSON:"favicon-url" 100 | let updated : Bool = false // JSON:"-" 101 | } 102 | ``` 103 | 104 | ### `json` 105 | 106 | `json` annotation is basically same as in Go-lang. 107 | 108 | ```swift 109 | // json:"" 110 | ``` 111 | 112 | * Name: field name for JSON object. Variable name is used when name is omitted or empty string. 113 | As special case, if `Name` is `-`, this variable is ignored for encoding and decoding. 114 | 115 | ### `router` 116 | 117 | ```swift 118 | // router:"," 119 | ``` 120 | 121 | * `Method`: HTTP Method for the request like `GET` or `POST`. `GET` is used when `Method` is omitted or empty string. 122 | * `Path`: path of the request URL. The name of case or class is used when `Path` is omitted or empty string. 123 | If you want to represent a path with parameters, you can use the notation like string interpolation such like `\(myParam)`. 124 | 125 | 126 | ## json2idl.py 127 | 128 | If you'll want to use `APIKitHelper` or `JSONDecodable`, `json2idl.py` will help your work. 129 | It creates `struct` from JSON input. 130 | 131 | The following example creates APIKit's `Request` from a response of the GitHub Web API. 132 | 133 | ```bash 134 | curl 'https://api.example.com/some/api' | json2idl.py -a -c some-API >> IDL/SomeAPI.swift 135 | ``` 136 | 137 | ![](Documents/json2idl.gif) 138 | 139 | You should modify some keywords whose types are not determined. 140 | You also modify or comment out some keywords like `private` since `swift-idl` can't process properties of Swift's resorved words currently. 141 | 142 | You can use this command with option `-a` (APIKit). You should add properties for this request in this case. 143 | -------------------------------------------------------------------------------- /json2idl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from __future__ import print_function 4 | 5 | import argparse 6 | import functools 7 | import itertools 8 | import json 9 | import os 10 | import subprocess 11 | import sys 12 | import re 13 | 14 | 15 | ST_NONE = 1 16 | ST_BOOL = 2 17 | ST_INT = 3 18 | ST_STR = 4 19 | ST_FLOAT = 5 20 | ST_URL = 6 21 | ST_ANYOBJECT = 999 22 | 23 | def mergeIntoSingleObject(guessedObjectArray): 24 | keys = reduce(lambda a, e: a.union(set(e.keys())), guessedObjectArray, set()) 25 | merged = {} 26 | for obj in guessedObjectArray: 27 | for key in keys: 28 | value = obj.get(key, set([ST_NONE])) 29 | if type(value) == dict: 30 | merged[key] = value 31 | elif type(value) == list: 32 | merged[key] = mergeIntoSingleObject(value) 33 | elif key in merged: 34 | merged[key] = merged[key].union(value) 35 | else: 36 | merged[key] = value 37 | return [merged] 38 | 39 | def guessTypenameForArray(json): 40 | arr = [] 41 | for value in json: 42 | if type(value) == dict: 43 | arr.append(guessTypenameForDict(value)) 44 | 45 | if all([type(i) == dict for i in arr]): 46 | return mergeIntoSingleObject(arr) 47 | 48 | assert(False) 49 | return arr # Unexpected return 50 | 51 | 52 | def guessTypenameForDict(json): 53 | info = {} 54 | for key, value in json.iteritems(): 55 | if type(value) == dict: 56 | info[key] = guessTypenameForDict(value) 57 | elif type(value) == list: 58 | info[key] = guessTypenameForArray(value) 59 | else: 60 | info[key] = set([guessTypename(value)]) 61 | return info 62 | 63 | 64 | def toCamelCase(name, firstLetterUpper=True): 65 | ts = re.split('[-_/]', name) 66 | if len(ts) > 1: 67 | ret = ''.join([e.capitalize() for e in ts]) 68 | else: 69 | ret = name 70 | return (unicode.upper if firstLetterUpper else unicode.lower)(ret[0]) + ret[1:] 71 | 72 | 73 | def guessTypename(v): 74 | if type(v) == type(None): return ST_NONE 75 | 76 | typemap = { 77 | bool: ST_BOOL, 78 | int: ST_INT, 79 | str: ST_STR, 80 | unicode: ST_STR, 81 | float: ST_FLOAT 82 | } 83 | 84 | typename = typemap.get(type(v), ST_NONE) 85 | if typename == ST_STR: 86 | if v.find('http://') == 0 or v.find('https://') == 0: 87 | if v.find('{') == -1 and v.find('{') == -1: 88 | return ST_URL 89 | 90 | return typename 91 | 92 | def guessTypenameFromSet(ts): 93 | assert(len(ts) > 0) 94 | 95 | isOptional = ST_NONE in ts 96 | if isOptional: 97 | ts.remove(ST_NONE) 98 | if len(ts) == 0: return '<# AnyObject #>' 99 | 100 | typemap = { 101 | ST_BOOL: 'Bool', 102 | ST_INT: 'Int', 103 | ST_STR: 'String', 104 | ST_FLOAT: 'Float', 105 | ST_URL: 'NSURL', 106 | ST_ANYOBJECT: 'AnyObject' 107 | } 108 | 109 | if len(ts) == 1: 110 | s = list(ts)[0] 111 | else: 112 | if ST_STR in ts: 113 | s = ST_STR 114 | else: 115 | s = ST_ANYOBJECT 116 | 117 | ret = typemap.get(s, None) 118 | if ret == None: return '<# AnyObject #>' 119 | return ret + ('?' if isOptional else '') 120 | 121 | 122 | def printClass(info, name, level = 0): 123 | subdicts = [] 124 | 125 | print('\t' * level + 'struct %s: Codable {' % name) 126 | for key, value in info.iteritems(): 127 | fieldName = toCamelCase(key, False) 128 | comment = '' if fieldName == key else '// json:"%s"' % key 129 | typename = toCamelCase(key) 130 | 131 | if type(value) == dict: 132 | subdicts.append((value, typename)) 133 | elif type(value) == list: 134 | assert(len(value) == 1) 135 | subdicts.append((value[0], typename)) 136 | typename = '[' + typename + ']' 137 | else: 138 | typename = guessTypenameFromSet(value) 139 | 140 | print('\t' * (level+1) + 'let %-20s: %-20s %s' % (fieldName, typename, comment)) 141 | 142 | if len(subdicts) > 0: 143 | print() 144 | map(lambda e: printClass(e[0], e[1], level + 1), subdicts) 145 | print('\t' * level + '}') 146 | 147 | 148 | def parseArgs(): 149 | parser = argparse.ArgumentParser(description='Swift source generator from JSON') 150 | parser.add_argument('jsonfile', type=argparse.FileType('r'), nargs='?', help='json to parse', default=sys.stdin) 151 | parser.add_argument('-c', '--classname', type=str, default=None, help='class name') 152 | parser.add_argument('-p', '--parameter', type=str, default=None, help='annotation parameter') 153 | parser.add_argument('-a', '--apikit', action='store_true', help='APIKit') 154 | return parser.parse_args() 155 | 156 | 157 | def resolveStructName(args): 158 | cname = args.classname 159 | if cname: 160 | return toCamelCase(cname) 161 | 162 | name = args.jsonfile.name 163 | if name == '': 164 | return '<# ClassName #>' 165 | name, ext = os.path.splitext(os.path.basename(name)) 166 | return toCamelCase(name) 167 | 168 | 169 | def execute(): 170 | args = parseArgs() 171 | obj = json.loads(args.jsonfile.read()) 172 | args.jsonfile.close() 173 | 174 | info = guessTypenameForDict(obj) 175 | typename = resolveStructName(args) 176 | 177 | if args.apikit: 178 | param = args.parameter 179 | if not param: 180 | param = ',' + (args.classname or typename) 181 | print('struct %s: ClassInit, APIKitHelper, Request { // router:"%s"' % (typename, param)) 182 | print(' typealias APIKitResponse = %sResponse' % (typename,)) 183 | print('') 184 | printClass(info, typename + 'Response', 1) 185 | print('}') 186 | else: 187 | printClass(info, typename) 188 | 189 | if __name__ == '__main__': 190 | execute() 191 | -------------------------------------------------------------------------------- /nu-scraper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from pyquery import PyQuery as pq 4 | import json 5 | import re 6 | import sys 7 | import json2idl 8 | 9 | SITE_ROOT = 'https://developer.nulab-inc.com' 10 | API_ROOT = SITE_ROOT + '/docs/typetalk/' 11 | DOC_BASE_PATH = '/docs/typetalk/api/1/' 12 | 13 | typenameMap = { 14 | 'Number': 'Int', 15 | 'Boolean': 'Bool' 16 | } 17 | 18 | defaultValueMap = { 19 | 'Number': '0', 20 | 'Boolean': 'false', 21 | 'String': '""', 22 | } 23 | 24 | 25 | def printXCTAssert(value, path): 26 | path = path.replace('.embed.', '.embed!.') 27 | if len(path) > 5 and path[-5:] == '.type': 28 | path += '.rawValue' 29 | 30 | def p(v): 31 | s = str(v) 32 | if type(v) == str or type(v) == unicode: 33 | return '"%s"' % s.replace('"', '\\"').replace('\n', '\\n') 34 | elif type(v) == type(None): 35 | return 'nil' 36 | elif type(v) == bool: 37 | return s.lower() 38 | return s 39 | 40 | if type(value) == type(None): 41 | print('\t\t\t\tXCTAssertNil(%s)' % (path)) 42 | return 43 | 44 | if path[-3:].lower() == 'url' and (value.find('http://') == 0 or value.find('https://') == 0): 45 | path += '.absoluteString' 46 | 47 | m = re.match(r'(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})Z', str(value)) 48 | if m: 49 | path += '.description' 50 | value = m.group(1) + ' ' + m.group(2) + ' +0000' 51 | print('\t\t\t\tXCTAssertEqual(%s, %s)' % (path, p(value))) 52 | 53 | 54 | def printTestForJSONDictionary(dic, prefix='r'): 55 | for k,v in dic.items(): 56 | varName = ''.join([e if idx == 0 else e[0].upper() + e[1:] for idx, e in enumerate(k.split('_'))]) 57 | pf = prefix + '.' + varName 58 | t = type(v) 59 | if t == dict: 60 | printTestForJSONDictionary(v, pf) 61 | elif t == list: 62 | printTestForJSONArray(v, pf) 63 | else: 64 | printXCTAssert(v, pf) 65 | 66 | def printTestForJSONArray(arr, prefix='r.'): 67 | print('\t\t\t\tXCTAssertEqual(%s.count, %d)' % (prefix, len(arr))) 68 | for k,v in enumerate(arr): 69 | pf = prefix + '[' + str(k) + ']' 70 | t = type(v) 71 | if t == dict: 72 | printTestForJSONDictionary(v, pf) 73 | elif t == list: 74 | printTestForJSONArray(v, pf) 75 | else: 76 | printXCTAssert(v, pf) 77 | 78 | 79 | class ParamInfo(object): 80 | def __init__(self, trElement): 81 | tds = [pq(trElement)('td').eq(j).text() for j in range(3)] 82 | ns = tds[0].split() 83 | self.name = ns[0] 84 | self.optional = len(ns) > 1 and ns[1] == '(Optional)' 85 | self.array = False 86 | self.typename = tds[1] 87 | self.description = tds[2] 88 | 89 | self.validVariableName = True 90 | name = self.name 91 | if name[-3:] == '[0]': 92 | self.array = True 93 | self.name = name[:-3] 94 | elif name.find('[0]') >= 0: 95 | self.validVariableName = False 96 | 97 | def getTypenameString(self): 98 | ret = typenameMap.get(self.typename, self.typename) 99 | if self.array: 100 | ret = '[%s] = []' % ret 101 | elif self.optional: 102 | ret += '? = nil' 103 | return ret 104 | 105 | def toParamString(self): 106 | return ('' if self.validVariableName else '//') + 'let %s: %s' % (self.name, self.getTypenameString()) 107 | 108 | def __repr__(self): 109 | return '(%s, %s, %s, %s)' % (self.name, str(self.optional), self.typename, self.description) 110 | 111 | 112 | class WebAPI(object): 113 | def __init__(self, hyphenName, method, path, scope, urlParams, formParams, queryParams, response): 114 | self.hyphenName = hyphenName 115 | self.method = method 116 | self.path = path 117 | self.scope = scope 118 | self.urlParams = urlParams 119 | self.formParams = formParams 120 | self.queryParams = queryParams 121 | self.response = response 122 | 123 | @property 124 | def className(self): 125 | def toCamelCase(name): 126 | return ''.join([e[0].upper() + e[1:] for e in name.split('-')]) 127 | return toCamelCase(self.hyphenName) 128 | 129 | @property 130 | def params(self): 131 | return [e for e in self.urlParams + self.formParams + self.queryParams] 132 | 133 | def createCtorExprString(self): 134 | return self.className + '(' + ', '.join([e.name + ': ' + defaultValueMap[e.typename] for e in self.params if e.validVariableName and not e.optional and not e.array]) + ')' 135 | 136 | def printTestCase(self): 137 | if self.response != None: 138 | print(''' 139 | func test%s() { 140 | createStub("%s") 141 | 142 | let expectation = self.expectation(description: "") 143 | TypetalkAPI.send(%s()) { result in 144 | switch result { 145 | case .success(let r): 146 | ''' % (self.className, self.hyphenName, self.className)) 147 | root = json.loads(self.response) 148 | printTestForJSONDictionary(root) 149 | 150 | 151 | print(''' 152 | expectation.fulfill() 153 | case .failure(let error): 154 | XCTFail("\(error)") 155 | } 156 | } 157 | 158 | waitForExpectations(timeout: 3) { (error) in 159 | XCTAssertNil(error, "\(error)") 160 | } 161 | } 162 | ''') 163 | 164 | def getRequestClassString(self): 165 | paramsString = '' 166 | if len(self.params): 167 | maxLen = max([len(e.toParamString()) for e in self.params]) 168 | formatString = '%-' + str(maxLen) + 's // %s\n' 169 | paramsString = ''.join(['\t' + formatString % (e.toParamString(), e.description) for e in self.params]) 170 | 171 | return """class %s: ClassInit, APIKitHelper, TypetalkRequest { // router:"%s,%s" 172 | %s} 173 | """ % (self.className, self.method, self.path, paramsString) 174 | 175 | def printResponseClass(self): 176 | if self.response != None: 177 | obj = json.loads(self.response) 178 | info = json2idl.guessTypenameForDict(obj) 179 | #print(info) 180 | json2idl.printClass(info, self.className + 'Response') 181 | 182 | def writeResponseJson(self): 183 | if self.response == None: 184 | return 185 | 186 | outputName = 'api_' + self.hyphenName + '.json' 187 | with file(outputName, 'w') as f: 188 | f.write(self.response) 189 | try: 190 | json.loads(self.response) 191 | except: 192 | print('WARNING:' + self.hyphenName + ' is not valid JSON.') 193 | 194 | 195 | def getWebAPI(apiPagePath): 196 | getParam = lambda e: [ParamInfo(e) for e in e('table > tbody > tr')] 197 | def composeUrl(urlBase, urlParams): 198 | return urlBase + ''.join(['\\(' + e[1:] + ')' for e in urlParams]) 199 | 200 | data = pq(url=SITE_ROOT + apiPagePath) 201 | 202 | method = None 203 | url = None 204 | scope = None 205 | urlParams = [] 206 | queryParams = [] 207 | formParams = [] 208 | response = None 209 | 210 | hs = data('div.content__wrapper > h3') 211 | for q in range(len(hs)): 212 | e = hs.eq(q).next() 213 | f = hs.eq(q).text() 214 | if f == 'Method': 215 | method = e.text() 216 | elif f == 'URL': 217 | cs = e.contents() 218 | urlBase = cs[0] 219 | #print(cs, urlBase, e.text()) 220 | #urlParams = [e.text for e in cs[1:]] 221 | #url = composeUrl(urlBase, urlParams) 222 | 223 | url = e.text() 224 | elif f == 'Scope': 225 | scope = e.text() 226 | elif f == 'URL parameters': 227 | urlParams = getParam(e) 228 | elif f == 'Query parameters': 229 | queryParams = getParam(e) 230 | elif f == 'Form parameters': 231 | formParams = getParam(e) 232 | elif f == 'Response Example': 233 | response = e.text() if e.text() != '' else '{}' 234 | else: 235 | print(f) 236 | raise 'Error' 237 | 238 | hyphenName = apiPagePath.split('/')[-1] 239 | path = url.replace('https://typetalk.in/api/v1/', '') # FIXME: typetalk 240 | return WebAPI(hyphenName, method, path, scope, urlParams, formParams, queryParams, response) 241 | 242 | 243 | 244 | top = pq(url=API_ROOT) 245 | rrr = 999 246 | for i in top('a.sidebar__links'): 247 | path = i.get('href') 248 | if path.find(DOC_BASE_PATH) != 0: continue 249 | if path == '/docs/typetalk/api/1/streaming': continue # FIXME 250 | 251 | api = getWebAPI(path) 252 | #print("----- " + api.hyphenName) 253 | #print(api.getRequestClassString()) 254 | api.printTestCase() 255 | #api.printResponseClass() 256 | #api.writeResponseJson() 257 | 258 | rrr -= 1 259 | if rrr < 0: 260 | sys.exit(0) # DELETEME 261 | -------------------------------------------------------------------------------- /protocols/APIKitRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIKitRequest.swift 3 | // Swift-idl 4 | // 5 | // Created by Safx Developer on 2015/05/12. 6 | // Copyright (c) 2015年 Safx Developers. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import APIKit 11 | 12 | public protocol APIKitRequest: Request { 13 | associatedtype APIKitResponse 14 | } 15 | 16 | 17 | /* 18 | extension APIKitRequest { 19 | public var baseURL: NSURL { return NSURL(string: "https://api.example.com/")! } 20 | } 21 | */ 22 | 23 | public struct NullDataParser: DataParser { 24 | public var contentType: String? = "application/json" 25 | public func parse(data: Data) throws -> Any { 26 | return data 27 | } 28 | } 29 | 30 | extension APIKitRequest where Self.Response: Decodable { 31 | public typealias DataParser = NullDataParser 32 | 33 | public var dataParser: DataParser { 34 | return NullDataParser() 35 | } 36 | 37 | public func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Self.Response { 38 | let decoder = JSONDecoder() 39 | if #available(OSX 10.12, iOS 10.0, *) { 40 | decoder.dateDecodingStrategy = .iso8601 41 | } else { 42 | fatalError("Please use newer macOS") 43 | } 44 | guard let data = object as? Data else { 45 | throw ResponseError.unexpectedObject(object) 46 | } 47 | do { 48 | return try decoder.decode(Self.Response.self, from: data) 49 | } catch DecodingError.typeMismatch(let dataType, let context) { 50 | print("JSON Decoding Error: typeMismatch") 51 | print(" base:", Self.Response.self) 52 | print(" desc:", context.debugDescription) 53 | print(" type:", dataType) 54 | print(" ctx :", context.codingPath.map{$0.stringValue}) 55 | print(" data:", String(data: data, encoding: String.Encoding.utf8) ?? "") 56 | throw ResponseError.unexpectedObject((context, dataType)) 57 | } catch DecodingError.valueNotFound(let dataType, let context) { 58 | print("JSON Decoding Error: valueNotFound") 59 | print(" base:", Self.Response.self) 60 | print(" type:", dataType) 61 | print(" desc:", context.debugDescription) 62 | print(" ctx :", context.codingPath.map{$0.stringValue}) 63 | print(" data:", String(data: data, encoding: String.Encoding.utf8) ?? "") 64 | throw ResponseError.unexpectedObject((context, dataType)) 65 | } catch DecodingError.keyNotFound(let codingPath, let context) { 66 | print("JSON Decoding Error: keyNotFound") 67 | print(" base:", Self.Response.self) 68 | print(" type:", codingPath) 69 | print(" desc:", context.debugDescription) 70 | print(" ctx :", context.codingPath.map{$0.stringValue}) 71 | print(" data:", String(data: data, encoding: String.Encoding.utf8) ?? "") 72 | throw ResponseError.unexpectedObject((context, codingPath)) 73 | } catch DecodingError.dataCorrupted(let context) { 74 | print("JSON Decoding Error: dataCorrupted") 75 | print(" base:", Self.Response.self) 76 | print(" desc:", context.debugDescription) 77 | print(" ctx :", context.codingPath.map{$0.stringValue}) 78 | print(" data:", String(data: data, encoding: String.Encoding.utf8) ?? "") 79 | throw ResponseError.unexpectedObject(context) 80 | } catch let error { 81 | print("JSON Decoding Error:", error) 82 | print(" base:", Self.Response.self) 83 | print(" data:", String(data: data, encoding: String.Encoding.utf8) ?? "") 84 | throw ResponseError.unexpectedObject(error) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /protocols/IDLProtocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Protocols.swift 3 | // Swift-idl 4 | // 5 | // Created by Safx Developer on 2015/05/12. 6 | // Copyright (c) 2015年 Safx Developers. All rights reserved. 7 | // 8 | 9 | protocol ClassInit {} 10 | protocol EnumStaticInit {} 11 | protocol Equatable {} 12 | 13 | protocol Printable {} 14 | 15 | protocol Encodable {} // Swift 4 16 | protocol Decodable {} // Swift 4 17 | 18 | protocol URLRequestHelper {} 19 | protocol APIKitHelper {} 20 | protocol WSHelper {} 21 | 22 | protocol ErrorType {} 23 | 24 | protocol Lensy {} 25 | -------------------------------------------------------------------------------- /protocols/JSON.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSON.swift 3 | // Swift-idl 4 | // 5 | // Created by Safx Developer on 2018/04/24. 6 | // Copyright (c) 2018 Safx Developers. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct AnyCodingKeys: CodingKey { 13 | var stringValue: String 14 | 15 | init?(stringValue: String) { 16 | self.stringValue = stringValue 17 | } 18 | 19 | var intValue: Int? 20 | 21 | init?(intValue: Int) { 22 | self.init(stringValue: "\(intValue)") 23 | self.intValue = intValue 24 | } 25 | } 26 | 27 | 28 | public enum JSON { 29 | case null 30 | case boolean(Bool) 31 | case number(Double) 32 | case string(String) 33 | indirect case array([JSON]) 34 | indirect case object([String:JSON]) 35 | } 36 | 37 | 38 | extension JSON: Decodable { 39 | public init(from decoder: Decoder) throws { 40 | if let container = try? decoder.container(keyedBy: AnyCodingKeys.self) { 41 | self = JSON(from: container) 42 | } else if let container = try? decoder.unkeyedContainer() { 43 | self = JSON(from: container) 44 | } else { 45 | throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "malformed JSON")) 46 | } 47 | } 48 | 49 | private init(from container: KeyedDecodingContainer) { 50 | var d: [String: JSON] = [:] 51 | for key in container.allKeys { 52 | if let value = try? container.decode(Bool.self, forKey: key) { 53 | d[key.stringValue] = .boolean(value) 54 | } else if let value = try? container.decode(Double.self, forKey: key) { 55 | d[key.stringValue] = .number(value) 56 | } else if let value = try? container.decode(String.self, forKey: key) { 57 | d[key.stringValue] = .string(value) 58 | } else if let value = try? container.nestedContainer(keyedBy: AnyCodingKeys.self, forKey: key) { 59 | d[key.stringValue] = JSON(from: value) 60 | } else if let value = try? container.nestedUnkeyedContainer(forKey: key) { 61 | d[key.stringValue] = JSON(from: value) 62 | } 63 | } 64 | self = .object(d) 65 | } 66 | 67 | private init(from container: UnkeyedDecodingContainer) { 68 | var container = container 69 | var a: [JSON] = [] 70 | while !container.isAtEnd { 71 | if let value = try? container.decode(Bool.self) { 72 | a.append(.boolean(value)) 73 | } else if let value = try? container.decode(Double.self) { 74 | a.append(.number(value)) 75 | } else if let value = try? container.decode(String.self) { 76 | a.append(.string(value)) 77 | } else if let value = try? container.nestedContainer(keyedBy: AnyCodingKeys.self) { 78 | a.append(JSON(from: value)) 79 | } else if let value = try? container.nestedUnkeyedContainer() { 80 | a.append(JSON(from: value)) 81 | } 82 | } 83 | self = .array(a) 84 | } 85 | } 86 | 87 | 88 | extension JSON: Encodable { 89 | 90 | public func encode(to encoder: Encoder) throws { 91 | var container = encoder.singleValueContainer() 92 | switch self { 93 | case let .array(array): 94 | try container.encode(array) 95 | case let .object(object): 96 | try container.encode(object) 97 | case let .string(string): 98 | try container.encode(string) 99 | case let .number(number): 100 | try container.encode(number) 101 | case let .boolean(bool): 102 | try container.encode(bool) 103 | case .null: 104 | try container.encodeNil() 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /protocols/Lensy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Lensy.swift 3 | // Lensy 4 | // 5 | // Created by Safx Developer on 2016/02/12. 6 | // Copyright © 2016 Safx Developers. All rights reserved. 7 | // 8 | 9 | 10 | // MARK: - Lenses API 11 | 12 | public protocol LensType { 13 | associatedtype Whole 14 | associatedtype Part 15 | var get: Whole -> LensResult { get } 16 | var set: (Whole, Part) -> LensResult { get } 17 | } 18 | 19 | public struct Lens: LensType { 20 | public let get: Whole -> LensResult 21 | public let set: (Whole, Part) -> LensResult 22 | } 23 | 24 | public struct OptionalUnwrapLens: LensType { 25 | public typealias Whole = Element? 26 | public typealias Part = Element 27 | public let get: Whole -> LensResult 28 | public let set: (Whole, Part) -> LensResult 29 | } 30 | 31 | public struct ArrayIndexLens: LensType { 32 | public typealias Whole = [Element] 33 | public typealias Part = Element 34 | public let get: Whole -> LensResult 35 | public let set: (Whole, Part) -> LensResult 36 | } 37 | 38 | public extension LensType { 39 | public func compose(other: L) -> Lens { 40 | return Lens( 41 | get: { (object: Whole) -> LensResult in 42 | return self.get(object) 43 | .then(other.get) 44 | }, 45 | set: { (object: Whole, newValue: Subpart) -> LensResult in 46 | return self.get(object) 47 | .then { other.set($0, newValue) } 48 | .then { self.set(object, $0) } 49 | } 50 | ) 51 | } 52 | 53 | public func modify(object: Whole, @noescape _ closure: Part -> Part) -> LensResult { 54 | return get(object) 55 | .then { self.set(object, closure($0)) } 56 | } 57 | 58 | public func tryGet(object: Whole) throws -> Part { 59 | switch get(object) { 60 | case .OK(let v): 61 | return v 62 | case .Error(let e): 63 | throw e 64 | } 65 | } 66 | 67 | public func trySet(object: Whole, _ newValue: Part) throws -> Whole { 68 | switch set(object, newValue) { 69 | case .OK(let v): 70 | return v 71 | case .Error(let e): 72 | throw e 73 | } 74 | } 75 | } 76 | 77 | extension Lens { 78 | public init(g: Whole -> Part, s: (Whole, Part) -> Whole) { 79 | get = { .OK(g($0)) } 80 | set = { .OK(s($0, $1)) } 81 | } 82 | } 83 | 84 | extension OptionalUnwrapLens { 85 | public init() { 86 | get = { optional in 87 | guard let v = optional else { 88 | return .Error(LensErrorType.OptionalNone) 89 | } 90 | return .OK(v) 91 | } 92 | set = { optional, newValue in 93 | guard var v = optional else { 94 | return .Error(LensErrorType.OptionalNone) 95 | } 96 | v = newValue 97 | return .OK(v) 98 | } 99 | } 100 | } 101 | 102 | extension ArrayIndexLens { 103 | public init(at idx: Int) { 104 | get = { array in 105 | guard 0..() -> Lens { 125 | return Lens( 126 | g: { $0 }, 127 | s: { $1 } 128 | ) 129 | } 130 | 131 | // MARK: - Result 132 | 133 | public enum LensResult { 134 | case OK(Element) 135 | case Error(LensErrorType) 136 | } 137 | 138 | extension LensResult { 139 | func then(@noescape closure: Element -> LensResult) -> LensResult { 140 | switch self { 141 | case .OK(let v): 142 | return closure(v) 143 | case .Error(let e): 144 | return .Error(e) 145 | } 146 | } 147 | } 148 | 149 | // MARK: - Error 150 | 151 | public enum LensErrorType: ErrorType { 152 | case OptionalNone 153 | case ArrayIndexOutOfBounds 154 | } 155 | 156 | 157 | // MARK: - Lens Helper 158 | 159 | public protocol LensHelperType { 160 | associatedtype Whole 161 | associatedtype Part 162 | 163 | init(lens: Lens) 164 | var lens: Lens { get } 165 | } 166 | 167 | public protocol HasSubLensHelper { 168 | associatedtype SubLensHelper 169 | } 170 | 171 | public struct LensHelper: LensHelperType { 172 | public let lens: Lens 173 | public init(lens: Lens) { 174 | self.lens = lens 175 | } 176 | } 177 | 178 | public struct ArrayLensHelper: LensHelperType, HasSubLensHelper { 179 | public typealias Part = [Element] 180 | public typealias SubLensHelper = Sub 181 | public let lens: Lens 182 | public init(lens: Lens) { 183 | self.lens = lens 184 | } 185 | } 186 | 187 | public struct OptionalLensHelper: LensHelperType, HasSubLensHelper { 188 | public typealias Part = Element? 189 | public typealias SubLensHelper = Sub 190 | public let lens: Lens 191 | public init(lens: Lens) { 192 | self.lens = lens 193 | } 194 | } 195 | 196 | extension LensHelperType { 197 | public init(parent: Parent, lens: Lens) { 198 | self.init(lens: parent.lens.compose(lens)) 199 | } 200 | 201 | public func get(object: Whole) -> LensResult { 202 | return lens.get(object) 203 | } 204 | 205 | public func set(object: Whole, _ newValue: Part) -> LensResult { 206 | return lens.set(object, newValue) 207 | } 208 | 209 | public func modify(object: Whole, @noescape closure: Part -> Part) -> LensResult { 210 | return lens.modify(object, closure) 211 | } 212 | } 213 | 214 | extension ArrayLensHelper where Sub: LensHelperType, Sub.Whole == Whole, Sub.Part == Element { 215 | public subscript(idx: Int) -> SubLensHelper { 216 | return SubLensHelper(lens: lens.compose(ArrayIndexLens(at: idx))) 217 | } 218 | 219 | public func modifyMap(object: Whole, @noescape closure: Element -> Element) -> LensResult { 220 | return lens.modify(object) { $0.map(closure) } 221 | } 222 | } 223 | 224 | extension OptionalLensHelper where Sub: LensHelperType, Sub.Whole == Whole, Sub.Part == Element { 225 | public var unwrap: SubLensHelper { 226 | return SubLensHelper(lens: lens.compose(OptionalUnwrapLens())) 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /swift_idl-test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import swift_idl as IDL 3 | 4 | test_structure = { 5 | "key.kind" : "source.lang.swift.decl.struct", 6 | "key.offset" : 19, 7 | "key.nameoffset" : 26, 8 | "key.namelength" : 3, 9 | "key.inheritedtypes" : [ 10 | { 11 | "key.name" : "JSONDecodable" 12 | } 13 | ], 14 | "key.bodylength" : 110, 15 | "key.accessibility" : "source.lang.swift.accessibility.internal", 16 | "key.substructure" : [ 17 | { 18 | "key.kind" : "source.lang.swift.decl.var.instance", 19 | "key.offset" : 72, 20 | "key.attributes" : [ 21 | { 22 | "key.attribute" : "source.decl.attribute.__raw_doc_comment" 23 | } 24 | ], 25 | "key.nameoffset" : 76, 26 | "key.namelength" : 2, 27 | "key.length" : 15, 28 | "key.accessibility" : "source.lang.swift.accessibility.internal", 29 | "key.substructure" : [ 30 | 31 | ], 32 | "key.typename" : "Int", 33 | "key.name" : "id" 34 | }, 35 | { 36 | "key.kind" : "source.lang.swift.decl.var.instance", 37 | "key.offset" : 92, 38 | "key.nameoffset" : 96, 39 | "key.namelength" : 5, 40 | "key.length" : 17, 41 | "key.accessibility" : "source.lang.swift.accessibility.internal", 42 | "key.substructure" : [ 43 | 44 | ], 45 | "key.typename" : "String", 46 | "key.name" : "query" 47 | }, 48 | { 49 | "key.kind" : "source.lang.swift.decl.var.instance", 50 | "key.offset" : 126, 51 | "key.attributes" : [ 52 | { 53 | "key.attribute" : "source.decl.attribute.__raw_doc_comment" 54 | } 55 | ], 56 | "key.nameoffset" : 130, 57 | "key.namelength" : 1, 58 | "key.length" : 13, 59 | "key.accessibility" : "source.lang.swift.accessibility.internal", 60 | "key.substructure" : [ 61 | 62 | ], 63 | "key.typename" : "String", 64 | "key.name" : "z" 65 | } 66 | ], 67 | "key.name" : "Foo", 68 | "key.length" : 138, 69 | "key.bodyoffset" : 46 70 | } 71 | 72 | test_syntax = [ 73 | { "offset" : 0, "length" : 6, "type" : "source.lang.swift.syntaxtype.keyword" }, 74 | { "offset" : 7, "length" : 10, "type" : "source.lang.swift.syntaxtype.identifier" }, 75 | { "offset" : 19, "length" : 6, "type" : "source.lang.swift.syntaxtype.keyword" }, 76 | { "offset" : 26, "length" : 3, "type" : "source.lang.swift.syntaxtype.identifier" }, 77 | { "offset" : 31, "length" : 13, "type" : "source.lang.swift.syntaxtype.typeidentifier" }, 78 | { "offset" : 47, "length" : 21, "type" : "source.lang.swift.syntaxtype.comment" }, 79 | { "offset" : 72, "length" : 3, "type" : "source.lang.swift.syntaxtype.keyword" }, 80 | { "offset" : 76, "length" : 2, "type" : "source.lang.swift.syntaxtype.identifier" }, 81 | { "offset" : 80, "length" : 3, "type" : "source.lang.swift.syntaxtype.typeidentifier" }, 82 | { "offset" : 86, "length" : 1, "type" : "source.lang.swift.syntaxtype.number" }, 83 | { "offset" : 92, "length" : 3, "type" : "source.lang.swift.syntaxtype.keyword" }, 84 | { "offset" : 96, "length" : 5, "type" : "source.lang.swift.syntaxtype.identifier" }, 85 | { "offset" : 103, "length" : 6, "type" : "source.lang.swift.syntaxtype.typeidentifier" }, 86 | { "offset" : 110, "length" : 12, "type" : "source.lang.swift.syntaxtype.comment" }, 87 | { "offset" : 126, "length" : 3, "type" : "source.lang.swift.syntaxtype.keyword" }, 88 | { "offset" : 130, "length" : 1, "type" : "source.lang.swift.syntaxtype.identifier" }, 89 | { "offset" : 133, "length" : 6, "type" : "source.lang.swift.syntaxtype.typeidentifier" }, 90 | { "offset" : 144, "length" : 12, "type" : "source.lang.swift.syntaxtype.comment" } 91 | ] 92 | 93 | test_source = '''import Foundation 94 | 95 | struct Foo: JSONDecodable { // sample:"foo,,bar" 96 | let id: Int = 3 97 | let query: String // json:"q" 98 | let z: String // json:"-" 99 | } 100 | ''' 101 | 102 | 103 | class SampleStructTest(unittest.TestCase): 104 | def test_getSwiftTokens(self): 105 | tk = IDL.getSwiftTokens(test_syntax, test_source) 106 | self.assertEqual('import', tk[0].content) 107 | self.assertEqual(1, tk[0].line) 108 | self.assertEqual('source.lang.swift.syntaxtype.keyword', tk[0].tokenType) 109 | 110 | self.assertEqual('}\n', tk[-1].content) 111 | self.assertEqual(7, tk[-1].line) 112 | self.assertEqual('omittedtoken', tk[-1].tokenType) 113 | 114 | if __name__ == '__main__': 115 | unittest.main() 116 | -------------------------------------------------------------------------------- /swift_idl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from __future__ import print_function 4 | 5 | import argparse 6 | import functools 7 | import itertools 8 | import json 9 | import os 10 | import re 11 | import subprocess 12 | from mako.template import Template 13 | from mako import exceptions 14 | 15 | PROGRAM_NAME='swift-idl' 16 | SOURCEKITTEN='sourceKitten' 17 | 18 | 19 | ### Tokens 20 | 21 | class SwiftToken(): 22 | def __init__(self, line, offset, tokenType, content): 23 | self.line = line 24 | self.offset = offset 25 | self.tokenType = tokenType 26 | self.content = content 27 | 28 | def __repr__(self): 29 | return self.content + ' ' + self.tokenType 30 | 31 | @property 32 | def isComment(self): 33 | return self.tokenType == 'source.lang.swift.syntaxtype.comment' 34 | 35 | @property 36 | def annotations(self): 37 | cs = self.content.split('//') 38 | if len(cs) < 2: return {} 39 | # the first comment area is only checked 40 | return {g.group(1).lower():map(str.strip, g.group(2).split(',')) 41 | for g in re.finditer(r'\b(\w+):"([^"]+)"', cs[1]) 42 | if g.lastindex == 2} 43 | 44 | def getSwiftTokens(tokens, source): 45 | linenum_map = map(lambda x: 1 if x == '\n' else 0, source) # FIXME: check CRLF or LF 46 | def getLinenumber(offset): 47 | return sum(linenum_map[:offset], 1) 48 | 49 | def getOmittedTokens(tokens, source): 50 | begins = [0] + [t['offset'] + t['length'] for t in tokens] 51 | ends = [t['offset'] for t in tokens] + [len(source)] 52 | return [SwiftToken(getLinenumber(b), b, "omittedtoken", source[b:e]) 53 | for b,e in zip(begins, ends) 54 | if b != e] 55 | 56 | def conv(tk): 57 | offset = tk['offset'] 58 | length = tk['length'] 59 | tktype = tk['type'] 60 | return SwiftToken(getLinenumber(offset), offset, tktype, source[offset : offset + length]) 61 | 62 | merged = map(conv, tokens) + getOmittedTokens(tokens, source) 63 | return sorted(merged, key=lambda e: e.offset) 64 | 65 | def tokenrange(tokens, offset, length): 66 | start = None 67 | end = len(tokens) 68 | for i in range(end): 69 | t = tokens[i] 70 | if t.offset < offset: continue 71 | if not start: start = i 72 | if t.offset >= offset + length: 73 | end = i 74 | break 75 | return range(start, end) 76 | 77 | ### Swift Typename 78 | 79 | class SwiftTypealias(): 80 | def __init__(self, name, assignment): 81 | self._name = name 82 | self._assignment = assignment 83 | 84 | @property 85 | def name(self): return self._name 86 | 87 | @property 88 | def assignment(self): return self._assignment 89 | 90 | class SwiftTypename(): 91 | def __init__(self, typename): 92 | self._typename = typename 93 | 94 | def __repr__(self): 95 | return self._typename 96 | 97 | @property 98 | def baseTypename(self): return self._typename 99 | 100 | class SwiftArray(): 101 | def __init__(self, targetClass): 102 | self._targetClass = targetClass 103 | 104 | def __repr__(self): 105 | return '[' + str(self._targetClass) + ']' 106 | 107 | @property 108 | def baseTypename(self): 109 | return self._targetClass.baseTypename 110 | 111 | class SwiftOptional(): 112 | def __init__(self, targetClass): 113 | self._targetClass = targetClass 114 | 115 | def __repr__(self): return str(self._targetClass) + '?' 116 | 117 | @property 118 | def baseTypename(self): return self._targetClass.baseTypename 119 | 120 | def parseTypename(typename): 121 | if typename[-1] == '?': 122 | return SwiftOptional(parseTypename(typename[:-1])) 123 | elif typename[0] == '[' and typename[-1] == ']': 124 | return SwiftArray(parseTypename(typename[1:-1])) 125 | else: 126 | return SwiftTypename(typename) 127 | 128 | ### Swift structures base 129 | 130 | class SwiftVariableBase(object): 131 | def __init__(self, name, typename): 132 | self._name = name # maybe None 133 | self._typename = typename 134 | 135 | @property 136 | def name(self): return self._name 137 | 138 | @property 139 | def typename(self): return self._typename 140 | 141 | @property 142 | def isOptional(self): return self._typename[-1] == '?' # FIXME 143 | 144 | @property 145 | def isArray(self): return self._typename[0] == '[' # FIXME 146 | 147 | @property 148 | def isArrayOfOptional(self): return len(self._typename) > 3 and self._typename[-2] == ']?' # FIXME 149 | 150 | @property 151 | def baseTypename(self): 152 | return parseTypename(self._typename).baseTypename 153 | 154 | # variable of tuple in case 155 | class SwiftTupleVariable(SwiftVariableBase): 156 | def __init__(self, name, typename, position): 157 | super(SwiftTupleVariable, self).__init__(name, typename) 158 | self._positon = position 159 | 160 | @property 161 | def positon(self): return self._positon 162 | 163 | @property 164 | def varname(self): 165 | return self._name if self._name else 'v' + str(self._positon) 166 | 167 | @property 168 | def keyname(self): 169 | return self._name if self._name else str(self._positon) 170 | 171 | @property 172 | def declaredString(self): 173 | if self._name: 174 | return self._name + ': ' + self.typename 175 | return self.typename 176 | 177 | # variable in struct or class 178 | class SwiftVariable(SwiftVariableBase): 179 | def __init__(self, name, typename, defaultValue, accessibility, annotations, parsedDeclaration): 180 | super(SwiftVariable, self).__init__(name, typename) 181 | self._defaultValue = defaultValue 182 | self._accessibility = accessibility 183 | self._annotations = annotations 184 | self._parsedDeclaration = parsedDeclaration 185 | 186 | @property 187 | def accessibility(self): return self._accessibility 188 | 189 | @property 190 | def annotations(self): return self._annotations 191 | 192 | @property 193 | def hasDefaultValue(self): 194 | return self.isOptional or self._defaultValue != None 195 | 196 | @property 197 | def defaultValue(self): 198 | if self._defaultValue: 199 | return self._defaultValue 200 | elif self.isOptional: 201 | return 'nil' 202 | return self.typename + '()' 203 | 204 | @property 205 | def parsedDeclarationWithoutDefaultValue(self): 206 | # FIXME: use original code: self._parsedDeclaration 207 | anon = ' '.join([k + ':"' + ','.join(v) + '"' for k,v in self._annotations.iteritems()]).strip() 208 | if len(anon) > 0: anon = ' // ' + anon 209 | return 'public let ' + self._name + ': ' + self._typename + anon 210 | 211 | def annotation(self, name): 212 | return getAnnotationMap()[name](self) 213 | 214 | # tuple in case or variables in struct or class 215 | class SwiftVariableList(object): 216 | def __init__(self, name, variables, annotations): 217 | self._name = name 218 | self._variables = variables # SwiftVariable or SwiftTupleVariable 219 | self._annotations = annotations 220 | 221 | @property 222 | def name(self): return self._name 223 | 224 | @property 225 | def variables(self): return self._variables 226 | 227 | @property 228 | def annotations(self): return self._annotations 229 | 230 | def annotation(self, name): 231 | return getAnnotationMap()[name](self) 232 | 233 | class SwiftCase(SwiftVariableList): 234 | def __init__(self, name, variables, annotations, rawValue): 235 | super(SwiftCase, self).__init__(name, variables, annotations) 236 | self._rawValue = rawValue 237 | 238 | @property 239 | def letString(self): 240 | if len(self.variables) == 0: return '' 241 | return '(let (' + ', '.join([i.varname for i in self.variables]) + '))' 242 | 243 | @property 244 | def declaredString(self): 245 | if self._rawValue != None: 246 | return str(self._name) + ' = ' + str(self._rawValue) 247 | else: 248 | p = '' 249 | if len(self._variables) > 0: 250 | p = '(' + ', '.join([e.declaredString for e in self._variables]) + ')' 251 | return str(self._name) + p 252 | 253 | 254 | def getDeclarationString(clazzOrEnum, clazzOrEnumString, templateHeader, classes, protocols, isRootLevel): 255 | template = Template(templateHeader + ''' 256 | % for i in innerDecls: 257 | // 258 | ${i} 259 | % endfor 260 | 261 | % for s in subDecls: 262 | // 263 | ${s} 264 | % endfor 265 | } 266 | % for i in outerDecls: 267 | // 268 | ${i} 269 | // 270 | % endfor 271 | ''') 272 | def getattrif(me, name, defaultValue = None): 273 | if hasattr(me, name): 274 | return getattr(me, name) 275 | return defaultValue 276 | 277 | def callattrif(me, name, *p): 278 | if hasattr(me, name): 279 | return getattr(me, name)(*p) 280 | return None 281 | 282 | assert(clazzOrEnumString == 'Class' or clazzOrEnumString == 'Enum') 283 | 284 | ps = getIdlProtocols(protocols, clazzOrEnum.inheritedTypes, clazzOrEnumString + 'Default') 285 | for p in ps: 286 | callattrif(p, 'modify' + clazzOrEnumString, clazzOrEnum) 287 | 288 | subTemplateParams = { 289 | 'classes': classes, 290 | 'clazz' if clazzOrEnumString == 'Class' else 'enum': clazzOrEnum 291 | } 292 | 293 | isRawStyle = getattrif(clazzOrEnum, 'isRawStyle', False) # FIXME 294 | 295 | templates = [callattrif(p, clazzOrEnumString.lower() + 'Templates', isRawStyle) for p in ps] 296 | tupledTemplates = [e if type(e) == tuple else (e, None) for e in templates if e] 297 | innerTemplates, outerTemplates = zip(*tupledTemplates) if len(tupledTemplates) > 0 else ([], []) # unzip 298 | 299 | try: 300 | innerDecls = [indent(Template(e).render(**subTemplateParams)) for e in innerTemplates] 301 | outerDecls = [Template(e).render(**subTemplateParams) for e in outerTemplates if e] 302 | except: 303 | print(exceptions.html_error_template().render(css=False, full=False)) 304 | exit(1) 305 | 306 | typeInheritances = sum([getattrif(p, 'protocol' + clazzOrEnumString, []) for p in ps], getNonIdlProtocols(protocols, clazzOrEnum.inheritedTypes)) 307 | templateParams = { 308 | 'clazz' if clazzOrEnumString == 'Class' else 'enum': clazzOrEnum, 309 | 'innerDecls': innerDecls, 310 | 'outerDecls': outerDecls, 311 | 'inheritances': ': ' + ', '.join(typeInheritances) if len(typeInheritances) > 0 else '', 312 | 'subDecls': map(lambda e: e.getDeclarationString(classes, protocols, False), clazzOrEnum.substructure) 313 | } 314 | output = template.render(**templateParams) 315 | return indent(output, isRootLevel) 316 | 317 | class SwiftEnum(object): 318 | def __init__(self, name, cases, inheritedTypes, annotations, substructure): 319 | self._name = name 320 | self._cases = cases 321 | self._inheritedTypes = inheritedTypes 322 | self._annotations = annotations 323 | self._substructure = substructure 324 | 325 | @property 326 | def name(self): return self._name 327 | 328 | @property 329 | def cases(self): return self._cases 330 | 331 | @property 332 | def inheritedTypes(self): return self._inheritedTypes 333 | 334 | @property 335 | def annotations(self): return self._annotations 336 | 337 | @property 338 | def substructure(self): return self._substructure 339 | 340 | @property 341 | def isEnum(self): return True 342 | 343 | @property 344 | def isRawStyle(self): 345 | if len(self._inheritedTypes) > 0: 346 | n = self._inheritedTypes[0] 347 | return n in ['String', 'Int', 'Float', 'Character'] # FIXME: naive guess 348 | return False 349 | 350 | #@property 351 | #def rawType(self): 352 | # return self.isRawStyle if self._inheritedTypes[0] else None 353 | 354 | def annotation(self, name): 355 | return getAnnotationMap()[name](self) 356 | 357 | def getDeclarationString(self, classes, protocols, isRootLevel = True): 358 | header = ''' 359 | public enum ${enum.name}${inheritances} { 360 | % for c in enum.cases: 361 | case ${c.declaredString} 362 | % endfor 363 | ''' 364 | return getDeclarationString(self, 'Enum', header, classes, protocols, isRootLevel) 365 | 366 | 367 | class SwiftClass(SwiftVariableList): 368 | def __init__(self, name, decltype, variables, inheritedTypes, typealiases, annotations, substructure): 369 | super(SwiftClass, self).__init__(name, variables, annotations) 370 | self._decltype = decltype 371 | self._inheritedTypes = inheritedTypes 372 | self._typealiases = typealiases 373 | self._substructure = substructure 374 | 375 | @property 376 | def name(self): return self._name 377 | 378 | @property 379 | def inheritedTypes(self): return self._inheritedTypes 380 | 381 | @property 382 | def decltype(self): return self._decltype 383 | 384 | @property 385 | def typealiases(self): return self._typealiases 386 | 387 | @property 388 | def substructure(self): return self._substructure 389 | 390 | @property 391 | def isEnum(self): return False 392 | 393 | @property 394 | def static(self): return 'static' if self._decltype == 'struct' else 'class' 395 | 396 | def getDeclarationString(self, classes, protocols, isRootLevel = True): 397 | header = ''' 398 | public ${clazz.decltype} ${clazz.name}${inheritances} { 399 | % for a in clazz.typealiases: 400 | public typealias ${a.name} = ${a.assignment} 401 | % endfor 402 | % for v in clazz.variables: 403 | ${v.parsedDeclarationWithoutDefaultValue} 404 | % endfor 405 | ''' 406 | return getDeclarationString(self, 'Class', header, classes, protocols, isRootLevel) 407 | 408 | 409 | ### Parsing functions 410 | 411 | def visitProtocol(node): 412 | name = node['key.name'] 413 | clazz = globals()[name] 414 | inheritedtypes = map(lambda e: e['key.name'], node.get('key.inheritedtypes', [])) 415 | return SwiftProtocol(name, inheritedtypes, clazz) 416 | 417 | def visitClass(node, tokens): 418 | # `sourcekitten doc` doesn't return `typealias` information. So we have to process. 419 | # * You must to declare typealias at the beginning of body in class. 420 | def getTypealiases(): 421 | aliases = [] 422 | def addTypealias(begin, end): 423 | if begin != None: 424 | aliases.append(visitTypealias([tokens[x] for x in range(begin, end)])) 425 | 426 | tkrange = tokenrange(tokens, node['key.bodyoffset'], node['key.bodylength']) 427 | begin = None 428 | for i in tkrange: 429 | t = tokens[i] 430 | if t.tokenType == 'source.lang.swift.syntaxtype.attribute.builtin': 431 | addTypealias(begin, i) 432 | begin = None 433 | if t.tokenType == 'source.lang.swift.syntaxtype.keyword': 434 | if t.content == 'typealias': 435 | addTypealias(begin, i) 436 | begin = i 437 | else: 438 | addTypealias(begin, i) 439 | break 440 | elif i == tkrange[-1]: 441 | addTypealias(begin, i + 1) 442 | 443 | return aliases 444 | 445 | def getVariables(a, n): 446 | if n.get('key.kind', None) == 'source.lang.swift.decl.var.instance': 447 | return a + [visitVariable(n, tokens)] 448 | return a 449 | 450 | name = node['key.name'] 451 | decltype = 'struct' if node['key.kind'] == 'source.lang.swift.decl.struct' else 'class' 452 | 453 | variables = reduce(getVariables, node.get('key.substructure', []), []) 454 | inheritedTypes = map(lambda e: e['key.name'], node.get('key.inheritedtypes', [])) 455 | 456 | annotations = getAnnotations([tokens[i] for i in tokenrange(tokens, node['key.bodyoffset'], node['key.bodylength'])]) 457 | typealiases = getTypealiases() 458 | 459 | subs = node.get('key.substructure', []) 460 | innerDecls = visitSubstructure(getDeclarations, tokens, subs, []) 461 | 462 | return SwiftClass(name, decltype, variables, inheritedTypes, typealiases, annotations, innerDecls) 463 | 464 | def visitEnum(node, tokens): 465 | name = node['key.name'] 466 | inheritedTypes = map(lambda e: e['key.name'], node.get('key.inheritedtypes', [])) 467 | 468 | subs = node.get('key.substructure', []) 469 | pred = lambda e: e['key.kind'] == 'source.lang.swift.decl.enumcase' 470 | cases = map(functools.partial(visitCase, tokens), filter(pred, subs)) 471 | innerDecls = visitSubstructure(getDeclarations, tokens, itertools.ifilterfalse(pred, subs), []) 472 | 473 | annotations = {} # FIXME 474 | return SwiftEnum(name, cases, inheritedTypes, annotations, innerDecls) 475 | 476 | def visitVariable(node, tokens): 477 | offset = node['key.offset'] 478 | length = node['key.length'] 479 | 480 | def getContent(t): 481 | if t.tokenType == 'omittedtoken': 482 | return t.content.split('\n')[0].split('}')[0] 483 | return t.content 484 | 485 | def getDefaultValue(var_tokens): 486 | eqs = [e[0] for e in enumerate(var_tokens) if e[1].tokenType == 'omittedtoken' and e[1].content.find('=') >= 0] 487 | assert(len(eqs) <= 1) 488 | if len(eqs) == 1: 489 | p = eqs[0] 490 | before = var_tokens[p].content.split('=')[1].split('\n')[0].split('}')[0].strip() 491 | assign_tokens = [i for i in var_tokens[p+1:] if i.tokenType != 'source.lang.swift.syntaxtype.comment'] 492 | value = before + ''.join([getContent(e) for e in assign_tokens]).strip() 493 | return value if len(value) > 0 else None 494 | return None 495 | 496 | name = node['key.name'] 497 | typename = node['key.typename'] 498 | decl_tokens = getTokenForDecl(tokens, offset, length) 499 | defaultValue = getDefaultValue(decl_tokens) 500 | accessibility = None # FIXME 501 | parsedDecl = ''.join([getContent(i) for i in decl_tokens]).strip() # FIXME: unused 502 | annotations = getAnnotations(decl_tokens) 503 | return SwiftVariable(name, typename, defaultValue, accessibility, annotations, parsedDecl) 504 | 505 | def visitCase(tokens, enumcase): 506 | # `sourcekitten doc` doesn't return `case` information. So we have to process. 507 | # * You hove to declare `case`s at the beginning of body of enum when you'll contain sub enums, due to parsing limitation. 508 | # * You cannot include tuple in case. 509 | offset = enumcase['key.offset'] 510 | length = enumcase['key.length'] 511 | caseTokens = getTokenForDecl(tokens, offset, length) 512 | 513 | assert(caseTokens[0].content == 'case' and caseTokens[0].tokenType == 'source.lang.swift.syntaxtype.keyword') 514 | assert(caseTokens[1].tokenType == 'omittedtoken') 515 | 516 | assocVals = [] 517 | def addAssociateValue(valuePair, token): 518 | value, typename = tuple_pair 519 | if token.content.find('?') >= 0: 520 | typename += '?' 521 | if token.content.find(']') >= 0: 522 | typename = '[' + typename + ']' 523 | assocVals.append(SwiftTupleVariable(tuple_pair[0], typename, len(assocVals))) 524 | 525 | label = None 526 | value = None 527 | tuple_pair = None 528 | for t in caseTokens[2:]: 529 | if t.tokenType == 'source.lang.swift.syntaxtype.identifier': 530 | if label == None: 531 | label = t.content 532 | else: 533 | if tuple_pair == None: 534 | tuple_pair = (None, t.content) 535 | else: 536 | tuple_pair = (tuple_pair[1], t.content) 537 | elif t.tokenType == 'omittedtoken': 538 | # FIXME: This code would parse incorrectly in some cases. 539 | if t.content.find(',') >= 0 or t.content.find(')') >= 0: 540 | addAssociateValue(tuple_pair, t) 541 | tuple_pair = None 542 | elif t.isComment: 543 | pass 544 | else: 545 | assert(value == None) 546 | value = t.content # FIXME: check tokenType 547 | 548 | annotations = getAnnotations(caseTokens) 549 | return SwiftCase(label, assocVals, annotations, value) 550 | 551 | def visitTypealias(tokens): 552 | assert(tokens[0].content == 'typealias' and tokens[0].tokenType == 'source.lang.swift.syntaxtype.keyword') 553 | assert(tokens[1].tokenType == 'omittedtoken') 554 | 555 | label = None 556 | typeident_base = '' 557 | before = '' 558 | for t in tokens[2:]: 559 | if t.tokenType == 'source.lang.swift.syntaxtype.identifier': 560 | assert(label == None) 561 | label = t.content 562 | elif t.tokenType == 'source.lang.swift.syntaxtype.typeidentifier': 563 | typeident_base += t.content 564 | elif t.tokenType == 'omittedtoken': 565 | # FIXME: This code would parse incorrectly in some cases. 566 | if typeident_base != '': 567 | typeident_base += re.sub(r'[\n{].+', '', t.content) 568 | elif label != None: 569 | before = re.sub(r'.+=\s+', '', t.content) 570 | 571 | typeident = before + typeident_base 572 | return SwiftTypealias(label, typeident) 573 | 574 | def getTokenForDecl(tokens, offset, length): 575 | def getRangeForDecl(tokens, offset, length): 576 | tkrange = tokenrange(tokens, offset, length) 577 | start = tkrange[0] 578 | end = tkrange[-1] + 1 579 | 580 | # include other elements of last line 581 | last_linenum = tokens[end - 1].line 582 | for pos in range(end, len(tokens)): 583 | if tokens[pos].line != last_linenum: 584 | return range(start, pos) 585 | 586 | return range(start, end) 587 | 588 | return [tokens[i] for i in getRangeForDecl(tokens, offset, length)] 589 | 590 | def getAnnotations(tokens): 591 | annons = [t.annotations for t in tokens if t.isComment] + [{}] 592 | return annons[0] 593 | 594 | def getTokenList(filepath): 595 | with file(filepath) as f: 596 | source = f.read() 597 | syntax = sourcekittenSyntax(filepath) 598 | return getSwiftTokens(syntax, source) 599 | 600 | def processProject(func, structure): 601 | def visit(filepath, contents): 602 | sublist = contents.get('key.substructure', []) 603 | tokens = getTokenList(filepath) 604 | 605 | tmp_list = [] 606 | for node in sublist: 607 | tmp_list = func(tmp_list, node, tokens) 608 | return tmp_list 609 | 610 | assert(all([len(i) == 1 for i in structure])) 611 | return {dic.keys()[0]:visit(*dic.items()[0]) for dic in structure} 612 | 613 | def visitSubstructure(func, tokens, sublist, initial_list): 614 | tmp_list = initial_list 615 | for node in sublist: 616 | tmp_list = func(tmp_list, node, tokens) 617 | return tmp_list 618 | 619 | 620 | ### Annotation classes 621 | 622 | class JSONAnnotation: 623 | def __init__(self, var): 624 | anon_dic = var.annotations 625 | self.isOmitValue = False 626 | self.jsonLabel = var.name 627 | 628 | annons = anon_dic.get('json', ['']) 629 | 630 | if len(annons) == 0: 631 | return 632 | 633 | name = annons[0] 634 | if name != '': 635 | if name == '-': 636 | self.isOmitValue = True 637 | else: 638 | self.jsonLabel = name 639 | 640 | for i in annons[1:]: 641 | if i == 'omitempty': 642 | pass 643 | elif i == 'string': 644 | pass 645 | else: 646 | raise RuntimeError('Unknown annotation: ' + i) 647 | 648 | 649 | class RouterAnnotation: 650 | def __init__(self, case_or_class): 651 | self._variables = case_or_class.variables 652 | self.method = 'GET' 653 | self.path = case_or_class.name 654 | 655 | annons = case_or_class.annotations.get('router', ['']) 656 | 657 | if len(annons) > 0: 658 | method = annons[0].upper() 659 | if method != '': 660 | assert(method in ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'TRACE', 'CONNECT', 'OPTIONS']) 661 | self.method = method 662 | 663 | if len(annons) > 1: 664 | path = annons[1] 665 | if path != '': 666 | self.path = path 667 | 668 | def paramSets(self): 669 | pathParams = re.findall(r'\(([^)]+)\)', self.path) 670 | params = [i.name for i in self._variables if i.name != None and i.annotations.get('router', [''])[0] != '-'] 671 | return (pathParams, params) 672 | 673 | @property 674 | def casePathString(self): 675 | pathParams, params = self.paramSets() 676 | union = set(pathParams).intersection(set(params)) 677 | if len(union) == 0: return '' 678 | lets = [i if i in union else '_' for i in params] 679 | return '(let (' + ', '.join(lets) + '))' 680 | 681 | class WSAnnotation: 682 | def __init__(self, case_or_class): 683 | self._variables = case_or_class.variables 684 | self.name = case_or_class.name 685 | self.isOmitValue = False 686 | 687 | annons = case_or_class.annotations.get('ws', ['']) 688 | 689 | if len(annons) > 0: 690 | name = annons[0] 691 | if name == '-': 692 | self.isOmitValue = True 693 | elif name != '': 694 | self.name = name 695 | 696 | if len(annons) > 1: 697 | self.typename = annons[1] 698 | 699 | @property 700 | def eventTypename(self): 701 | n = self.name[0].upper() + self.name[1:] 702 | return n + 'Event' 703 | 704 | def getAnnotationMap(): 705 | return { 706 | 'json' : JSONAnnotation, 707 | 'router': RouterAnnotation, 708 | 'ws' : WSAnnotation 709 | } 710 | 711 | ### Protocols class and functions 712 | 713 | class SwiftProtocol(): 714 | def __init__(self, name, inheritedTypes, clazz): 715 | self._name = name 716 | self._inheritedTypes = inheritedTypes 717 | self._clazz = clazz 718 | 719 | @property 720 | def name(self): return self._name 721 | 722 | @property 723 | def inheritedTypes(self): return self._inheritedTypes 724 | 725 | @property 726 | def clazz(self): return self._clazz 727 | 728 | 729 | def getIdlProtocolByName(protocols, name): 730 | for p in protocols: 731 | if p.name == name: 732 | return p.clazz 733 | return None 734 | 735 | def getIdlProtocols(protocols, typenames, default_protocols): 736 | def getIdlProtocolsByNames(protocols, names): 737 | clazzes = [getIdlProtocolByName(protocols, n) for n in names] 738 | return [c() for c in clazzes if c] 739 | 740 | def getDefaultIdlProtocol(protocols, name = 'Default'): 741 | for p in protocols: 742 | if p.name == name: 743 | return getIdlProtocolsByNames(protocols, p.inheritedTypes) 744 | return [] 745 | 746 | ps = getIdlProtocolsByNames(protocols, typenames) 747 | if len(ps) == 0: 748 | ps = getDefaultIdlProtocol(protocols, default_protocols) 749 | 750 | return ps 751 | 752 | def getNonIdlProtocols(protocols, typenames): 753 | return [n for n in typenames if getIdlProtocolByName(protocols, n) == None] 754 | 755 | ### Render functions 756 | 757 | def indent(text, isRootLevel=False): 758 | lines = [('' if isRootLevel else '\t') + t.replace(' ' * 4, '\t') for t in text.split('\n') if len(t.strip()) > 0] 759 | if isRootLevel: lines = [t if t.strip() != '//' else '' for t in lines] 760 | return '\n'.join(lines) 761 | 762 | ### Render Class (IDL protocols) 763 | 764 | class ClassInit(): 765 | def classTemplates(self, _): 766 | return ''' 767 | <% 768 | p = ', '.join([v.name + ': ' + v.typename + (' = ' + v.defaultValue if v.hasDefaultValue else '') for v in clazz.variables]) 769 | %> 770 | public init(${p}) { 771 | % for v in clazz.variables: 772 | self.${v.name} = ${v.name} 773 | % endfor 774 | } 775 | ''' 776 | 777 | 778 | class Equatable(): 779 | @property 780 | def protocolClass(self): return ['Equatable'] 781 | 782 | @property 783 | def protocolEnum(self): return ['Equatable'] 784 | 785 | def enumTemplates(self, isRawStyle): 786 | if isRawStyle: 787 | return None 788 | return '', ''' 789 | public func == (lhs: ${enum.name}, rhs: ${enum.name}) -> Bool { 790 | switch (lhs, rhs) { 791 | % for case in enum.cases: 792 | <% 793 | if len(case.variables) == 0: 794 | av = 'true' 795 | elif len(case.variables) == 1: 796 | av = 'l == r' 797 | else: 798 | av = ' && '.join(['l.%s == r.%s' % (v.name, v.name) if v.name else 'l.%d == r.%d' % (v.positon, v.positon) for v in case.variables]) 799 | %> 800 | % if len(case.variables) == 0: 801 | case (.${case.name}, .${case.name}): return ${av} 802 | % else: 803 | case let (.${case.name}(l), .${case.name}(r)): return ${av} 804 | % endif 805 | % endfor 806 | default: return false 807 | } 808 | } 809 | ''' 810 | 811 | def classTemplates(self, _): 812 | return '', ''' 813 | <% 814 | p = ' &&\\n\t\t'.join(['lhs.' + v.name + ' == rhs.' + v.name for v in clazz.variables]) 815 | %> 816 | public func == (lhs: ${clazz.name}, rhs: ${clazz.name}) -> Bool { 817 | return ${p} 818 | } 819 | ''' 820 | 821 | class Decodable(): 822 | @property 823 | def protocolClass(self): return ['Decodable'] 824 | 825 | @property 826 | def protocolEnum(self): return ['Decodable'] 827 | 828 | def classTemplates(self, _): 829 | return ''' 830 | <% 831 | hasOmitValues = any(map(lambda v: v.annotation('json').isOmitValue , clazz.variables)) 832 | hasRenameValues = any(map(lambda v: v.annotation('json').jsonLabel != v.name, clazz.variables)) 833 | %> 834 | % if hasOmitValues or hasRenameValues: 835 | private enum CodingKeys: String, CodingKey { 836 | % for v in clazz.variables: 837 | <% 838 | an = v.annotation('json') 839 | %> 840 | % if not an.isOmitValue: 841 | % if v.name == an.jsonLabel: 842 | case ${v.name} 843 | % else: 844 | case ${v.name} = "${an.jsonLabel}" 845 | % endif 846 | % endif 847 | % endfor 848 | } 849 | % endif 850 | ''' 851 | 852 | class Lensy(): 853 | def classTemplates(self, _): 854 | templateInner = ''' 855 | public struct Lenses { 856 | % for v in clazz.variables: 857 | public static let ${v.name} = Lens<${clazz.name}, ${v.typename}>( 858 | g: { $0.${v.name} }, 859 | <% 860 | p = ', '.join([w.name + ': ' + ('newValue' if v.name == w.name else 'this.' + w.name) for w in clazz.variables]) 861 | %> 862 | s: { (this, newValue) in ${clazz.name}(${p}) } 863 | ) 864 | % endfor 865 | } 866 | 867 | public static var $: ${clazz.name}LensHelper<${clazz.name}> { 868 | return ${clazz.name}LensHelper<${clazz.name}>(lens: createIdentityLens()) 869 | } 870 | ''' 871 | templateOuter = ''' 872 | <% 873 | allLenses = [e.name for e in classes if 'Lensy' in e.inheritedTypes] 874 | %> 875 | public struct ${clazz.name}LensHelper: LensHelperType { 876 | public assciatedtype Part = ${clazz.name} 877 | public let lens: Lens 878 | public init(lens: Lens) { 879 | self.init(lens: lens) 880 | } 881 | 882 | % for v in clazz.variables: 883 | <% 884 | helperType = (v.baseTypename + "LensHelper") if v.baseTypename in allLenses else ("LensHelper") 885 | if v.isArray: 886 | helperType = "ArrayLensHelper" % (v.baseTypename, helperType) 887 | elif v.isOptional: 888 | helperType = "OptionalLensHelper" % (v.baseTypename, helperType) 889 | %> 890 | public var ${v.name}: ${helperType} { 891 | return ${helperType}(parent: self, lens: ${clazz.name}.Lenses.${v.name}) 892 | } 893 | % endfor 894 | } 895 | ''' 896 | return templateInner, templateOuter 897 | 898 | 899 | class ErrorType(): 900 | @property 901 | def protocolEnum(self): return ['ErrorType'] 902 | 903 | def enumTemplates(self, _): return None 904 | 905 | 906 | class Printable(): 907 | @property 908 | def protocolClass(self): return ['CustomStringConvertible'] 909 | 910 | @property 911 | def protocolEnum(self): return ['CustomStringConvertible'] 912 | 913 | def classTemplates(self, _): 914 | # FIXME: always public 915 | return ''' 916 | <% 917 | p = ", ".join(["%s=\(%s)" % (v.name, v.name) for v in clazz.variables]) 918 | %> 919 | public var description: String { 920 | return "${clazz.name}(${p})" 921 | } 922 | ''' 923 | 924 | def enumTemplates(self, isRawStyle): 925 | if isRawStyle: # FIXME 926 | return 'public var description: String { return rawValue }' 927 | 928 | return ''' 929 | public var description: String { 930 | switch self { 931 | % for case in enum.cases: 932 | <% 933 | av = ['%s=\(%s)' % (v.name, v.name) if v.name else '\(%s)' % v.varname for v in case.variables] 934 | out = '(' + ', '.join(av) + ')' if len(av) else '' 935 | %> 936 | case .${case.name}${case.letString}: return "${case.name}${out}" 937 | % endfor 938 | } 939 | } 940 | ''' 941 | 942 | 943 | class EnumStaticInit(): 944 | def enumTemplates(self, isRawStyle): 945 | if isRawStyle: 946 | return None 947 | return ''' 948 | % for case in enum.cases: 949 | <% 950 | ais = map(lambda x: '%s: %s = %s()' % (x._name, x.typename, x.typename) if x._name else 'arg%d: %s = %s()' % (x._positon, x.typename, x.typename), case.variables) 951 | cis = map(lambda x: '%s: %s' % (x._name, x._name) if x._name else 'arg%d' % x._positon, case.variables) 952 | params = ", ".join(ais) 953 | out = '(' + ', '.join(cis) + ')' if len(cis) > 0 else '' 954 | %> 955 | public static func make${case.name}(${params}) -> ${enum.name} { 956 | return .${case.name}${out} 957 | } 958 | % endfor 959 | ''' 960 | 961 | 962 | class URLRequestHelper(): 963 | def enumTemplates(self, isRawStyle): 964 | if isRawStyle: 965 | return None # FIXME 966 | else: 967 | return ''' 968 | public var method: String { 969 | switch self { 970 | % for case in enum.cases: 971 | <% an = case.annotation('router') %> 972 | case .${case.name}: return "${an.method}" 973 | % endfor 974 | } 975 | } 976 | // 977 | public var path: String { 978 | switch self { 979 | % for case in enum.cases: 980 | <% an = case.annotation('router') %> 981 | case .${case.name}${an.casePathString}: return "${an.path}" 982 | % endfor 983 | } 984 | } 985 | // 986 | public var params: [String: AnyObject] { 987 | switch self { 988 | % for case in enum.cases: 989 | <% 990 | def toJsonString(info): 991 | if info._isArray: return info._name + '.map { $0.toJSON() }' 992 | return info._name + '.toJSON()' 993 | 994 | an = case.annotation('router') 995 | pathParams, params = an.paramSets() 996 | diff = set(params).difference(set(pathParams)) 997 | 998 | lets = [i if i in diff else '_' for i in params] 999 | letString = ('(let (' + ', '.join(lets) + '))') if len(diff) > 0 else '' 1000 | 1001 | dicx = [i for i in case.variables if i._name in diff] 1002 | 1003 | inits = ['"%s": %s' % (i._name, toJsonString(i)) for i in dicx if not i._isOptional] 1004 | initStr = ', '.join(inits) if len(inits) else ':' 1005 | 1006 | params = ['_ = %s.map { p["%s"] = $0.toJSON() }' % (i._name, i._name) for i in dicx if i._isOptional] 1007 | %> 1008 | % if len(diff) > 0: 1009 | case .${case.name}${letString}: 1010 | % if len(params) > 0: 1011 | var p: [String: AnyObject] = [${initStr}] 1012 | % for p in params: 1013 | ${p} 1014 | % endfor 1015 | return p 1016 | % else: 1017 | return [${initStr}] 1018 | % endif 1019 | % endif 1020 | % endfor 1021 | default: return [:] 1022 | } 1023 | } 1024 | ''' 1025 | 1026 | 1027 | class APIKitHelper(): 1028 | def modifyClass(self, swiftClass): 1029 | # add typealias Response 1030 | ts = [e for e in swiftClass.typealiases if e.name == 'APIKitResponse'] 1031 | ar = None 1032 | if len(ts) == 0: 1033 | ar = SwiftTypealias('APIKitResponse', swiftClass.name + 'Response') 1034 | swiftClass.typealiases.append(ar) 1035 | else: 1036 | ar = ts[0] 1037 | 1038 | rs = [e for e in swiftClass.typealiases if e.name == 'Response'] 1039 | if len(rs) == 0: 1040 | swiftClass.typealiases.append(SwiftTypealias('Response', ar.assignment)) 1041 | 1042 | def classTemplates(self, _): 1043 | return ''' 1044 | <% 1045 | an = clazz.annotation('router') 1046 | %> 1047 | public var method: HTTPMethod { 1048 | return .${an.method.lower()} 1049 | } 1050 | // 1051 | public var path: String { 1052 | return "${an.path}" 1053 | } 1054 | // 1055 | <% 1056 | def makeAssignParamStatement(variable, classes, variable_name = '$0'): 1057 | found = [e for e in classes if e.name == variable.baseTypename] 1058 | if len(found) == 0: 1059 | return variable_name + ' as AnyObject' 1060 | else: 1061 | assert(len(found) == 1) 1062 | typeOrEnum = found[0] 1063 | if typeOrEnum.isEnum: 1064 | if typeOrEnum.isRawStyle: 1065 | return variable_name + '.rawValue as AnyObject' 1066 | else: 1067 | return '/* FIXME: not raw enum */' 1068 | else: 1069 | return '/* FIXME: not enum */' 1070 | 1071 | def toJsonString(variable, classes): 1072 | if variable.isArray: return variable.name + '.map { ' + makeAssignParamStatement(variable, classes) + ' } as AnyObject' 1073 | return makeAssignParamStatement(variable, classes, variable.name) 1074 | 1075 | pathParams, params = an.paramSets() 1076 | diff = set(params).difference(set(pathParams)) 1077 | 1078 | lets = [i if i in diff else '_' for i in params] 1079 | letString = ('(let (' + ', '.join(lets) + '))') if len(diff) > 0 else '' 1080 | 1081 | dicx = [i for i in clazz.variables if i.name in diff] 1082 | 1083 | # FIXME: use annotation('route') insteadof annotation('json') 1084 | inits = ['"%s": %s' % (i.annotation('json').jsonLabel, toJsonString(i, classes)) for i in dicx if not i.isOptional] 1085 | initStr = ', '.join(inits) if len(inits) else ':' 1086 | 1087 | params = ['_ = %s.map { p["%s"] = %s }' % (i.name, i.annotation('json').jsonLabel, makeAssignParamStatement(i, classes)) for i in dicx if i.isOptional] 1088 | %> 1089 | % if len(params) > 0 or len(inits) > 0: 1090 | public var parameters: Any? { 1091 | % if len(diff) > 0 and len(params) > 0: 1092 | var p: [String: AnyObject] = [${initStr}] 1093 | % for p in params: 1094 | ${p} 1095 | % endfor 1096 | return p 1097 | % else: 1098 | return [${initStr}] 1099 | % endif 1100 | } 1101 | % endif 1102 | ''' 1103 | 1104 | 1105 | class WSHelper(): 1106 | def modifyEnum(self, swiftEnum): 1107 | for c in swiftEnum.cases: 1108 | anon = c.annotation('ws') 1109 | if len(c.variables) == 0 and not anon.isOmitValue: 1110 | c._variables = [SwiftTupleVariable(None, anon.eventTypename, 0)] 1111 | 1112 | def enumTemplates(self, _): 1113 | return ''' 1114 | fileprivate struct WSEventTypeChecker: Decodable { 1115 | let type: String 1116 | } 1117 | // 1118 | fileprivate struct WSEvent: Decodable { 1119 | let type: String 1120 | let data: T 1121 | } 1122 | // 1123 | static func parse(data: Data) throws -> ${enum.name}? { 1124 | let decoder = JSONDecoder() 1125 | if #available(OSX 10.12, iOS 10.0, *) { 1126 | decoder.dateDecodingStrategy = .iso8601 1127 | } else { 1128 | fatalError("Please use newer macOS") 1129 | } 1130 | if let eventType = try? decoder.decode(WSEventTypeChecker.self, from: data) { 1131 | let type = eventType.type 1132 | % for case in enum.cases: 1133 | <% an = case.annotation('ws') %> 1134 | % if not an.isOmitValue: 1135 | <% 1136 | name = case.variables[0].typename 1137 | %> 1138 | if type == "${an.name}" { let ev = try decoder.decode(WSEvent<${name}>.self, from: data); return .${case.name}(ev.data) } 1139 | % endif 1140 | % endfor 1141 | } 1142 | return nil // FIXME 1143 | } 1144 | ''' 1145 | 1146 | 1147 | ### command line pipe lines 1148 | 1149 | def getXcodeVersion(): 1150 | p = subprocess.Popen(['xcodebuild', '-version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 1151 | out, err = p.communicate() 1152 | vs = out.split('\n')[0].split() 1153 | if len(vs) > 0: 1154 | return vs[1] 1155 | return None 1156 | 1157 | def getSchemes(project): 1158 | p = subprocess.Popen(['xcodebuild', '-list', '-project', project], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 1159 | out, err = p.communicate() 1160 | a = itertools.dropwhile(lambda e: e != ' Schemes:', out.split('\n')) 1161 | return map(str.strip, itertools.islice(a, 1, None)) 1162 | 1163 | def execSourcekitten(args): 1164 | p = subprocess.Popen([SOURCEKITTEN] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 1165 | out, err = p.communicate() 1166 | return json.loads(out) 1167 | 1168 | def sourcekittenDoc(project, scheme): 1169 | return execSourcekitten(['doc', '--', '-project', project, '-scheme', scheme]) 1170 | 1171 | def sourcekittenSyntax(filepath): 1172 | return execSourcekitten(['syntax', '--file', filepath]) 1173 | 1174 | def parseArgs(): 1175 | parser = argparse.ArgumentParser(description=PROGRAM_NAME + ': Swift source generator from Swift') 1176 | parser.add_argument('project', type=str, nargs='?', default=None, help='project to parse') 1177 | parser.add_argument('scheme', type=str, nargs='?', default='IDL', help='sceheme to parse') 1178 | parser.add_argument('-s', '--sourcekitten', type=str, default='sourcekitten', help='path to sourcekitten') 1179 | parser.add_argument('-o', '--output_dir', type=str, default='out', help='directory to output') 1180 | parser.add_argument('-f', '--force', action='store_true', help='force to output') 1181 | return parser.parse_args() 1182 | 1183 | def checkOutputDir(output_dir): 1184 | if not os.path.isdir(output_dir): 1185 | print('output directory not found: ' + output_dir) 1186 | exit(0) 1187 | 1188 | def gatherIDLProtocol(structure): 1189 | def getProtocolNode(ls, n, tokens): 1190 | if n.get('key.kind', None) == 'source.lang.swift.decl.protocol': 1191 | return ls + [n] 1192 | else: 1193 | return ls 1194 | 1195 | protocol_nodes = sum(processProject(getProtocolNode, structure).values(), []) 1196 | return [visitProtocol(node) for node in protocol_nodes if node['key.name'] in globals()] 1197 | 1198 | def getDeclarations(ls, n, tokens): 1199 | if n.get('key.kind', None) == 'source.lang.swift.decl.class' or n.get('key.kind', None) == 'source.lang.swift.decl.struct': 1200 | return ls + [visitClass(n, tokens)] 1201 | elif n.get('key.kind', None) == 'source.lang.swift.decl.enum': 1202 | return ls + [visitEnum(n, tokens)] 1203 | else: 1204 | return ls 1205 | 1206 | def getImports(filepath): 1207 | r = [] 1208 | im = False 1209 | for t in getTokenList(filepath): 1210 | if t.tokenType == 'source.lang.swift.syntaxtype.keyword': 1211 | im = t.content == 'import' 1212 | elif t.tokenType == 'source.lang.swift.syntaxtype.identifier': 1213 | if im: 1214 | r.append(t.content.strip()) 1215 | im = False 1216 | return r 1217 | 1218 | def resolveProject(proj): 1219 | if proj: return proj 1220 | projs = [f for f in os.listdir('.') if os.path.splitext(f)[1] == '.xcodeproj'] 1221 | return sorted(projs)[0] if len(projs) > 0 else None 1222 | 1223 | def execute(): 1224 | args = parseArgs() 1225 | checkOutputDir(args.output_dir) 1226 | project = resolveProject(args.project) 1227 | if not project: 1228 | print('Xcode project not found') 1229 | return 1230 | schemes = getSchemes(project) 1231 | if not args.scheme in schemes: 1232 | print('Scheme named "%s" is not found in project "%s"' % (args.scheme, project)) 1233 | map(print, ['Available schemes:'] + map(lambda e: '\t' + e, schemes)) 1234 | return 1235 | 1236 | structure = sourcekittenDoc(project, args.scheme) 1237 | decls_map = processProject(getDeclarations, structure) 1238 | 1239 | classes = sum(decls_map.values(), []) 1240 | protocols = gatherIDLProtocol(structure) 1241 | 1242 | for filepath, decls in decls_map.items(): 1243 | if len(decls) == 0: continue 1244 | head, filename = os.path.split(filepath) 1245 | 1246 | outpath = os.path.join(args.output_dir, filename) 1247 | exists = os.path.exists(outpath) 1248 | if not args.force and exists: 1249 | print('Error: output file is already exists: ' + outpath) 1250 | exit(0) 1251 | 1252 | with file(outpath, 'w') as out: 1253 | print(outpath + (' (overwritten)' if exists else '')) 1254 | 1255 | out.write('// This file was auto-generated from %s with %s.' % (filename, PROGRAM_NAME)) 1256 | out.write('\n\n') 1257 | #out.write('import Foundation') 1258 | for i in getImports(filepath): 1259 | out.write('import ' + i + '\n') 1260 | out.write('\n\n') 1261 | map(lambda e: out.write(e.getDeclarationString(classes, protocols) + '\n\n'), decls) 1262 | 1263 | 1264 | if __name__ == '__main__': 1265 | assert(int(getXcodeVersion().split('.')[0]) >= 7) 1266 | execute() 1267 | --------------------------------------------------------------------------------