require 'rubygems' require 'json' require_relative 'swift_types' require_relative 'code_generation_common' require_relative 'swift_types' module SwiftGenerator # A Swift File to be generated. This file contains one or more elements # Elements can be classes (Future: Structs, Enums, Functions) class SwiftFile attr_accessor :file_name attr_accessor :file_path attr_accessor :elements attr_accessor :import_statements attr_accessor :is_user_file attr_accessor :company_name attr_accessor :include_editing_warnings def initialize (name, root_path, is_user_file:false, company_name:"") name += '.swift' unless name.end_with?( '.swift' ) @file_name = name @file_path = File.join(root_path, @file_name) #puts( "--- SwiftFile name = #{name} root_path = #{root_path} file_path = #{@file_path}" ) @is_user_file = is_user_file @elements = [] @import_statements = [] @include_editing_warnings = false @company_name = company_name end def add_element(swift_class) @elements << swift_class end # Called before all other generation-time methods. # Give user-defined elements ( classes, etc. ) the opportunity to construct other related or required elements def prepare_supporting_elements @elements.each do |element| element.prepare_supporting_elements end end def prepare_for_generation @elements.each do |element| element.prepare_for_generation end end # @param [String] module_name def add_import(module_name) import_statement = "import #{module_name}" return if @import_statements.include?(import_statement) @import_statements << import_statement end end # Base class for Struct, Enum and Class class SwiftNonPrimitive attr_accessor :definition_set attr_accessor :type_name attr_accessor :specified_type_name attr_accessor :access_control_modifiers attr_accessor :inheritance_list # Raw parent list attr_accessor :file_name attr_accessor :source_file attr_accessor :top_inner_comment_block # Useful for converted Objective-C classes attr_accessor :properties attr_accessor :initializers attr_accessor :methods attr_accessor :class_characteristics attr_accessor :is_user_editable attr_accessor :is_test_element def initialize( definition_set, specified_type_name, inheritance_list=[], type_name:nil, file_name: nil, characteristics:[], is_user_editable: false, is_test_element: false ) @definition_set = definition_set @specified_type_name = specified_type_name @inheritance_list = inheritance_list @type_name = type_name.nil? ? specified_type_name : type_name @file_name = file_name #@access_control_modifier = 'public ' @access_control_modifiers = [] @class_characteristics = [* characteristics] @is_user_editable = is_user_editable @is_test_element = is_test_element @methods = [] @initializers = [] @properties = [] @post_super_initializations = {} @top_inner_comment_block = [] # This class will now be added to the definition set and its source file # The source file will be created if needed. @definition_set.add_element(self) # Source file is now set @source_file.add_import('Foundation') end def transient_properties() @properties.select { |prop| !prop.is_persistent } end def persistent_properties() @properties.select { |prop| prop.is_persistent } end def comparable_properties() persistent_properties end def resolve_property_types() @properties.each do |property| property.resolve_type end end def swift_type_symbol return @type_name.to_sym end def make_property_type; raise $UNIMPLEMENTED; end def prepare_supporting_elements; raise $UNIMPLEMENTED; end def prepare_for_generation; raise $UNIMPLEMENTED; end end $default_swift_enum_characteristics = [:indexed, :make_test_support] class SwiftEnum < SwiftNonPrimitive attr_accessor :enum_flavor attr_accessor :enum_raw_type attr_accessor :is_integral_type attr_accessor :enum_cases def initialize( definition_set, type_name, inheritance_list=[], file_name: nil, characteristics:[] ) determine_raw_type( inheritance_list ) super( definition_set, type_name, inheritance_list=inheritance_list, file_name: file_name, characteristics:characteristics ) @is_integral_type = $SwiftIntegralTypes.include? @enum_raw_type @enum_cases = [] end def add_case( enum_case ) @enum_cases << enum_case end def make_property_type() # Make a type for this class so that references to this class can be resolved # Includes code for hashing this enum test_value_lambda = lambda{|num| make_test_value(num) } property_type = StringEnumPropertyType.new( self, @enum_raw_type.to_sym, test_value:test_value_lambda ) property_type.hashable_value_lambda = lambda{|var_name, is_optional| if is_optional return "#{var_name}?.toIndex()" else return "#{var_name}.toIndex()" end } #TODO Fix this Horror property_type.custom_unmarshaling = lambda{|var_name, unwrapped_var| "#{var_name} = #{unmarshal_expression(unwrapped_var)}" } return self.swift_type_symbol, property_type end def determine_raw_type(inheritace_list) return if inheritace_list.empty? possible_raw_type = inheritace_list[0] @enum_raw_type = possible_raw_type if $SwiftLegalEnumTypeNames.include? possible_raw_type end def prepare_supporting_elements() end def prepare_for_generation() make_enumeration_properties make_indexing_methods # make_test_support() if characteristics.include? :make_test_support end def make_enumeration_properties() # The count of all cases for convenience p_count = SwiftProperty.new(self, 'caseCount', :Int, initialization_value:self.enum_cases.count ) p_count.property_qualifiers = 'static' # An array of all cases in declaration order case_list = @enum_cases.map{ |a_case| ".#{a_case.case_name}" }.join( ', ' ) p_all_cases = SwiftProperty.new(self, 'allCases', self.type_name.to_sym , collection_type: :array, initialization_value:"[#{case_list}]" ) p_all_cases.property_qualifiers = 'static' end def make_indexing_methods # .toIndex() -> Int to_index_m = SwiftMethod.new(self, "toIndex", '', 'Int', comment: '/// USI standard enum method to get the case count') to_index_m << '// Support for indexing for hashing, etc.' to_index_m << "switch( self ) {" i=0 for a_case in self.enum_cases to_index_m._i "case #{a_case.case_name} : return #{i}" to_index_m._o "" i += 1 end to_index_m.ii "default : return 0" to_index_m << "}" # Not Yet Required # static fromIndex( Int ) -> Enum # from_index_m = SwiftMethod.new(self, "fromIndex", 'index:Int', type_name, comment: '/// USI standard enum method to get the case count') # from_index_m.func_qualifiers = 'static' # from_index_m << "return #{@type_name}.allCases[ index ]" #Note: this is not safe end # JSON marshaling support def marshal_expression( name ) #Probably only works for String enums return "#{name}.rawValue" end def unmarshal_expression( name ) #Probably only works for String enums return "#{@type_name}( rawValue:#{name} )" end # Test Support def make_test_value(index) return "#{@type_name}.allCases[ #{index} % #{@type_name}.caseCount ]" end end class SwiftEnumCase attr_accessor :enum_def attr_accessor :raw_value attr_accessor :case_name def initialize( enum_def, case_name, raw_value=nil ) @enum_def = enum_def @raw_value = raw_value @case_name = case_name enum_def.add_case( self ) end def declaration_lines() raw_value_literal = @raw_value case enum_def.enum_raw_type when "String" raw_value_literal = "\"#{@raw_value}\"" end if raw_value.nil? ["case #{@case_name}"] else ["case #{@case_name} = #{raw_value_literal}"] end end end # class SwiftNumericEnum # attr_accessor :enum_flavor # attr_accessor :enum_raw_type # attr_accessor :enum_cases # # def initialize( definition_set, type_name, file_name: nil, characteristics:[] ) # super( definition_set, type_name, file_name: file_name, characteristics:characteristics ) # end # # def prepare_supporting_elements() # super() # end # # def prepare_for_generation() # super() # end # end # # class SwiftStringEnum # attr_accessor :enum_flavor # attr_accessor :enum_raw_type # attr_accessor :enum_cases # # def initialize( definition_set, type_name, file_name: nil, characteristics:[] ) # super( definition_set, type_name, file_name: file_name, characteristics:characteristics ) # end # # def prepare_supporting_elements() # super() # end # # def prepare_for_generation() # super() # end # end # # # # # @do_generate = true # # @properties = [] # @methods = [] # #@access_control_modifier = 'public ' # @access_control_modifier = '' # # # This class will now be added to the definition set and its source file # # The source file will be created if needed. # # # Source file is now set # @source_file.add_import('Foundation') # @supporting_elements_created = false # end # end # class SwiftStringEnum < SwiftEnum # # end #change $default_swift_class_characteristics = [] # $default_swift_class_characteristics = [:json_serializable, :comparable, :make_test_class, :auto_describing] # A Swift Class to be generated. class SwiftClass < SwiftNonPrimitive attr_accessor :parent_class # The parent class. # Initializations attr_accessor :post_super_initializations # Associated classes attr_accessor :auto_test_class # Convenience attr_accessor :access_control_modifier attr_accessor :do_generate attr_accessor :supporting_elements_created attr_accessor :test_object_method # @param [SwiftDefinitionSet] definition_set # @param [String] type_name # @param [Array] inherited_from # @param [Object] file_name # @param [Object] characteristics # @param [Object] is_test_class def initialize (definition_set, specified_type_name, inheritance_list=[], file_name: nil, characteristics:$default_swift_class_characteristics, is_test_element: false, is_user_editable: false) # Generate the type name from the specified type name. Non-editable classes are prepended with "_" prefix = characteristics.include?(:create_user_class) ? '_' : '' type_name = prefix + specified_type_name super( definition_set, specified_type_name, inheritance_list=inheritance_list, type_name:type_name, file_name:file_name, characteristics:characteristics, is_user_editable:is_user_editable, is_test_element: is_test_element ) @parent_class = nil #change for known & legal combinations of characteristics abort( "illegal class characteristics" ) if ! definition_set.characteristics_are_legal(@class_characteristics ) # prefix = @class_characteristics.include?(:create_user_class) ? '_' : '' # @type_name = prefix + @specified_type_name @do_generate = true @supporting_elements_created = false end def post_super_init( values_for_properties ) @post_super_initializations.merge!( values_for_properties ) end def add_simple_class_property( name, type, value:nil, mutability: :var, override:false) # class variables not supported. Use class property instead. p = SwiftProperty.new(self, name, type, mutability ) p.property_qualifiers = 'class' p.property_qualifiers = "override #{p.property_qualifiers}" if override p.getter_body = "return #{value}" end def make_property_type() # Make a type for this class so that references to this class can be resolved type_symbol = @type_name.to_sym test_value_lambda = lambda{|num| ensure_test_object_method test_object_method_call(num) } property_type = SwiftObjectPropertyType.new( self, :NSDictionary, test_value:test_value_lambda ) property_type.hashable_value_lambda = lambda{|var_name, is_optional| if is_optional return "#{var_name}?.hashValue()" else return "#{var_name}.hashValue()" end } #TODO Fix this Horror property_type.custom_unmarshaling = lambda{|var_name, unwrapped_var| [ "#{var_name} = #{unmarshal_expression(unwrapped_var)}" ]} return self.swift_type_symbol, property_type end # JSON marshaling support def insert_marshal_expression( m, unwrapped_var, destination ) #Probably only works for String enums m << "let objectDictionary = NSMutableDictionary()" m << "#{unwrapped_var}.marshalToJSON( objectDictionary )" m << "#{destination} = objectDictionary" end def insert_unmarshal_expression( m, unwrapped_value, destination ) #Probably only works for String enums m << "let temp = #{self.type_name}()" m << "temp.unmarshalFromJSON( #{unwrapped_value} )" # TODO: validate? m << "#{destination} = temp" end # TODO: Future? unwrapping and success variable. # def insert_unmarshal_expression( m, jsonElement, destination, success_boolean ) # #Probably only works for String enums # m << "if let objectDictionary = #{jsonElement} as? NSDictionary {" # m._i "let temp = #{self.type_name}()" # m << "temp.unmarshalFromJSON( objectDictionary )" # m << "#{destination} = temp" # m._o "#{success_boolean} = true" # m << "} else {" # m.ii "#{success_boolean} = false" # m << "}" # end # Called before all other generation-time methods. # Construct other related or required elements # May be called more than once def prepare_supporting_elements() return if @supporting_elements_created create_user_classes if @class_characteristics.include?(:create_user_class) create_test_classes if ! @is_test_element && @class_characteristics.include?(:make_test_class) @supporting_elements_created = true end def resolve_inheritance() return if @inheritance_list.empty? @parent_class = @definition_set.elements_by_name[ @inheritance_list[0] ] end def prepare_for_generation() create_init_methods create_equality_methods if @class_characteristics.include?(:comparable) prepare_marshaling_code if @class_characteristics.include?(:json_serializable) create_description_methods if @class_characteristics.include?(:auto_describing) create_copy_methods if @class_characteristics.include?(:json_serializable) end def create_init_methods() # Only no-argument init methods & optional properties are currently supported return if @post_super_initializations.empty? init_m = SwiftInitializer.new(self, 'init', nil, override: true) init_m << "super.init()" keys = @post_super_initializations.keys.sort! keys.each do |prop_name| init_m << "#{prop_name} = #{@post_super_initializations[prop_name]}" end end def create_equality_methods() comparable_super = super_has_characteristics(:comparable) #func isEqual(_ anObject: AnyObject!) -> Bool is_equals_method = SwiftMethod.new(self, 'isEqual', 'other:AnyObject!', 'Bool', override: true) is_equals_method << "if( other == nil ) { return false }" other_typed_var = "other#{@type_name}" if comparable_super is_equals_method << "if( !super.isEqual( other )) { return false }" end is_equals_method << "" is_equals_method << "if let #{other_typed_var} = other as? #{@type_name} {" comparable_properties.each do |property| other_name = "#{other_typed_var}.#{property.property_name}" if property.property_type.nil? puts( property.property_name ) end if property.collection_type == :array is_equals_method.ii "if( !optionalArraysEqual( #{property.property_name}, #{other_name} )) { return false }" else custom_test = property.property_type.custom_equality_test if custom_test.nil? is_equals_method.ii "if( self.#{property.property_name} != #{other_name} ) { return false }" else is_equals_method.ii "if( !#{custom_test.call(property.property_name, other_name)} ) { return false }" end end end is_equals_method << "" << "\treturn true" is_equals_method << "} else {" is_equals_method << "\treturn false" is_equals_method << "}" #- (NSUInteger)hash hash_method = SwiftMethod.new(self, 'hashValue', '', 'Int', override: comparable_super) hash_method << "var hasher = Hasher() // Hasher must be mutable" << "" if comparable_super hash_method << 'hasher.hashIn( super.hashValue() )' end comparable_properties.each do |property| hash_element = lambda{ |var_name, is_optional| "hasher.hashIn( #{property.property_type.hashable_value(var_name, is_optional )} )"} if( property.collection_type.nil?) hash_method << hash_element.call(property.property_name, property.mutability_type.must_be_unwrapped) elsif( property.collection_type == :array ) arrayName = "#{property.property_name}Array" hash_method << "if let #{arrayName} = #{property.property_name} {" hash_method._i "for element in #{arrayName} {" hash_method.ii hash_element.call("element", false) hash_method._o "}" hash_method << "}" else hash_method << "ERROR: hashing of #{property.collection_type.to_s} collections not supported." end end hash_method << "" << "return hasher.hash" end def prepare_marshaling_code() super_does_json = super_has_characteristics(:json_serializable) # Reader & Writer reader = SwiftMethod.new(self, 'unmarshalFromJSON', 'jsonObject:NSDictionary', nil, override: super_does_json ) writer = SwiftMethod.new(self, 'marshalToJSON', 'jsonObject:NSMutableDictionary', nil, override: super_does_json ) if super_does_json reader << "super.unmarshalFromJSON( jsonObject )" writer << "super.marshalToJSON( jsonObject )" end writer << "" << "jsonObject[\"objectType\"] = \"#{@type_name}\"" persistent_properties.each do |prop| prop.unmarshal_code(reader) prop.marshal_code(writer) end # Object Reader object_reader = SwiftMethod.new(self, "objectFromJSON", 'newObjData:NSDictionary', @specified_type_name, override: super_does_json ) object_reader.func_qualifiers = 'class' object_reader << "let newObj = #{@specified_type_name}()" object_reader << "newObj.unmarshalFromJSON( newObjData )" object_reader << 'return newObj' # Array Reader & Writer # Array Reader array_reader = SwiftMethod.new(self, "arrayFromJSON", 'json:NSArray', '[USIBaseModel]', override: super_does_json ) array_reader.func_qualifiers = 'class' array_reader << "var newObjects = [#{@specified_type_name}]()" array_reader << "for objEntry in json {" array_reader << "\tlet newObj = #{@specified_type_name}()" array_reader << "\tif let newObjData = objEntry as? NSDictionary {" array_reader << "\t\tnewObj.unmarshalFromJSON( newObjData )" array_reader << "\t\tnewObjects.append( newObj )" array_reader << "\t}" array_reader << '}' array_reader << 'return newObjects' # Array Writer array_writer = SwiftMethod.new(self, "arrayToJSON", "array:[USIBaseModel]", 'NSMutableArray', override: super_does_json ) array_writer.func_qualifiers = 'class' array_writer << 'var dataArray = NSMutableArray()' array_writer << "for obj in array {" array_writer << "\tlet objData = NSMutableDictionary()" array_writer << "\tobj.marshalToJSON( objData )" array_writer << "\tdataArray.addObject( objData )" array_writer << "}" array_writer << "return dataArray" end # Copy methods def create_copy_methods() copy_to_method_name = "copyTo#{@type_name}" # The copy method - calls copyTo so that we can avoid duplicating copy code copy_m = SwiftMethod.new(self, 'copy', '', 'AnyObject', override: true) copy_m << "let theCopy = #{@type_name}()" copy_m << "#{copy_to_method_name}( theCopy )" copy_m << "return theCopy" copy_to_m = SwiftMethod.new(self, copy_to_method_name, "other: #{@type_name}", nil, override:false) if !@parent_class.nil? copy_to_m << "super.copyTo#{@parent_class.type_name}( other )" end self.persistent_properties.each do |prop| copy_to_m << "\tother.#{prop.property_name} = #{prop.property_name}" end end # Description methods def create_description_methods() # description dm = SwiftMethod.new(self, 'description', 'indent:String=" ", diagnostic:Bool=false', "NSString", override:!@parent_class.nil? ) dm << "let d = NSMutableString()" if @parent_class.nil? else dm << "d.appendString( super.description(indent:indent, diagnostic:diagnostic) )" end dm << "if( diagnostic ) {" dm.ii "d.appendString( \" properties of class #{@type_name} :\\n\")" dm.ii "d.appendString( \"\\(indent)- none -\\n\")" if @properties.empty? dm << "}" self.persistent_properties.each do |prop| if( prop.is_optional ) prop_value = prop.property_name if prop.property_type.swift_kind == :enum dm << "d.appendString( \"\\(indent)#{prop.property_name} = \\(prettyFormatEnum( #{prop_value} ))\\n\" )" else dm << "d.appendString( \"\\(indent)#{prop.property_name} = \\(prettyFormat( #{prop_value} ))\\n\" )" end else dm << "d.appendString( \"\\(indent)#{prop.property_name} = \\(#{prop_value})\\n\" )" end end dm << "return d" # className type_name_m = SwiftMethod.new(self, 'className', nil, 'String', override:!@parent_class.nil? ) type_name_m .func_qualifiers = 'class' type_name_m << "return \"#{@type_name}\"" end # User class generation def create_user_classes user_class_characteristics = @class_characteristics - [:make_test_class, :create_user_class] @user_editable_class = SwiftClass.new(@definition_set, @specified_type_name, [@type_name], characteristics:user_class_characteristics, is_user_editable:true ) end # Test class generation def create_test_classes() tc = @auto_test_class = SwiftUnitTestClass.new(@definition_set, self, 'AutoGenerated') end # Utility def super_has_characteristics( *characteristics ) remaining_characteristics = characteristics ancestor = @parent_class until ancestor.nil? || remaining_characteristics.empty? remaining_characteristics = (remaining_characteristics - ancestor.class_characteristics) ancestor = ancestor.parent_class end return remaining_characteristics.empty? end def set_test_values( set_method, variable_name, indexing_number ) if ! @parent_class.nil? indexing_number = @parent_class.set_test_values( set_method, variable_name, indexing_number ) end comparable_properties.each do |prop| set_method << "#{variable_name}.#{prop.property_name} = #{prop.make_test_value(indexing_number)}" indexing_number += 1 end return indexing_number end # Test Support ( Here for embedded objects ) def ensure_test_object_method #NOTE: this method is in the global test support class return if !@test_object_method.nil? comment = "/// Create a test #{@type_name} object with varying values" # e.g. func makeTestUser() -> User obj_name = "test#{@type_name}" m = SwiftMethod.new(@definition_set.test_support_class, "makeTest#{@type_name}", '_ index:Int = 0', "#{@type_name}", comment: comment) m.func_qualifiers = 'class' m << "let #{obj_name} = #{@type_name}()" << "" prop_index = 1 set_test_values( m, obj_name, 1 ) m << "" << "return #{obj_name}" @test_object_method = m end def test_object_method_call(index=0) argStr = index == 0 ? '' : "#{index}" "#{@definition_set.test_support_class.type_name}.makeTest#{@type_name}(#{argStr})" end end class SwiftCategory < SwiftClass attr_accessor :categorized_class_name def initialize( definition_set, specified_type_name, categorized_class_name, file_name: nil, characteristics:$default_swift_class_characteristics, is_test_element: false, is_user_editable: false ) super( definition_set, specified_type_name, inheritance_list=[], file_name:file_name, characteristics:characteristics, is_test_element:is_test_element, is_user_editable:is_user_editable ) @categorized_class_name = categorized_class_name || nil end end class SwiftProtocol < SwiftClass attr_accessor :categorized_class_name def initialize( definition_set, specified_type_name, inheritance_list, file_name: nil, characteristics:$default_swift_class_characteristics ) super( definition_set, specified_type_name, inheritance_list=[], file_name:file_name, characteristics:characteristics, is_test_element:false, is_user_editable:false ) @categorized_class_name = categorized_class_name || nil end end class SwiftUnitTestClass < SwiftClass attr_accessor :tested_class attr_accessor :tested_class_name # @param [SwiftClass] tested_class # @param [String] class_purpose def initialize(definition_set, tested_class, class_purpose) @tested_class = tested_class @tested_class_name = tested_class.type_name class_name = tested_class.specified_type_name + class_purpose + "Test" super(definition_set, class_name, ['XCTestCase'], file_name: class_name, is_test_element: true, characteristics:[] ) @source_file.add_import('XCTest') end def prepare_for_generation() super() generate_copy_test generate_marshal_test generate_json_round_trip_test end def generate_copy_test ensure_test_object_method comment = "/// Test copy() implementation. Requires isEqual()" m = SwiftMethod.new(self, "testCopying", '', '', comment: comment) m << "let original = #{test_object_method_call()}" m << "let theCopy = original.copy() as #{@tested_class_name}" m << "if( theCopy != original ) {" m << " print(\"original\")" m << " print(original.description())" m << " print(\"theCopy\")" m << " print(theCopy.description())" m << "}" m << "XCTAssertEqual( theCopy, original, \"copy does not match original\" )" end def generate_marshal_test ensure_test_object_method comment = "/// Test Marshaling to JSON-compatible dictionaries" m = SwiftMethod.new(self, 'testMarshaling', '', nil, comment: comment) m << "let original = #{test_object_method_call()}" m << "" << "let jsonObject = NSMutableDictionary()" m << "original.marshalToJSON( jsonObject )" << "" m << "let theCopy = #{@tested_class_name}()" m << "theCopy.unmarshalFromJSON( jsonObject )" << "" m << "if( theCopy != original ) {" m._i "print(\"original\")" m << "print(original.description())" m << "print(\"theCopy\")" m._o "print(theCopy.description())" m << "}" m << "XCTAssertEqual( theCopy, original, \"unmarshalled copy does not match original\" )" end def generate_json_round_trip_test ensure_test_object_method comment = "/// Test JSON round trip for a single object" m = SwiftMethod.new(self, 'testJSONRoundTrip', '', nil, comment: comment) m << "let original = #{test_object_method_call()}" m << "" << "let jsonObject = NSMutableDictionary()" m << "original.marshalToJSON( jsonObject )" << "" m << "var error: NSError? = nil" m << "let jsonData = NSJSONSerialization.dataWithJSONObject(jsonObject, options: nil, error:&error) as NSData?" m << "XCTAssertNotNil( jsonData, \"Could not serialize to NSData\" )" m << "var deserializedJSON:AnyObject? = NSJSONSerialization.JSONObjectWithData(jsonData!, options: nil, error:&error)" m << "XCTAssertNotNil( deserializedJSON, \"Could not serialize to NSData\" )" m << "if let newJSONObject = deserializedJSON as? NSDictionary {" m._i "let theCopy = #{@tested_class_name}()" m << "theCopy.unmarshalFromJSON( newJSONObject )" m << "if( theCopy != original ) {" m._i "print(\"original\")" m << "print(original.description())" m << "print(\"theCopy\")" m._o "print(theCopy.description())" m << "}" m._o "XCTAssertEqual( theCopy, original, \"unmarshalled object should be == to original\" )" m << "} else {" m.ii "XCTAssert( false, \"JSON did not deserialize to an NSDictionary\" )" m << "}" end # Utility def ensure_test_object_method @tested_class.ensure_test_object_method end def test_object_method_call(index=0) @tested_class.test_object_method_call( index ) end end class SwiftProperty attr_accessor :swift_class attr_accessor :property_name attr_accessor :property_type_symbol attr_accessor :property_type attr_accessor :mutability_type attr_accessor :property_qualifiers attr_accessor :is_persistent attr_accessor :collection_type attr_accessor :required attr_accessor :initialization_value attr_accessor :getter_body attr_accessor :setter_body attr_accessor :rest_omit attr_accessor :access_control_modifiers attr_accessor :protocol_get_set_spec # for declarations in protocols def initialize(swift_class, property_name, property_type_symbol, mutability= :let, initialization_value:nil, collection_type: nil, required: true, rest_omit:nil ) @swift_class = swift_class @property_name = property_name @property_type_symbol = property_type_symbol @property_type = nil # @property_type = swift_class.definition_set.property_type_for_symbol(property_type) @mutability_type = SwiftDefinitionSet.mutability_types[mutability] @is_persistent = false @collection_type = collection_type @required = required #@access_control_modifier = 'public ' @access_control_modifiers = nil @property_qualifiers = nil @initialization_value = initialization_value @getter_body = nil @setter_body = nil @rest_omit = rest_omit @protocol_get_set_spec = nil swift_class.properties << self end def declaration_lines qualifiers = [] qualifiers += [*@access_control_modifiers] unless @access_control_modifiers.nil? qualifiers += [*@property_qualifiers] unless @property_qualifiers.nil? qualifiers << @mutability_type.mutability declaration = "#{qualifiers.join(' ')} #{@property_name} : #{full_type_specifier()}" # Initial Value initial_value = @initialization_value if !initial_value.nil? if( collection_type == :array ) if( @mutability_type.mutability_id == :optional ) # Initialize variable arrays to empty by default initial_value = "[]" end end end declaration += " = #{initial_value}" unless initial_value.nil? declaration += " #{@protocol_get_set_spec}" unless @protocol_get_set_spec.nil? # Must be set if part of a protocol definition # Computed Properties if !( @getter_body.nil? && @setter_body.nil? ) declaration = [declaration + " {"] if !@getter_body.nil? declaration << "\tget {" declaration.concat([*@getter_body].map { |line| "\t\t" + line }) declaration << "\t}" end if !@setter_body.nil? declaration << "" unless @getter_body.nil? declaration << "\tset {" declaration.concat([*@setter_body].map { |line| "\t\t" + line }) declaration << "\t}" end declaration << "}" end return [*declaration] end def full_type_specifier # Apply Collection full_type_name = @property_type.swift_type_name if @collection_type == :array full_type_name = "[#{full_type_name}]" end "#{full_type_name}#{@mutability_type.declaration_wrapping}" end def resolve_type() @property_type = @swift_class.definition_set.property_type_for_symbol(@property_type_symbol) abort( "No property type found for #{@property_type_symbol.to_s}") if @property_type.nil? end def make_test_value(index) if @collection_type.nil? return @property_type.make_test_value(index) else return '[]' end end def property_declared_type @property_type.swift_type_name + @mutability_type.declaration_wrapping end #Utility def is_array_of_nsobject (@collection_type == :array) && (@property_type.swift_kind == :class) end def is_optional return @mutability_type.mutability_id == :optional end end class SwiftBlockProperty < SwiftProperty attr_accessor :declaration_string def initialize( swift_class, property_name, declaration_string, mutability= :let, initialization_value:nil, required: false ) super(swift_class, property_name, property_type_symbol, mutability= :let, initialization_value:nil, collection_type: nil, required: required, rest_omit:true ) @declaration_string = declaration_string end def resolve_type() @property_type = @declaration_string end def full_type_specifier return @declaration_string end def property_declared_type @declaration_string + @mutability_type.declaration_wrapping end end class SwiftPersistentProperty < SwiftProperty attr_accessor :json_key def initialize(swift_class, property_name, property_type, mutability=:let, initialization_value=nil, collection_type: nil, json_key: nil, rest_omit:nil ) super(swift_class, property_name, property_type, mutability, initialization_value:initialization_value, collection_type: collection_type, rest_omit:rest_omit) @is_persistent = true @json_key = json_key.nil? ? property_name : json_key end def marshal_code(marshal_method) if @collection_type.nil? marshal_single_element(marshal_method) else case @collection_type when :array marshal_array(marshal_method) when :dictionaryByString else puts "ERROR: Unknown collection_type: #{@collection_type.to_s}" end end end def marshal_single_element(marshal_method) return if self.rest_omit == :omit omit_null = self.rest_omit == :omit_if_null case @property_type.swift_kind when :primitive case @mutability_type.mutability_id when :optional marshal_template(marshal_method, omit_null){ |m, unwrapped_var, destination| m.<< "#{destination} = #{unwrapped_var}" } else marshal_method << "ERROR Non-optional properties not yet supported" end when :enum case @mutability_type.mutability_id when :optional marshal_template(marshal_method, omit_null){ |m, unwrapped_var, destination| # TODO: Make this look like the class property method below m << "#{destination} = #{@property_type.enum.marshal_expression( unwrapped_var)}" } else marshal_method << "ERROR Non-optional properties not yet supported" end when :class case @mutability_type.mutability_id when :optional marshal_template(marshal_method, omit_null){ |m, unwrapped_var, destination| @property_type.swift_class.insert_marshal_expression( m, unwrapped_var, destination) } else marshal_method << "ERROR Non-optional properties not yet supported" end when :struct return 'Structs not yet supported' else puts "ERROR: Unknown swift_kind: #{@property_type.swift_kind.to_s}" end end def marshal_array(marshal_method) return if self.rest_omit == :omit omit_null = self.rest_omit == :omit_if_null case @property_type.swift_kind when :primitive if( @mutability_type.mutability_id != :optional ) marshal_method << "ERROR Non-optional properties not yet supported" end marshal_template(marshal_method, omit_null) { |m, unwrapped_var, destination| m << "var newArray = NSMutableArray()" m << "for element in #{unwrapped_var} {" m.ii "newArray.addObject( element as #{@property_type.serialized_json_type.to_s} )" m << "}" m << "#{destination} = newArray" } when :class if( @mutability_type.mutability_id != :optional ) marshal_method << "ERROR Non-optional properties not yet supported" end marshal_template(marshal_method, omit_null) { |m, unwrapped_var, destination| m.ii "#{destination} = #{@property_type.swift_type_name}.arrayToJSON(#{unwrapped_var})" } when :struct return 'Arrays of structs not yet supported' when :enum if( @mutability_type.mutability_id != :optional ) marshal_method << "ERROR Non-optional properties not yet supported" end marshal_template(marshal_method, omit_null) { |m, unwrapped_var, destination| m << "var newArray = NSMutableArray()" m << "for element in #{unwrapped_var} {" m.ii "newArray.addObject( #{@property_type.enum.marshal_expression( "element" )} )" m << "}" m << "#{destination} = newArray" } else puts "ERROR: Unknown swift_kind: #{@property_type.swift_kind.to_s}" end end def marshal_template(marshal_method, omit_null, &detail) m = marshal_method unwrapped_var = @property_name + "Val" destination = "jsonObject[ \"#{@json_key}\" ]" m << "" m << "if let #{unwrapped_var} = #{@property_name} {" if self.property_type.custom_marshaling.nil? m.indent += 1 detail.call( m, unwrapped_var, destination) m.indent -= 1 else m.ii self.property_type.custom_marshaling.call( destination ,unwrapped_var) end m << "} else {" if( ! omit_null ) m << "\t#{destination} = NSNull()" else m << "// Omit nil values" end m << "}" end def unmarshal_code(unmarshal_method) if @collection_type.nil? unmarshal_single_element(unmarshal_method) else case @collection_type when :array unmarshal_array(unmarshal_method) when :dictionaryByString else puts "ERROR: Unknown collection_type: #{@collection_type.to_s}" end end end def unmarshal_single_element(unmarshal_method) m = unmarshal_method case @property_type.swift_kind when :primitive, :enum case @mutability_type.mutability_id when :optional unwrapped_var = @property_name + "Val" m << "" m << "if let #{unwrapped_var} = jsonObject[ \"#{@json_key}\" ] as? #{@property_type.serialized_json_type} {" if self.property_type.custom_unmarshaling.nil? m.ii "#{@property_name} = #{unwrapped_var} " else m.ii self.property_type.custom_unmarshaling.call( @property_name, unwrapped_var ) end m << "} else {" m.ii "#{@property_name} = nil" m << "}" else m << "ERROR Non-optional properties not yet supported" end # if let aVal = jsonObject[ "a" ] as? Int { # a = aVal # } else { # a = nil # } # if @property_type.auto_bridged # return "#{@property_name} = jsonObject[ \"#{@json_key}\" ]" # else return "#{@property_name} = jsonObject[ \"#{@json_key}\" ] as #{property_declared_type}" # end when :class case @mutability_type.mutability_id when :optional unwrapped_var = @property_name + "Val" # TODO: check for success # m << "var success = false" m << "if let #{unwrapped_var} = jsonObject[ \"#{@json_key}\" ] as? #{@property_type.serialized_json_type} {" self.property_type.swift_class.insert_unmarshal_expression( m, unwrapped_var, @property_name ) m << "} else {" m.ii "#{@property_name} = nil" m << "}" else m << "ERROR Non-optional properties not yet supported" end when :struct return 'Structs not yet supported' else puts "ERROR: Unknown swift_kind: #{@property_type.swift_kind.to_s}" end end def unmarshal_array(unmarshal_method) m = unmarshal_method case @property_type.swift_kind when :primitive if( @mutability_type.mutability_id == :optional ) unwrapped_var = @property_name + "Array" prop_type = @property_type.swift_type_name json_type = @property_type.serialized_json_type.to_s m << "" m << "if let #{unwrapped_var} = jsonObject[ \"#{@property_name}\" ] as? NSArray {" m._i "var newArray = [#{prop_type}]()" m << "for element in #{unwrapped_var} {" m._i "if let typedElement = element as? #{json_type} {" if @property_type.auto_bridged m.ii "newArray.append( typedElement )" else m._i "if let unmarshalledValue = typedElement as #{prop_type} {" m.ii "newArray.append( unmarshalledValue )" m << "} else {" m.ii "println( \"Error in converting \\(typedElement) to #{prop_type}\")" m._o "}" end m << "} else {" m.ii "println( \"Unexpected json value: \\(element) for #{prop_type}\")" m._o "}" m << "}" m._o "#{@property_name} = newArray" m << "} else {" m.ii "#{@property_name} = nil" m << "}" else m << "ERROR Non-optional properties not yet supported" end when :class if( @mutability_type.mutability_id == :optional ) unwrapped_var = @property_name + "Array" prop_type = @property_type.swift_type_name m << "" m << "if let #{unwrapped_var} = jsonObject[ \"#{@property_name}\" ] as? NSArray {" m.ii "#{@property_name} = #{prop_type}.arrayFromJSON( #{unwrapped_var} ) as? [#{prop_type}]" m << "} else {" m.ii "#{@property_name} = nil" m << "}" else m << "ERROR Non-optional properties not yet supported" end when :struct m << 'ERROR Arrays of structs not yet supported' when :enum if( @mutability_type.mutability_id == :optional ) unwrapped_var = @property_name + "Array" prop_type = @property_type.swift_type_name m << "" m << "if let #{unwrapped_var} = jsonObject[ \"#{@property_name}\" ] as? NSArray {" m._i "var newArray = [#{prop_type}]()" m << "for element in #{unwrapped_var} {" m._i "if let typedElement = element as? NSString {" m._i "if let enumValue = #{@property_type.enum.unmarshal_expression( "typedElement" )} {" m.ii "newArray.append( enumValue )" m << "} else {" m.ii "println( \"Error in converting \\(typedElement) to #{prop_type}\")" m._o "}" m << "} else {" m.ii "println( \"Unexpected json value: \\(element) for #{prop_type}\")" m._o "}" m << "}" m._o "#{@property_name} = newArray" m << "} else {" m.ii "#{@property_name} = nil" m << "}" else m << "ERROR Non-optional properties not yet supported" end else puts "ERROR: Unknown swift_kind: #{@property_type.swift_kind.to_s}" end end def all_collection_types [ :array, :dictionaryByString ] end end class SwiftMethodBase attr_accessor :name attr_accessor :argStr attr_accessor :override attr_accessor :returns attr_accessor :comment attr_accessor :access_control_modifiers attr_accessor :func_qualifiers attr_accessor :indent attr_accessor :bodyLines def initialize (swift_element, name, argStr, returns, override: false, comment: nil) @name = name @argStr = argStr @returns = returns @override = override @comment = comment @indent = 0 @bodyLines = [] @access_control_modifiers = [] end def << (*line_or_lines) line_or_lines = line_or_lines.flatten() line_or_lines.each do |line| new_line = ("\t" * @indent) + line @bodyLines << new_line end @bodyLines = @bodyLines.flatten() end def _i (*line_or_lines) @indent += 1 self << line_or_lines end def _o (*line_or_lines) self << line_or_lines @indent -= 1 end def ii (*line_or_lines) @indent += 1 self << line_or_lines @indent -= 1 end def func_fragment() return 'func' if func_qualifiers.nil? return [*func_qualifiers].join( ' ' ) + ' func' end end class SwiftInitializer < SwiftMethodBase def initialize (swift_element, name, argStr, override: false, comment: nil) super(swift_element, name, argStr, nil, override:override, comment:comment) swift_element.initializers << self end def func_fragment() return [*func_qualifiers].join( ' ' ) end end class SwiftMethod < SwiftMethodBase def initialize (swift_element, name, argStr, returns, override: false, comment: nil) super(swift_element, name, argStr, returns, override:override, comment:comment) swift_element.methods << self end def func_fragment() return 'func' if func_qualifiers.nil? return [*func_qualifiers].join( ' ' ) + ' func' end end # Variable & Property Mutability class MutabilityType attr_accessor :mutability_id attr_accessor :mutability attr_accessor :declaration_wrapping attr_accessor :must_be_unwrapped def initialize(mutability_id, mutability, declaration_wrapping, must_be_unwrapped: false) @mutability_id = mutability_id @mutability = mutability @declaration_wrapping = declaration_wrapping @must_be_unwrapped = must_be_unwrapped end end # The single coordinator object for a set of swift file generations class SwiftDefinitionSet attr_accessor :generated_root attr_accessor :generated_test_root attr_accessor :generated_user_root attr_accessor :make_unknown_types attr_accessor :all_class_characteristics attr_accessor :output_files attr_accessor :elements_by_name # All non-primitives # attr_accessor :classes_by_name # Only classes # attr_accessor :enums_by_name # Only enums attr_accessor :types_by_symbol attr_accessor :make_more_supporting_elements attr_accessor :test_support_class attr_accessor :company_name def self.mutability_types { let: MutabilityType.new(:let, 'let', ''), var: MutabilityType.new(:var, 'var', ''), optional: MutabilityType.new(:optional, 'var', '?', must_be_unwrapped: true), unwrapped: MutabilityType.new(:unwrapped, 'var', '!') } end def self.collection_types { list: "list", idList: "idList" } end def initialize( generated_root:nil, generated_user_root:nil, generated_test_root:nil) @generated_root = File.expand_path(generated_root) @generated_test_root = File.expand_path(generated_test_root) unless generated_test_root.nil? @generated_user_root = File.expand_path(generated_user_root) unless generated_test_root.nil? @make_unknown_types = false @output_files = {} @elements_by_name = {} @make_more_supporting_elements = true @all_class_characteristics = [ :json_serializable, :comparable, :make_test_class, :create_user_class ] @types_by_symbol = {} initial_property_types.each do |propType| @types_by_symbol[propType.swift_type_symbol] = propType end @company_name = "" end def run_generation_sequence prepare_output_paths prepare_supporting_elements resolve_property_types resolve_inheritance prepare_for_generation resolve_property_types end def prepare_output_paths # Create / empty the generated output directories paths = [] paths << @generated_root unless @generated_root.nil? paths << @generated_test_root unless @generated_test_root.nil? paths.each do |path| FileUtils.mkdir_p(path) Dir.foreach(path) do |entry| file_path = File.join(path, entry) if (File::ftype(file_path) == "file") puts "deleting " + file_path File.delete(file_path) end end end end # Called first during code generation. Note that this method may lead to the creation of new files def prepare_supporting_elements() unless @generated_test_root.nil? @test_support_class = SwiftClass.new(self, 'AutoGeneratedTestSupport', [], characteristics:[], is_test_element: true ) unless while @make_more_supporting_elements do @make_more_supporting_elements = false original_output_files = @output_files.dup original_output_files.each do |_, swiftFile| swiftFile.prepare_supporting_elements end end end end # Called during code generation. def resolve_inheritance() @elements_by_name.each do |_, element| element.resolve_inheritance if element.respond_to? :resolve_inheritance end end def resolve_property_types() @elements_by_name.each do |_, element| element.resolve_property_types if element.respond_to? :resolve_property_types end end def prepare_for_generation @output_files.each do |_, swiftFile| swiftFile.prepare_for_generation end end def add_element(swift_element) file_name = swift_element.file_name || swift_element.type_name the_file = file_for_name( file_name, swift_element.is_test_element, swift_element.is_user_editable ) the_file.add_element(swift_element) swift_element.source_file = the_file # if swift_element.is_a? SwiftEnum # @enums_by_name[swift_element.type_name] = swift_element # elsif swift_element.is_a? SwiftClass # @elements_by_name[swift_element.type_name] = swift_element # end @elements_by_name[swift_element.type_name] = swift_element @make_more_supporting_elements = true # # Make a type for this class so that references to this class can be resolved # type_symbol = swift_element.type_name.to_sym # element_property_type = SwiftPropertyType.new( type_symbol, :NSDictionary, swift_kind: :class, test_value:nil ) # @types_by_symbol[type_symbol] = element_property_type (type_symbol, property_type) = swift_element.make_property_type @types_by_symbol[type_symbol] = property_type end # def add_json_serializable_class(serializable_class) # @json_serializable_classes << serializable_class # end def file_for_name(name, is_test_element, is_user_editable) if( is_test_element ) abort( "User-modifiable test classes are not generated" ) if is_user_editable file_dir = @generated_test_root else if( is_user_editable ) file_dir = @generated_user_root else file_dir = @generated_root end end @output_files[name] ||= SwiftFile.new(name, file_dir, is_user_file:is_user_editable, company_name:@company_name) return @output_files[name] end def property_type_for_symbol(symbol) resolved = @types_by_symbol[symbol] if resolved.nil? if @make_unknown_types resolved = make_unknown_type( symbol ) else puts( "ERROR: Unknown symbol: #{symbol.to_s}") end end resolved end def make_unknown_type( symbol ) property_type = SwiftPropertyType.new( symbol, :NSDictionary ) #TODO: NSDictionary? for unknown type? @types_by_symbol[ symbol ] = property_type property_type end # General purpose def characteristics_are_legal( characteristics ) unknown_characteristics = (characteristics - @all_class_characteristics) unknown_characteristics.empty? end end end