namespace Cobra.Core use System.Collections use System.Globalization use System.Reflection use System.Runtime.CompilerServices class StringMaker is abstract """ StringMaker provides the base capability to create strings from objects. Well known subclasses are PrintStringMaker and TechStringMaker. The main public method is .makeString(x as Object?) as String Subclasses should set _methodName in .init and override abstract methods like .collectionTypeToString. """ var _level as int var _methodName as String var _memo = Set() cue init base.init _methodName = '' # ideally this wouldn't have to be set here get level from var """ Returns the recursion level of .makeString which starts at 1 and increases. Some string makers use this information to alter their output. """ def makeString(x as Object?, format as String) as String """ Returns the formatted version of the object. This is was created in support of string substitution which can specify format codes such as 'x = [x:N2]' Example: sm.makeString(5.0, 'N2') """ if format.length == 0 return .makeString(x) else # Is there a faster way to do this? Some types already have .toString(format as String) overload. return String.format(CultureInfo.invariantCulture, '{0:' + format + '}', x) to ! def makeString(x as Object?) as String has MethodImpl(MethodImplOptions.Synchronized) if x if .isMemoable(x) if x in _memo return '(Repeated [CobraCore.typeName(x.getType)])' _memo.add(x) try isEnumerable = x inherits System.Collections.IEnumerable if isEnumerable, _level += 1 try return _makeString(x) finally if isEnumerable, _level -= 1 finally if _level < 1, _memo.clear def _makeString(x as Object?) as String if x is nil return .nilToString if x inherits bool return .boolToString(x) if x inherits char return .charToString(x) if x inherits String return .stringToString(x) if _methodName.length s = '' if .customMethodToString(x to !, inout s) return s if x inherits System.Collections.IList return .listToString(x) if x inherits System.Collections.IDictionary return .dictToString(x) if x inherits ISet return .setToString(x) if x inherits System.Collections.IEnumerable # This must come after the above types like IDictionary and ISet. return .enumerableToString(x) if x inherits System.Enum return .enumToString(x) if x inherits StringMaker return .stringMakerToString(x) return .objectToString(x to !) def isMemoable(x as Object) as bool if x inherits BasePair, return false if x inherits String, return false if x.typeOf.isValueType, return false if x inherits IEnumerable, return true return false def boolToString(b as bool) as String return if(b, 'true', 'false') def charToString(c as char) as String return c.toString def collectionTypeToString(t as Type) as String is abstract var _customMethodsCache = Dictionary() def customMethodToString(x as Object, s as inout String) as bool # There's a nice article on speeding up dynamic dispatch at http://www.codeproject.com/KB/cs/fast_dynamic_properties.aspx # The technique below is likely slower than what's in that article. type = x.getType methodInfo as MethodInfo? if not _customMethodsCache.tryGetValue(type, out methodInfo) methodInfo = type.getMethod(_methodName) _customMethodsCache[type] = methodInfo if methodInfo try s = (methodInfo.invoke(x, nil) to String?) ? '' catch exc as TargetInvocationException # target invocation exceptions are themselves, not very interesting # their inner exception describes the problem throw exc.innerException to ! return true else return false def dictToString(dict as System.Collections.IDictionary) as String sb = StringBuilder() sb.append(.collectionTypeToString(dict.getType) + '{') sep = '' hasKeys = false for key in dict.keys sb.append(sep) sb.append(.makeString(key)) sb.append(': ') sb.append(.makeString(dict[key])) sep = ', ' hasKeys = true if not hasKeys, sb.append(':') sb.append('}') return sb.toString def enumToString(en as Enum) as String return en.toString def enumerableToString(en as System.Collections.IEnumerable) as String sb = StringBuilder() sb.append(.collectionTypeToString(en.getType) + r'[') sep = '' for item in en sb.append(sep) sb.append(.makeString(item)) sep = ', ' sb.append(']') return sb.toString def listToString(list as System.Collections.IList) as String sb = StringBuilder() sb.append(.collectionTypeToString(list.getType) + r'[') sep = '' for item in list sb.append(sep) sb.append(.makeString(item)) sep = ', ' sb.append(']') return sb.toString def nilToString as String return 'nil' def objectToString(obj as Object) as String return obj.toString def setToString(s as ISet) as String sb = StringBuilder() sb.append(.collectionTypeToString(s.getType) + '{') sep = '' hasItems = false for item in s sb.append(sep) sb.append(.makeString(item)) sep = ', ' hasItems = true if not hasItems, sb.append(',') sb.append('}') return sb.toString def stringToString(s as String) as String return s def charToCobraLiteral(c as char) as String return 'c' + .stringToCobraLiteral(c.toString) def stringToCobraLiteral(s as String) as String # TODO: not complete. and upon completing a single pass would be better s = s.replace('\n', '\\n') s = s.replace('\r', '\\r') s = s.replace('\t', '\\t') s = "'" + s + "'" # TODO: could be more sophisticated with respect to ' and " return s def stringMakerToString(sm as StringMaker) as String return '[sm.getType.name]([.stringToCobraLiteral(sm.toString)])' def testCases(cases as IList) is protected verbose = false if verbose print print .getType.name for case as IList in cases if verbose print case[0], '==> ' stop if case[1] inherits IList sep = '' for item in case[1] to IList print sep + item.toString stop sep = ' OR ' print else print case[1] input = case[0] answer = .makeString(input) if verbose, print ' ' + answer if case[1] inherits IList # when there is more than one expected, the test is that one of them is given for expected in case[1] to IList if answer == expected found = true break assert found, {'input': input, 'answer': answer, 'expected': case[1]} else expected = case[1] assert answer == expected # test formatting assert .makeString(6.0, 'N2') in ['6.00', '6,00'] class PrintStringMaker inherits StringMaker """ .toPrintString only quotes strings if they are inside collections such as a list or dictionary. This is like Python's str() behavior which has worked well in practice. """ def makePrintString(x as Object?) as String test cases = [ [nil, 'nil'], [true, 'true'], [false, 'false'], [c'x', 'x'], [5, '5'], ['aoeu', 'aoeu'], [[], ns'[]'], [[1, 2], ns'[1, 2]'], [[[1, 2], [3, 4]], ns'[[1, 2], [3, 4]]'], [['x', 'x'], ns"['x', 'x']"], [['x', 'x', 'x'], ns"['x', 'x', 'x']"], [{:}, '{:}'], [{'a': 1, 'b': 2}, ["{'a': 1, 'b': 2}", "{'b': 2, 'a': 1}"]], [{'x': [1, 2]}, ns"{'x': [1, 2]}"], [{,}, '{,}'], [{1, 2}, ['{1, 2}', '{2, 1}']], [{'aoeu', 'asdf'}, ["{'aoeu', 'asdf'}", "{'asdf', 'aoeu'}"]], [FB.Foo, 'Foo'], [FB.Bar, 'Bar'], [Object(), 'System.Object'], [TestDefaultToString(), 'Test Default To String'], [CustomToString(), '-- to print string --'], [CustomToString(), '-- to print string --'], [EnumerableToString(), ns'[1, 2, 3]'], [@[1, 2, 3], ns'[1, 2, 3]'], ] list = [] list.add(list) cases.add([list, ns'[(Repeated List)]']) innerList = [1] outerList = [innerList, innerList, innerList] cases.add([outerList, ns'[[1], (Repeated List), (Repeated List)]']) PrintStringMaker().testCases(cases) n = NestedToString() answer = r'[[1, 2, 3], [1, 2, 3]]' assert PrintStringMaker().makeString(n) == answer assert Cobra.Core.CobraCore.printStringMaker.makeString(n) == answer assert '[n]' == answer body return .makeString(x) cue init base.init _methodName = 'ToPrintString' def collectionTypeToString(t as Type) as String is override return '' def stringToString(s as String) as String is override if .level == 1 return s else return .stringToCobraLiteral(s) class TechStringMaker inherits StringMaker """ TechStringMaker stands for "technical string maker" and is used for inspection and debugging. It's the default string maker for the AssertException (and its subclasses such as RequireException) and the trace statement. It provides more information about collections and enumerations. It recovers from exceptions when making strings so that debugging can proceed. Idea: Could use a flag to control if _makeString recovers from exceptions. Idea: TechStringMaker should look for .toPrintString after looking for .toTechString. """ def makeTechString(x as Object?) as String test cases = [ [nil, 'nil'], [true, 'true'], [false, 'false'], [c'x', "c'x'"], [5, '5'], ['aoeu', "'aoeu'"], [[], ns'List[]'], [[1, 2], ns'List[1, 2]'], [[[1, 2], [3, 4]], ns'List>[List[1, 2], List[3, 4]]'], [['x', 'x'], ns"List['x', 'x']"], [['x', 'x', 'x'], ns"List['x', 'x', 'x']"], [{:}, 'Dictionary{:}'], [{'a': 1, 'b': 2}, ["Dictionary{'a': 1, 'b': 2}", "{'b': 2, 'a': 1}"]], [{'x': [1, 2]}, ns"Dictionary>{'x': List[1, 2]}"], [{,}, 'Set{,}'], [{1, 2}, ['Set{1, 2}', 'Set{2, 1}']], [{'aoeu', 'asdf'}, ["Set{'aoeu', 'asdf'}", "Set{'asdf', 'aoeu'}"]], [FB.Foo, 'FB.Foo enum'], [FB.Bar, 'FB.Bar enum'], [Object(), 'System.Object'], [TestDefaultToString(), 'Test Default To String (TestDefaultToString)'], [CustomToString(), '-- to tech string --'], [CustomToString(), '-- to tech string --'], [EnumerableToString(), ns'EnumerableToString[1, 2, 3]'], [@[1, 2, 3], ns'Int32[][1, 2, 3]'], ] list = [] list.add(list) cases.add([list, ns'List[(Repeated List)]']) innerList = [1] outerList = [innerList, innerList, innerList] cases.add([outerList, ns'List>[List[1], (Repeated List), (Repeated List)]']) TechStringMaker().testCases(cases) body return .makeString(x) cue init base.init _methodName = 'ToTechString' def _makeString(x as Object?) as String test s = TechStringMaker().makeString(BadToTechString()) assert s == '(Exception on a BadToTechString: InvalidOperationException: intentional exception in .toTechString)' body try return base._makeString(x) catch exc as Exception return '(Exception' + if(x, ' on a [x.getType.name]', '') + ': [exc.getType.name]: [exc.message])' def enumToString(en as Enum) as String is override # Example: 'Color.Red enum' # as opposed to just 'Red' return en.getType.name + '.' + en.toString + ' enum' def collectionTypeToString(t as Type) as String is override return CobraCore.typeName(t) def objectToString(obj as Object) as String is override # Examples: '5 (Int64)' 'System.Object' 'Void Compute (MethodInfo)' s = obj.toString if .isInterestingType(obj.getType) typeName = obj.getType.name if not typeName in s and not obj inherits CobraDirectString s = '[s] ([typeName])' return s def charToString(c as char) as String is override return .charToCobraLiteral(c) def stringToString(s as String) as String is override return .stringToCobraLiteral(s) def isInterestingType(t as Type) as bool test b = TechStringMaker() assert not b.isInterestingType(5.getType) assert not b.isInterestingType(c'a'.getType) assert not b.isInterestingType(true.getType) assert b.isInterestingType(IDisposable) body if t is Int32, return false if t is Char, return false if t is Boolean, return false return true