Repository: tevelee/Eval Branch: master Commit: df80bd880d56 Files: 98 Total size: 374.9 KB Directory structure: gitextract_vfh2ubbu/ ├── .codecov.yml ├── .github/ │ └── .config.yml ├── .gitignore ├── .jazzy.yml ├── .swift-version ├── .swiftlint.yml ├── .travis.yml ├── .version ├── CONTRIBUTING.md ├── Dangerfile ├── Documentation/ │ ├── Example projects.md │ ├── Interpreter engine details.md │ ├── Strongly-typed evaluator.md │ ├── Template evaluator.md │ └── Tips & Tricks.md ├── Eval.playground/ │ ├── Contents.swift │ ├── Sources/ │ │ ├── Helpers.swift │ │ └── TypesAndFunctions.swift │ ├── contents.xcplayground │ ├── playground.xcworkspace/ │ │ └── contents.xcworkspacedata │ └── xcshareddata/ │ └── xcschemes/ │ └── Playground.xcscheme ├── Eval.podspec ├── Eval.xcodeproj/ │ ├── EvalTests_Info.plist │ ├── Eval_Info.plist │ ├── project.pbxproj │ └── xcshareddata/ │ └── xcschemes/ │ ├── Eval-Package.xcscheme │ └── xcschememanagement.plist ├── Eval.xcworkspace/ │ ├── contents.xcworkspacedata │ └── xcshareddata/ │ └── IDEWorkspaceChecks.plist ├── Examples/ │ ├── .swiftlint.yml │ ├── AttributedStringExample/ │ │ ├── .gitignore │ │ ├── Package.swift │ │ ├── README.md │ │ ├── Sources/ │ │ │ └── AttributedStringExample/ │ │ │ └── TemplateExample.swift │ │ └── Tests/ │ │ ├── .swiftlint.yml │ │ ├── AttributedStringExampleTests/ │ │ │ └── AttributedStringExampleTests.swift │ │ └── LinuxMain.swift │ ├── ColorParserExample/ │ │ ├── .gitignore │ │ ├── Package.swift │ │ ├── README.md │ │ ├── Sources/ │ │ │ └── ColorParserExample/ │ │ │ └── ColorParserExample.swift │ │ └── Tests/ │ │ ├── .swiftlint.yml │ │ ├── ColorParserExampleTests/ │ │ │ └── ColorParserExampleTests.swift │ │ └── LinuxMain.swift │ └── TemplateExample/ │ ├── .gitignore │ ├── Package.swift │ ├── README.md │ ├── Sources/ │ │ └── TemplateExample/ │ │ └── TemplateExample.swift │ └── Tests/ │ ├── .swiftlint.yml │ ├── LinuxMain.swift │ └── TemplateExampleTests/ │ ├── TemplateExampleComponentTests.swift │ ├── TemplateExampleTests.swift │ ├── import.txt │ └── template.txt ├── Gemfile ├── LICENSE.txt ├── Package.swift ├── README.md ├── Scripts/ │ ├── .gitignore │ ├── .swiftlint.yml │ ├── Package.swift │ ├── Sources/ │ │ └── Automation/ │ │ ├── Error.swift │ │ ├── Eval.swift │ │ ├── Shell.swift │ │ ├── Travis.swift │ │ └── main.swift │ ├── ci.sh │ └── git_auth.sh ├── Sources/ │ └── Eval/ │ ├── Common.swift │ ├── Elements.swift │ ├── TemplateInterpreter.swift │ ├── TypedInterpreter.swift │ └── Utilities/ │ ├── MatchResult.swift │ ├── Matcher.swift │ ├── Pattern.swift │ └── Utils.swift ├── Tests/ │ ├── .swiftlint.yml │ ├── EvalTests/ │ │ ├── IntegrationTests/ │ │ │ ├── InterpreterTests.swift │ │ │ ├── PerformanceTest.swift │ │ │ ├── Suffix.swift │ │ │ └── TemplateTests.swift │ │ ├── UnitTests/ │ │ │ ├── DataTypeTests.swift │ │ │ ├── FunctionTests.swift │ │ │ ├── InterpreterContextTests.swift │ │ │ ├── KeywordTests.swift │ │ │ ├── LiteralTests.swift │ │ │ ├── MatchResultTests.swift │ │ │ ├── MatchStatementTests.swift │ │ │ ├── MatcherTests.swift │ │ │ ├── PatternTests.swift │ │ │ ├── TemplateInterpreterTests.swift │ │ │ ├── TypedInterpreterTests.swift │ │ │ ├── UtilTests.swift │ │ │ ├── VariableProcessor.swift │ │ │ └── VariableTests.swift │ │ └── Utils.swift │ └── LinuxMain.swift └── github_rsa.enc ================================================ FILE CONTENTS ================================================ ================================================ FILE: .codecov.yml ================================================ coverage: ignore: - Applications/Xcode.app/.* - build/.* - .build/.* - Documentation/.* - Example/Pods/.* - Scripts/.* - vendor/.* precision: 2 round: down range: "80...100" status: patch: yes project: yes changes: no ================================================ FILE: .github/.config.yml ================================================ updateDocsComment: > Thanks for opening this pull request! The maintainers of this repository would appreciate it if you would update some of our documentation based on your changes. requestInfoReplyComment: > We would appreciate it if you could provide us with more info about this issue/pr! requestInfoLabelToAdd: request-more-info newPRWelcomeComment: > Thanks so much for opening your first PR here! firstPRMergeComment: > Congrats on merging your first pull request here! :tada: How awesome! newIssueWelcomeComment: > Thanks for opening this issue, a maintainer will get back to you shortly! sentimentBotToxicityThreshold: .7 sentimentBotReplyComment: > Please be sure to review the code of conduct and be respectful of other users lockThreads: toxicityThreshold: .7 numComments: 2 setTimeInHours: 72 replyComment: > This thread is being locked due to exceeding the toxicity minimums ================================================ FILE: .gitignore ================================================ .DS_Store /.build build /Packages xcuserdata .xcuserstate Pods Package.resolved docs gh-pages Documentation/Output ================================================ FILE: .jazzy.yml ================================================ author: Laszlo Teveli author_url: https://tevelee.github.io readme: README.md documentation: Documentation/*.md abstract: Documentation/Sections/*.md module: Eval xcodebuild_arguments: [-scheme,Eval-Package] output: Documentation/Output theme: apple github_url: https://github.com/tevelee/Eval github_file_prefix: https://github.com/tevelee/Eval/tree/master root_url: https://tevelee.github.io/Eval clean: true min_acl: internal framework_root: Sources source_directory: . ================================================ FILE: .swift-version ================================================ 5.0 ================================================ FILE: .swiftlint.yml ================================================ reporter: "xcode" opt_in_rules: - array_init - attributes - block_based_kvo - class_delegate_protocol - closing_brace - closure_end_indentation - closure_parameter_position - closure_spacing - colon - comma - compiler_protocol_init # - conditional_returns_on_newline - contains_over_first_not_nil - control_statement - custom_rules - cyclomatic_complexity - discarded_notification_center_observer - discouraged_direct_init - discouraged_object_literal - dynamic_inline - empty_count - empty_enum_arguments - empty_parameters - empty_parentheses_with_trailing_closure # - explicit_acl - explicit_enum_raw_value - explicit_init - explicit_top_level_acl # - explicit_type_interface - extension_access_modifier - fallthrough - fatal_error_message - file_header - file_length - first_where - for_where - force_cast - force_try - force_unwrapping - function_body_length - function_parameter_count - generic_type_name - identifier_name - implicit_getter - implicit_return - implicitly_unwrapped_optional - is_disjoint - joined_default_parameter - large_tuple - leading_whitespace - legacy_cggeometry_functions - legacy_constant - legacy_constructor - legacy_nsgeometry_functions - let_var_whitespace - line_length - literal_expression_end_indentation - mark - multiline_arguments - multiline_parameters - multiple_closures_with_trailing_closure - nesting - nimble_operator # - no_extension_access_modifier # - no_grouping_extension - notification_center_detachment - number_separator - object_literal - opening_brace - operator_usage_whitespace - operator_whitespace - overridden_super_call - override_in_extension - pattern_matching_keywords - prefixed_toplevel_constant - private_action - private_outlet - private_over_fileprivate - private_unit_test - prohibited_super_call - protocol_property_accessors_order - quick_discouraged_call - quick_discouraged_focused_test - quick_discouraged_pending_test - redundant_discardable_let - redundant_nil_coalescing - redundant_optional_initialization - redundant_string_enum_value - redundant_void_return - required_enum_case - return_arrow_whitespace - shorthand_operator - single_test_class - sorted_first_last - sorted_imports - statement_position - strict_fileprivate - superfluous_disable_command - switch_case_alignment - switch_case_on_newline - syntactic_sugar - todo - trailing_closure - trailing_comma - trailing_newline - trailing_semicolon - trailing_whitespace - type_body_length - type_name - unneeded_break_in_switch - unneeded_parentheses_in_closure_argument - unused_closure_parameter - unused_enumerated - unused_optional_binding - valid_ibinspectable - vertical_parameter_alignment - vertical_parameter_alignment_on_call - vertical_whitespace - void_return - weak_delegate - xctfail_message - yoda_condition included: - Sources - Tests - Examples/AttributedStringExample/Sources - Examples/AttributedStringExample/Tests - Examples/ColorParserExample/Sources - Examples/ColorParserExample/Tests - Examples/TemplateExample/Sources - Examples/TemplateExample/Tests - Scripts/Sources force_cast: error force_try: error line_length: 320 type_body_length: - 300 - 400 file_length: warning: 500 error: 1000 type_name: min_length: 4 max_length: 25 identifier_name: min_length: 3 max_length: 35 file_header: severity: error required_string: | /* * Copyright (c) 2018 Laszlo Teveli. * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ ================================================ FILE: .travis.yml ================================================ osx_image: xcode11.3 language: swift sudo: true env: global: - EXPANDED_CODE_SIGN_IDENTITY="-" - EXPANDED_CODE_SIGN_IDENTITY_NAME="-" - EXPANDED_PROVISIONING_PROFILE="-" before_script: - sh Scripts/git_auth.sh script: - travis_retry Scripts/ci.sh - sleep 3 ================================================ FILE: .version ================================================ 1.5.0 ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Eval ## 👍🎉 First off, thanks for taking the time to contribute! 🎉👍 **You are more than welcomed to do so!** The following is a set of guidelines and best practices for contributing to Eval. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a Pull Request. ## Code of Conduct Nothing serious, all I ask is to be respectful and as helpful as you would expect others commnicating with you. ## I don't want to read this whole thing, I just have a question! The easiest channel is Twitter. You can reach out to me `@tevelee`, or feel free to write a mail at `tevelee [at] gmail [dot] com`. If you have bigger concerns, feel free to write a GitHub issue. ## What should I know before I get started? First of all, please read the [documentation pages](Documentation), and feel free to check out the examples. There are a few things you need to be aware of when contributing: * The repository is only opened to a very selected set of contributors. If you need any code modifications, you need to fork and create a Pull Request. * There is a CI up and running * I use SwiftLint with build-in rules to keep the code as nice as possible * There is Danger configured with some rules, auto-checking contributions before I do * I intend to keep the code test coverage as high as possible. Please be mindful about this when contributing # How Can I Contribute? ## Reporting Bugs or Suggesting Enhancements Feel free to use the same channels as I described in the README: The easiest channel is Twitter. You can reach out to me `@tevelee`, or feel free to write a mail at `tevelee [at] gmail [dot] com`. If you have bigger concerns, feel free to write a GitHub issue. ## Pull Requests ### Git Commit Messages Please use git rebase keep your number commits as low as possible. ### Documentation Styleguide I use a standard style of markdown pages in the [README.md](README.md) and in the [Documentation folder](Documentation). I also document the code, most importantly the public interfaces. I intend to keep the line documentation coverage of the publicly available methods and classes a 100%. ### Issue and Pull Request Labels No rules you need to be aware of ================================================ FILE: Dangerfile ================================================ # Sometimes it's a README fix, or something like that - which isn't relevant for # including in a project's CHANGELOG for example not_declared_trivial = !(github.pr_title.include? "#trivial") has_app_changes = !git.modified_files.grep(/Sources/).empty? # ENSURE THAT LABELS HAVE BEEN USED ON THE PR fail "Please add labels to this PR" if github.pr_labels.empty? # Mainly to encourage writing up some reasoning about the PR, rather than just leaving a title if github.pr_body.length < 5 fail "Please provide a summary in the Pull Request description" end # Pay extra attention if external contributors modify certain files if git.modified_files.include?("LICENSE.txt") fail "External contributor has edited the LICENSE.txt" end if git.modified_files.include?("Gemfile") or git.modified_files.include?("Gemfile.lock") warn "External contributor has edited the Gemfile and/or Gemfile.lock" end if git.modified_files.include?("Eval.podspec") or git.modified_files.include?("Package.swift") warn "External contributor has edited the Eval.podspec and/or Package.swift" end # Make it more obvious that a PR is a work in progress and shouldn't be merged yet warn("PR is classed as Work in Progress") if github.pr_title.include? "WIP" # Warn when there is a big PR warn("Big PR, try to keep changes smaller if you can") if git.lines_of_code > 500 # Changelog entries are required for changes to library files. no_changelog_entry = !git.modified_files.include?("Changelog.md") if has_app_changes && no_changelog_entry && not_declared_trivial #warn("Any changes to library code should be reflected in the Changelog. Please consider adding a note there") end # Added (or removed) library files need to be added (or removed) from the Carthage Xcode project to avoid breaking things for our Carthage users. added_swift_library_files = !(git.added_files.grep(/Sources.*\.swift/).empty?) deleted_swift_library_files = !(git.deleted_files.grep(/Sources.*\.swift/).empty?) modified_carthage_xcode_project = !(git.modified_files.grep(/Eval\.xcodeproj/).empty?) if (added_swift_library_files || deleted_swift_library_files) && !modified_carthage_xcode_project warn("Added or removed library files require the Carthage Xcode project to be updated") end missing_doc_changes = git.modified_files.grep(/Documentation/).empty? doc_changes_recommended = git.insertions > 15 if has_app_changes && missing_doc_changes && doc_changes_recommended && not_declared_trivial warn("Consider adding supporting documentation to this change. Documentation can be found in the `Documentation` directory.") end # Warn when library files has been updated but not tests. tests_updated = !git.modified_files.grep(/Tests/).empty? if has_app_changes && !tests_updated warn("The library files were changed, but the tests remained unmodified. Consider updating or adding to the tests to match the library changes.") end # Give inline build results (compile and link time warnings and errors) xcode_summary.report 'build/tests/summary.json' if File.file?('build/tests/summary.json') xcode_summary.report 'build/example/summary.json' if File.file?('build/example/summary.json') # Run SwiftLint swiftlint.lint_files #swiftlint.lint_files inline_mode: true ================================================ FILE: Documentation/Example projects.md ================================================ # Example projects I included a few use-cases, which bring significant improvements on how things are processed before - at least in my previous projects. ​ ### [Template language](https://github.com/tevelee/Eval/blob/master/Examples/TemplateExample/Tests/TemplateExampleTests/TemplateExampleTests.swift) I was able to create a full-blown template language, completely, using this framework and nothing else. It's almost like a competitor of the one I mentioned ([Twig](https://github.com/twigphp/Twig)). This is the most advanced example of them all! I created a standard library with all the possible operators you can imagine. With helpers, each operator is a small, one-liner addition. Added the important data types, such as arrays, strings, numbers, booleans, dates, etc., and a few functions, to be more awesome. [Take a look for inspiration!](https://github.com/tevelee/Eval/blob/master/Examples/TemplateExample/Sources/TemplateExample/TemplateExample.swift) Together, it makes an excellent addition to my model-object generation project, and **REALLY useful for server-side Swift development as well**! ### [Attributed string parser](https://github.com/tevelee/Eval/blob/master/Examples/AttributedStringExample/Tests/AttributedStringExampleTests/AttributedStringExampleTests.swift) I created another small example, parsing attribtuted strings from simple expressions using XML style tags, such as bold, italic, underlined, colored, etc. With just a few operators, this solution can deliver attributed strings from basic APIs, which otherwise would be hard to manage. My connected project is an iOS application, using the Spotify [HUB framework](https://github.com/spotify/HubFramework), in which I can now provide rich strings with my view-models and parse them from the JSON string results. ### [Color parser](https://github.com/tevelee/Eval/blob/master/Examples/ColorParserExample/Tests/ColorParserExampleTests/ColorParserExampleTests.swift) A color parser is also used by the BFF (Backend For Frontend, not 👭) project I mentioned before. It can parse Swift Color objects from many different styles of strings, such as `#ffddee`, or `red`, or `rgba(1,0.5,0.4,1)`. I included this basic example in the repository as well. ================================================ FILE: Documentation/Interpreter engine details.md ================================================ # Technical details ## Interpreter engine TBD ## Template engine TBD ================================================ FILE: Documentation/Strongly-typed evaluator.md ================================================ # Strongly typed evaluator This kind of evaluator interprets its input as one function. It searches for the one with the highest precedence and works its way down from that. Ocassionally, more functions are present in the same expression. In this case, it goes recursively, all the way down to the most basic elements: variables or literals, which are trivial to evaluate. ## Creation The way to create typed interpreters is the following: ```swift let interpreter = TypedInterpreter(dataTypes: [number, string, boolean, array, date], functions: [concat, add, multiply, substract, divide], context: Context(variables: ["example": 1])) ``` First, you'll need the data types you are going to work with. These are a smaller subsets of the build in Swift data types, you can map them to existing types (Swift types of the ones of your own) The second parameter are the functions you can apply on the above listed data types. All the functions - regardless of the grouping of the data types - should be listed here. Typically, in case of numbers, these are numeric operators. In case of string, these can be concatenation, getters, slicing, etc. Or, these can be complex things, such as parentheses, data factories, or high level functional operations, such as filter, map or reduce. And lastly, an optional context object, if you have any global variables. Variables in the context can be expression specific, or global that apply to every evaluation session. ## Data types Data types map the outside world to the inside of the expression. They map existing types to inner data types. They don't restrict any behaviour, so these types can either be built-in Swift types, such as String, Array, Date; or they can be your custom classes, srtucts, or enums. Let's see it in action: ```swift let number = DataType(type: Double.self, literals: [numberLiteral, piConstant]) { String(describing: $0) } ``` If has a type, that is the existing type of the sorrounding program. The literals, which can tell the framework whether a given string can be converted to a given type. Typical example is the `String` literal, which encloses something between quotes: `'like this'`. The can also be constants, for example `pi` for numbers of `true` for booleans. The last parameter is a `print` closure. It tells the framework how to render the given type when needed. Typically used while debugging, or when templates use the `print` statement. In summary: literals provide the input, print provides the output of a mapped type. ### Literals Let's check out some literals a bit more deeply. The block used for literals have two parameters: the input string, and an interpreter. Most of the times, only the input is enough to recognise things, like numbers: ```swift Literal { value, _ in Double(value) } ``` Arrays, on the other hand, should process their content (Comma `,` separated values between brackets `[` `]`). For this, the second parameter, the interpreter can be used. #### Constants Literals are the perfect place to recognise constants, such as: ```swift Literal("pi", convertsTo: Double.pi) ``` or ```swift [Literal("false", convertsTo: false) ``` Of course, there are multiple ways to represent them (for example, as a single keyword function pattern), but this seems like a place where they can be most closely connected to their type. The `convertsTo` parameter of `Literal`s are `autoclosure` parameters, which means, that they are going to be processed lazily. ```swift Literal("now", convertsTo: Date()) ``` The `now` string is going to be expressed as the current timestamp at the time of the evaluation, not the time of the initialisation. ## Functions Similarly to templates, typed interpreters use the same building blocks to build up their patterns: `Keyword`s and `Variable`s. ### Keywords `Keyword`s are the most basic elements of a pattern; they represent simple, static `String`s. You can chain them, for example `Keyword("<") + Keyword("br/") + Keyword(>}")`, or simply merge them `Keyword("
")`. Logically, these two are the same, but the former accepts any number of whitespaces between the tags, while the latter allows none, as it is a strict match. Most of the time though, you are going to need to handle placeholders, varying number of elements. That's where `Variable`s come into place. ### Variables Let's check out the following, really straightforward pattern: ```swift Function(Keyword("(") + Variable("body") + Keyword(")")) { variables, _, _ in return variables["body"] } ``` Something between two enclosing parentheses `(`, `)`. The middle tag is a `Variable`, which means that its value is going to be passed through in the block, using its name. Let's imagine the following input: `(5)`. Here, the `variables` dictionary is going to have `5` under the key `body`. #### Generics Since its value is going to be processed, there is a generic sign as well, signalling that this current `Variable` accepts `Any` kind of data, no transfer is needed. Let's imagine if we wrote `Variable` instead. In this case, `5` would not match to the pattern, it would be intact. But, for example, `('Hello')` would do. Let check out a `+` operator. This could equally mean addition for numeric types ```swift Function(Variable("lhs") + Keyword("+") + Variable("rhs")) { arguments,_,_ in guard let lhs = arguments["lhs"] as? Double, let rhs = arguments["rhs"] as? Double else { return nil } return lhs + rhs } ``` or concatenation for strings ```swift Function(Variable("lhs") + Keyword("+") + Variable("rhs")) { arguments,_,_ in guard let lhs = arguments["lhs"] as? String, let rhs = arguments["rhs"] as? String else { return nil } return lhs + rhs } ``` Since the interpreter is strongly typed, always the appropriate one is going to be selected by the framework. #### Evaluation Variables also have optional properties, such as `interpreted`, `shortest`, or `acceptsNilValue`. They might also have a `map` block, which by default is `nil`. * `interpreted` tells the framework, that its value should be evaluated. This is true, by default. But, the option exists to modify this to false. In that case, `(2 + 3)` would not generate the number `5` under the `body` key, but `2 + 3` as a `String`. * `shortest` signals the "strength" of the matching operation. By default it's false, we need the most possible characters. The only scenario where this could get tricky is if the last element of a pattern is a `Variable`. In that case, the preferred setting is `false`, so we need the largest possible match! Let's find out why! A general addition operator (which looks like this `Variable("lhs") + Keyword("+") + Variable("rhs")`) would recognise the pattern `12 + 34`, but it also matches to `12 + 3`. What's what shortest means, the shortest match, in this case, is `12 + 3`, which - semantically - is an incorrect match. But don't worry, the framework already knows about this, so it sets the right value for your variables, even in the last place! * `acceptsNilValue` informs the framework if `nil` should be accepted by the pattern. For example, `1 + '5'` with the previous example (`Double + Double`) would not match. But, if the `acceptsNilValue` is defined, then the block would trigger, with `{'lhs': 1, 'rhs': nil}`, so you can decide by your own logic what to do in this case. * Finally, the `map` block can be used to further transform the value of your `Variable` before calling the block on the `Pattern`. Since map is a trailing closure, it's quite easy to add. For example, `Variable("example") { Double($0) }` would recognise only `Int` values, but would transform them to `Double` instances when providing them in the `variables` dictionary. This map can also return `nil` values but depends on your logic if you want to accept them or not. Side note: the previous map generates a `Variable` kind of variable instance. ### Specialised elements #### Open & Close Keyword Parentheses are quite common in expressions. They are often embedded in each other. Embedding is a nasty problem of interpreters, as `(a * (b + c))` would logically be evaluated with `(b + c)` first, and the rest afterwards. But, an algorithm, by default, would interpret things linearly, disregarding the semantics: `(a * (b + c))` would be the match for the first if statement, with a totally invalid `a * (b + c` data, until the first match. This, of course, needs to be solved, but it's not that easy as it first looks! Some edge cases would not work unless we somehow try to connect them together. For this reason, I added two special elements: `OpenKeyword` and `CloseKeyword`. These work exactly the same way as normal `Keyword`s do, but add a bit more semantics to the framework: these two should be connected together, and therefore embedding them should not be a problem as they come in pairs. The previous parentheses statement should - correctly - look like this: ```swift Function(OpenVariable("lhs") + Keyword("+") + CloseVariable("rhs")) { arguments,_,_ in guard let lhs = arguments["lhs"] as? String, let rhs = arguments["rhs"] as? String else { return nil } return lhs + rhs } ``` By using the `OpenKeyword` and `CloseKeyword` types, these become connected, so embedding parentheses in an expression shouldn't be a problem. After this match is defined, they can be embedded in each other as deeply as needed. #### Multiple Patterns in one Function This is a rarely used pattern, but `Function`s consists of an array of `Pattern` elements. Usually, one `Function` does only one operation. Unless this is true, grouping multiple `Pattern`s into one `Function` allows semantical grouping of opeartors. For example a Boolean negation can be expressed in multiple ways: `not(true)` or `!true`. In this case, semantically both expressions do the same thing, therefore it might be a good practice to use one `Function` with two `Pattern`s for this. ## Context You can also pass contextual values, which - for now - equal to variables. ```swift expression.evaluate("1 + var", context: Context(variables: ["var": 2])) ``` The reason that the variables are encapsulated in a context is that context is a class, while variables are mutable `var` struct properties on that object. With this construction the context reference can be passed around to multiple interpreter instances, but keeps the copy-on-write (🐮) behaviour of the modification. Context defined during the initialisation apply to every evaluation performed with the given interpreter, while the ones passed to the `evaluate` method only apply to that specific expression instance. If some patterns modify the context, they have the option to modify the general context (for long term settings, such as `value++`), or the local one (for example, the interation variable of a `for` loop). ### Order of statements define precedence The earlier a pattern is represent in the array of `functions`, the higher precedence it gets. Practically, if there is an addition function and a multiplication one, the multiplication should be defined earlier (as it has higher precedence), because both are going to match the following expression: `1 * 2 + 3`, but if addition goes first, then the evaluation would process `1 * 2` on `lhs` and `3` on `rhs`, which - of course - is incorrect. Typically, parentheses and higher precedence operators should go earlier in the array. ================================================ FILE: Documentation/Template evaluator.md ================================================ # Template evaluator The logic of the interpreter is fairly easy: it goes over the input character by character and replaces any patterns it can find. ## Creation The way to create template interpreters is the following: ```swift let template = StringTemplateInterpreter(statements: [ifStatement, printStatement], interpreter: interpreter, context: Context(variables: ["example": 1])) ``` First, you'll need the statements that you aim to recognise. Then, you'll need a typed interpreter, so that you can evaluate strongly typed expressions. And lastly, an optional context object, if you have any global variables. Variables in the context can be expression specific, or global that apply to every evaluation session. The template interpreter and the given typed interpreter don't share the same context. Apart from the containment dependency, they don't have any logical connection. The reason for this is that templates need a special context feeding the template content, but typed interpreters might work with totally different data types. It is totally up to the developer how they want their context to be managed. Since the context is a class, its reference can be passed around, so it's quite straightforward to have them share the same context object - if needed. ## Statement examples ### Keywords `Keyword`s are the most basic elements of a pattern; they represent simple, static `String`s. You can chain them, for example `Keyword("{%") + Keyword("if") + Keyword("%}")`, or simply merge them `Keyword("{% if %}")`. Logically, these two are the same, but the former accepts any number of whitespaces between the tags, while the latter allows only one, as it is a strict match. Most of the time though, you are going to need to handle placeholders, varying number of elements. That's where `Variable`s come into place. ### Variables Let's check out the following, really straightforward pattern: ```swift Pattern(Keyword("{{") + Variable("body") + Keyword("}}")) { variables, interpreter, _ in guard let body = variables["body"] else { return nil } return interpreter.typedInterpreter.print(body) } ``` Something between two enclosing parentheses `{{`, `}}`. The middle tag is a `Variable`, which means that its value is going to be passed through in the block, using its name. Let's imagine the following input: `The winner is: {{ 5 }}`. Here, the `variables` dictionary is going to have `5` under the key `body`. #### Generics Since its value is going to be processed, there is a generic sign as well, signalling that this current `Variable` accepts `Any` kind of data, no transfer is needed. Let's imagine if we wrote `Variable` instead. In this case, `5` would not match to the template, it would be intact. But, for example, `{{ 'Hello' }}` would do. #### Evaluation Variables also have optional properties, such as `interpreted`, `shortest`, or `acceptsNilValue`. They might also have a `map` block, which by default is `nil`. * `interpreted` tells the framework, that its value should be evaluated. This is true, by default. But, the option exists to modify this to false. In that case, `{{ 2 + 3 }}` would not generate the number `5` under the `body` key, but `2 + 3` as a `String`. * `shortest` signals the "strength" of the matching operation. By default it's false, we need the most possible characters. The only scenario where this could get tricky is if the last element of a pattern is a `Variable`. In that case, the preferred setting is `false`, so we need the largest possible match! Let's find out why! A general addition operator (which looks like this `Variable("lhs") + Keyword("+") + Variable("rhs")`) would recognise the pattern `12 + 34`, but it also matches to `12 + 3`. What's what shortest means, the shortest match, in this case, is `12 + 3`, which - semantically - is an incorrect match. But don't worry, the framework already knows about this, so it sets the right value for your variables, even in the last place! * `acceptsNilValue` informs the framework if `nil` should be accepted by the pattern. For example, `1 + '5'` with the previous example (`Double + Double`) would not match. But, if the `acceptsNilValue` is defined, then the block would trigger, with `{'lhs': 1, 'rhs': nil}`, so you can decide by your own logic what to do in this case. * Finally, the `map` block can be used to further transform the value of your `Variable` before calling the block on the `Pattern`. Since map is a trailing closure, it's quite easy to add. For example, `Variable("example") { Double($0) }` would recognise only `Int` values, but would transform them to `Double` instances when providing them in the `variables` dictionary. This map can also return `nil` values but depends on your logic if you want to accept them or not. Side note: the previous map generates a `Variable` kind of variable instance. ### Specialised elements #### Template Variable By default, `Variable` instances use typed interpreters to evaluate their value. Sometimes though, they should be processed with the template interpreter. A good example is the `if` statement: ```swift Pattern(Keyword("{%") + Keyword("if") + Variable("condition") + Keyword("%}") + TemplateVariable("body") + Keyword("{% endif %}")) { variables, interpreter, _ in guard let condition = variables["condition"] as? Bool, let body = variables["body"] as? String else { return nil } if condition { return body } return nil } ``` This statement has two semantically different kinds of variable, but they both are just placeholders. The first (`condition`) is an interpreted variable, which at the end returns a `Boolean` value. The second one is a bit different; it should not be evaluated the same way as `condition`. We need to further evaluate the enclosed template, that's why this variable 1. Should not be interpreted 2. Should be evaluated using the template interpreter, not the typed interpreter That's why there's a subclass called `TemplateVariable`, which forces these two options when initialised. It DOES evaluate its content but uses the template interpreter to do so. A quick example: `Header ... {% if x > 0 %}Number of results: {{ x }} {% endif %} ... Footer` Here, `x > 0` is a `Boolean` expression, but the body between the `if`, and `endif` tags is a template, such as the whole expression. #### Open & Close Keyword `if` statements are quite common in templates. They are often chained and embedded in each other. Embedding is a nasty problem of interpreters, as `{% if %}a{% if %}b{% endif %}c{% endif %}` would logically be evaluated with `{% if %}b{% endif %}` first, and the rest afterwards. But, an algorithm, by default, would interpret things linearly, disregarding the semantics: `{% if %}a{% if %}b{% endif %}` would be the match for the first if statement, with a totally invalid `a{% if %}b` data. This, of course, needs to be solved, but it's not that easy as it first looks! Some edge cases would not work unless we somehow try to connect them together. For this reason, I added two special elements: `OpenKeyword` and `CloseKeyword`. These work exactly the same way as normal `Keyword`s do, but add a bit more semantics to the framework: these two should be connected together, and therefore embedding them should not be a problem as they come in pairs. The previous `if` statement, now with an `else` block should - correctly - look like this: ```swift Pattern(OpenKeyword("{% "if") + Variable("condition") + Keyword("%}") + TemplateVariable("body") + Keyword("{% else %}") + TemplateVariable("else") + CloseKeyword("{% endif %}")) { variables, interpreter, _ in guard let condition = variables["condition"] as? Bool, let body = variables["body"] as? String else { return nil } if condition { return body } else { return variables["else"] as? String } } ``` By using the `OpenKeyword` and `CloseKeyword` types, these become connected, so embedding `if` statements in a template shouldn't be a problem. Similarly, this works for the `print` statement from an earlier example: ```swift Pattern(OpenKeyword("{{") + Variable("body") + CloseKeyword("}}")) { variables, interpreter, _ in guard let body = variables["body"] else { return nil } return interpreter.typedInterpreter.print(body) } ``` ## Evaluation The evaluation of the templates happens with the `evaluate` function on the interpreter: ```swift template.evaluate("{{ 1 + 2 }}") ``` The result of the evaluation - in case of templates - is always a `String`. In the result you shouldn't see any template elements, because they were recognised, processed, and replaced during the evaluation by the interpreter. ### Context You can also pass contextual values, which - for now - equal to variables. ```swift template.evaluate("{{ 1 + var }}", context: Context(variables: ["var": 2])) ``` The reason that the variables are encapsulated in a context is that context is a class, while variables are mutable `var` struct properties on that object. With this construction the context reference can be passed around to multiple interpreter instances, but keeps the copy-on-write (🐮) behaviour of the modification. Context defined during the initialisation apply to every evaluation performed with the given interpreter, while the ones passed to the `evaluate` method only apply to that specific expression instance. If some patterns modify the context, they have the option to modify the general context (for long term settings), or the local one (for example, the interation variable of a `for` loop). ### Order of statements define precedence The earlier a pattern is represent in the array of `statements`, the higher precedence it gets. Practically, if there is an `if` statement and an `if-else` one, the `if-else` should be defined earlier, because both are going to match the following expression: `{% if x < 0 %}A{% else %}B{% endif %}`, but if `if` goes first, then the output - and the `body` of the `if` statement - is going to be processed as `A{% else %}B`. Typically, parentheses and richer type of expressions should go earlier in the array. ================================================ FILE: Documentation/Tips & Tricks.md ================================================ # Tips & Tricks The following sections provide handy Tips and Tricks to help you effectively build up your own interpreter using custom operators and data types. ## Get inspired by checking out the examples There are quite a few operators and data types available in the [TemplateLanguage Example](https://github.com/tevelee/Eval/blob/master/Examples/TemplateExample/Sources/TemplateExample/TemplateExample.swift#L114-L150) project, under the StandardLibrary class Also, there are quite a few expressions available [in some of the unit tests](https://github.com/tevelee/Eval/blob/master/Tests/EvalTests/IntegrationTests/InterpreterTests.swift#L47-L86) as well. ## Use helper functions to define operators It's a lot readable to define operators in a one-liner expression, rather than using long patterns: ```swift infixOperator("+") { (lhs: String, rhs: String) in lhs + rhs } ``` ```swift suffixOperator("is odd") { (value: Double) in Int(value) % 2 == 1 } ``` ```swift prefixOperator("!") { (value: Bool) in !value } ``` You can find a few helpers [in the examples](https://github.com/tevelee/Eval/tree/master/Examples/TemplateExample/Sources/TemplateExample/TemplateExample.swift#L331-L412). Feel free to use them! ## Be mindful about precedence #### Template expressions The earlier a pattern is represent in the array of `statements`, the higher precedence it gets. Practically, if there is an `if` statement and an `if-else` one, the `if-else` should be defined earlier, because both are going to match the following expression: `{% if x < 0 %}A{% else %}B{% endif %}`, but if `if` goes first, then the output - and the `body` of the `if` statement - is going to be processed as `A{% else %}B`. Typically, parentheses and richer type of expressions should go earlier in the array. #### Typed expressions The earlier a pattern is represent in the array of `functions`, the higher precedence it gets. Practically, if there is an addition function and a multiplication one, the multiplication should be defined earlier (as it has higher precedence), because both are going to match the following expression: `1 * 2 + 3`, but if addition goes first, then the evaluation would process `1 * 2` on `lhs` and `3` on `rhs`, which - of course - is incorrect. Typically, parentheses and higher precedence operators should go earlier in the array. ## Use Any for generics: `Variable` If you are not sure about the allowed input type of your expressions, or you just want to defer that decision until your match is ran and your hit the block in the pattern, feel free to use `Variable("name")` in your patterns. It makes life a lot easier, than definig functions for each type. ## Use map on `Variable`s for pre-filtering Before processing Variable values, there is an option to pre-filter or modify them before it hits the match block. Examples include data type conversion and other types of validation. ## Use `OpenKeyword` and `CloseKeyword` for embedding parentheses Embedding is a common issue with interpreters and compilers. In order to provide some extra semantics to the engine, please use the `OpenKeyword("[")` and `OpenKeyword("]")` options, when defining `Keyword`s that come in pairs. ## Share context between `StringTemplateInterpreter` and `TypedInterpreter` If you use template interpreters, they need a typed interpreter to hold. Both interpreters have `context` variables, so if you are not being careful enough, it can cause headaches. Since `Context` is a class, its reference can be passed around and used in multiple places. The reason that the variables are encapsulated in a context is that context is a class, while variables are mutable `var` struct properties on that object. With this construction the context reference can be passed around to multiple interpreter instances, but keeps the copy-on-write (🐮) behaviour of the modification. Context defined during the initialisation apply to every evaluation performed with the given interpreter, while the ones passed to the `evaluate` method only apply to that specific expression instance. ## Define constants in `Literal`s The frameworks allows multiple ways to express static strings and convert them. I believe the best place to put constants are in the `Literal`s of `DataType`s. Use the `Literal("YES", convertsTo: true)` `Literal` initialiser for easy definition. The `convertsTo` parameter of `Literal`s are `autoclosure` parameters, which means, that they are going to be processed lazily. ```swift Literal("now", convertsTo: Date()) ``` The `now` string is going to be expressed as the current timestamp at the time of the evaluation, not the time of the initialisation. ## Map any function signatures from Swift, dynamically The framework is really lightweight and not really restrictive in regards of how to parse your expressions. Free your mind, and do stuff dynamically. ```swift Function(Variable("lhs") + Keyword(".") + Variable("rhs", interpreted: false)) { (arguments,_,_) -> Double? in if let lhs = arguments["lhs"] as? NSObjectProtocol, let rhs = arguments["rhs"] as? String, let result = lhs.perform(Selector(rhs)) { return Double(Int(bitPattern: result.toOpaque())) } return nil } ]) ``` Perform any method call of any type and maybe process their output as well. It's not the safest way to go with it, but this is just an example. This opens up the way of running almost any arbitrary code on Apple platforms, from any backend. But, this does it in a very controlled way, as you must define a set of data types and functions that apply, unless you call them dynamically at runtime. ## Experiment with your expressions! It's quite easy to add new operators, functions, and data types. I suggest not to think about them too long, just dare to experpiment with them, what's possible and what is not. You can always add new types or functions if you need extra functionality. The options are practically endless! ## Debugging tips #### If an expression haven't been matched * It's common, that some validation caught the value * Print your expressions or put breakpoints into the affected match blocks or variable map blocks #### If you see weird output * Play with the order of the newly added opeartions. * Incorrect precedence can turn expressions upside down The framework is still in an early stage, so debugging helpers will follow in upcoming releases. Please stay tuned! ## Validate your expressions before putting them out in production code Not every expression work out of the box as you might expect. Operators and functions depend on each other, especially in terms of precedence. If one pattern was recognised before the other one, your code might not run as you expected. Pro Tip: Write unit tests to validate expressions. Feel free to use `as!` operator to force-cast the result expressions in tests, but only in tests. It's not a problem is tests crash, you can fix it right away, but it's not okay in production. ================================================ FILE: Eval.playground/Contents.swift ================================================ //: Playground - noun: a place where people can play import Foundation import Eval let context = InterpreterContext() let interpreter = TypedInterpreter(dataTypes: [numberDataType, stringDataType, arrayDataType, booleanDataType, dateDataType], functions: [parentheses, multipication, addition, lessThan], context: context) let template = TemplateInterpreter(statements: [ifStatement, printStatement], interpreter: interpreter, context: context) interpreter.evaluate("2 + 3 * 4") template.evaluate("{% if 10 < 21 %}Hello{% endif %} {{ name }}!", context: InterpreterContext(variables: ["name": "Eval"])) ================================================ FILE: Eval.playground/Sources/Helpers.swift ================================================ import Foundation import Eval public func infixOperator(_ symbol: String, body: @escaping (A, B) -> T) -> Function { return Function([Variable("lhs", shortest: true), Keyword(symbol), Variable("rhs", shortest: false)]) { arguments,_,_ in guard let lhs = arguments["lhs"] as? A, let rhs = arguments["rhs"] as? B else { return nil } return body(lhs, rhs) } } public func prefixOperator(_ symbol: String, body: @escaping (A) -> T) -> Function { return Function([Keyword(symbol), Variable("value", shortest: false)]) { arguments,_,_ in guard let value = arguments["value"] as? A else { return nil } return body(value) } } public func suffixOperator(_ symbol: String, body: @escaping (A) -> T) -> Function { return Function([Variable("value", shortest: true), Keyword(symbol)]) { arguments,_,_ in guard let value = arguments["value"] as? A else { return nil } return body(value) } } public func function(_ name: String, body: @escaping ([Any]) -> T?) -> Function { return Function([Keyword(name), Keyword("("), Variable("arguments", shortest: true, interpreted: false), Keyword(")")]) { variables, interpreter, _ in guard let arguments = variables["arguments"] as? String else { return nil } let interpretedArguments = arguments.split(separator: ",").flatMap { interpreter.evaluate(String($0).trimmingCharacters(in: .whitespacesAndNewlines)) } return body(interpretedArguments) } } public func functionWithNamedParameters(_ name: String, body: @escaping ([String: Any]) -> T?) -> Function { return Function([Keyword(name), Keyword("("), Variable("arguments", shortest: true, interpreted: false), Keyword(")")]) { variables, interpreter, _ in guard let arguments = variables["arguments"] as? String else { return nil } var interpretedArguments: [String: Any] = [:] for argument in arguments.split(separator: ",") { let parts = String(argument).trimmingCharacters(in: .whitespacesAndNewlines).split(separator: "=") if let key = parts.first, let value = parts.last { interpretedArguments[String(key)] = interpreter.evaluate(String(value)) } } return body(interpretedArguments) } } public func objectFunction(_ name: String, body: @escaping (O) -> T?) -> Function { return Function([Variable("lhs", shortest: true), Keyword("."), Variable("rhs", shortest: false, interpreted: false) { value,_ in guard let value = value as? String, value == name else { return nil } return value }]) { variables, interpreter, _ in guard let object = variables["lhs"] as? O, variables["rhs"] != nil else { return nil } return body(object) } } public func objectFunctionWithParameters(_ name: String, body: @escaping (O, [Any]) -> T?) -> Function { return Function([Variable("lhs", shortest: true), Keyword("."), Variable("rhs", interpreted: false) { value,_ in guard let value = value as? String, value == name else { return nil } return value }, Keyword("("), Variable("arguments", interpreted: false), Keyword(")")]) { variables, interpreter, _ in guard let object = variables["lhs"] as? O, variables["rhs"] != nil, let arguments = variables["arguments"] as? String else { return nil } let interpretedArguments = arguments.split(separator: ",").flatMap { interpreter.evaluate(String($0).trimmingCharacters(in: .whitespacesAndNewlines)) } return body(object, interpretedArguments) } } public func objectFunctionWithNamedParameters(_ name: String, body: @escaping (O, [String: Any]) -> T?) -> Function { return Function([Variable("lhs", shortest: true), Keyword("."), Variable("rhs", interpreted: false) { value,_ in guard let value = value as? String, value == name else { return nil } return value }, Keyword("("), Variable("arguments", interpreted: false), Keyword(")")]) { variables, interpreter, _ in guard let object = variables["lhs"] as? O, variables["rhs"] != nil, let arguments = variables["arguments"] as? String else { return nil } var interpretedArguments: [String: Any] = [:] for argument in arguments.split(separator: ",") { let parts = String(argument).trimmingCharacters(in: .whitespacesAndNewlines).split(separator: "=") if let key = parts.first, let value = parts.last { interpretedArguments[String(key)] = interpreter.evaluate(String(value)) } } return body(object, interpretedArguments) } } ================================================ FILE: Eval.playground/Sources/TypesAndFunctions.swift ================================================ import Foundation import Eval //MARK: Double public let numberDataType = DataType(type: Double.self, literals:[ Literal { Double($0.value) }, Literal("pi", convertsTo: Double.pi) ]) { value, _ in String(describing: value) } //MARK: Bool public let booleanDataType = DataType(type: Bool.self, literals: [ Literal("false", convertsTo: false), Literal("true", convertsTo: true) ]) { $0.value ? "true" : "false" } //MARK: String let singleQuotesLiteral = Literal { (input, _) -> String? in guard let first = input.first, let last = input.last, first == last, first == "'" else { return nil } let trimmed = input.trimmingCharacters(in: CharacterSet(charactersIn: "'")) return trimmed.contains("'") ? nil : trimmed } public let stringDataType = DataType(type: String.self, literals: [singleQuotesLiteral]) { $0.value } //MARK: Date public let dateDataType = DataType(type: Date.self, literals: [Literal("now", convertsTo: Date())]) { let dateFormatter = DateFormatter() dateFormatter.calendar = Calendar(identifier: .gregorian) dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" return dateFormatter.string(from: $0) } //MARK: Array let arrayLiteral = Literal { (input, interpreter) -> [CustomStringConvertible]? in guard let first = input.first, let last = input.last, first == "[", last == "]" else { return nil } return input .trimmingCharacters(in: CharacterSet(charactersIn: "[]")) .split(separator: ",") .map{ $0.trimmingCharacters(in: .whitespacesAndNewlines) } .map{ interpreter.evaluate(String($0)) as? CustomStringConvertible ?? String($0) } } public let arrayDataType = DataType(type: [CustomStringConvertible].self, literals: [arrayLiteral]) { $0.value.map{ $0.description }.joined(separator: ",") } //MARK: Operators public let max = objectFunction("max") { (object: [Double]) -> Double? in object.max() } public let min = objectFunction("min") { (object: [Double]) -> Double? in object.min() } public let formatDate = objectFunctionWithParameters("format") { (object: Date, arguments: [Any]) -> String? in guard let format = arguments.first as? String else { return nil } let dateFormatter = DateFormatter() dateFormatter.calendar = Calendar(identifier: .gregorian) dateFormatter.dateFormat = format return dateFormatter.string(from: object) } public let not = prefixOperator("!") { (value: Bool) in !value } public let dateFactory = function("Date") { (arguments: [Any]) -> Date? in guard let arguments = arguments as? [Double], arguments.count >= 3 else { return nil } var components = DateComponents() components.calendar = Calendar(identifier: .gregorian) components.year = Int(arguments[0]) components.month = Int(arguments[1]) components.day = Int(arguments[2]) components.hour = arguments.count > 3 ? Int(arguments[3]) : 0 components.minute = arguments.count > 4 ? Int(arguments[4]) : 0 components.second = arguments.count > 5 ? Int(arguments[5]) : 0 return components.date } public let parentheses = Function([Keyword("("), Variable("body"), Keyword(")")]) { $0.variables["body"] } public let addition = infixOperator("+") { (lhs: Double, rhs: Double) in lhs + rhs } public let multipication = infixOperator("*") { (lhs: Double, rhs: Double) in lhs * rhs } public let concat = infixOperator("+") { (lhs: String, rhs: String) in lhs + rhs } public let inNumberArray = infixOperator("in") { (lhs: Double, rhs: [Double]) in rhs.contains(lhs) } public let inStringArray = infixOperator("in") { (lhs: String, rhs: [String]) in rhs.contains(lhs) } public let range = infixOperator("...") { (lhs: Double, rhs: Double) in CountableClosedRange(uncheckedBounds: (lower: Int(lhs), upper: Int(rhs))).map { Double($0) } } public let prefix = infixOperator("starts with") { (lhs: String, rhs: String) in lhs.hasPrefix(lhs) } public let isOdd = suffixOperator("is odd") { (value: Double) in Int(value) % 2 == 1 } public let isEven = suffixOperator("is even") { (value: Double) in Int(value) % 2 == 0 } public let lessThan = infixOperator("<") { (lhs: Double, rhs: Double) in lhs < rhs } public let greaterThan = infixOperator(">") { (lhs: Double, rhs: Double) in lhs > rhs } public let equals = infixOperator("==") { (lhs: Double, rhs: Double) in lhs == rhs } //MARK: Template elements public let ifStatement = Matcher([Keyword("{%"), Keyword("if"), Variable("condition"), Keyword("%}"), TemplateVariable("body"), Keyword("{%"), Keyword("endif"), Keyword("%}")]) { (variables, interpreter: StringTemplateInterpreter, _) -> String? in guard let condition = variables["condition"] as? Bool, let body = variables["body"] as? String else { return nil } if condition { return body } return nil } public let printStatement = Matcher([Keyword("{{"), Variable("body"), Keyword("}}")]) { (variables, interpreter: StringTemplateInterpreter, _) -> String? in guard let body = variables["body"] else { return nil } return interpreter.typedInterpreter.print(body) } ================================================ FILE: Eval.playground/contents.xcplayground ================================================ ================================================ FILE: Eval.playground/playground.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Eval.playground/xcshareddata/xcschemes/Playground.xcscheme ================================================ ================================================ FILE: Eval.podspec ================================================ Pod::Spec.new do |s| s.name = "Eval" s.version = "1.5.0" s.summary = "Eval is a lightweight interpreter framework written in Swift, evaluating expressions at runtime" s.description = <<-DESC Eval is a lightweight interpreter framework written in Swift, for 📱iOS, 🖥 macOS, and 🐧Linux platforms. It evaluates expressions at runtime, with operators and data types you define. DESC s.homepage = "https://tevelee.github.io/Eval/" s.license = { :type => "Apache 2.0", :file => "LICENSE.txt" } s.author = { "Laszlo Teveli" => "tevelee@gmail.com" } s.social_media_url = "http://twitter.com/tevelee" s.source = { :git => "https://github.com/tevelee/Eval.git", :tag => "#{s.version}" } s.source_files = "Sources/**/*.{h,swift}" s.ios.deployment_target = "8.0" s.osx.deployment_target = "10.10" s.watchos.deployment_target = "2.0" s.tvos.deployment_target = "9.0" end ================================================ FILE: Eval.xcodeproj/EvalTests_Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass ================================================ FILE: Eval.xcodeproj/Eval_Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass ================================================ FILE: Eval.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXAggregateTarget section */ "Eval::EvalPackageTests::ProductTarget" /* EvalPackageTests */ = { isa = PBXAggregateTarget; buildConfigurationList = OBJ_67 /* Build configuration list for PBXAggregateTarget "EvalPackageTests" */; buildPhases = ( ); dependencies = ( OBJ_70 /* PBXTargetDependency */, ); name = EvalPackageTests; productName = EvalPackageTests; }; /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ 3AED32EA232D382D00FA2596 /* PerformanceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AED32E9232D382D00FA2596 /* PerformanceTest.swift */; }; 3AED32ED232D3D4D00FA2596 /* Suffix.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AED32EB232D3D3500FA2596 /* Suffix.swift */; }; OBJ_51 /* Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* Common.swift */; }; OBJ_52 /* Elements.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* Elements.swift */; }; OBJ_53 /* TemplateInterpreter.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* TemplateInterpreter.swift */; }; OBJ_54 /* TypedInterpreter.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* TypedInterpreter.swift */; }; OBJ_55 /* MatchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_14 /* MatchResult.swift */; }; OBJ_56 /* Matcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_15 /* Matcher.swift */; }; OBJ_57 /* Pattern.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_16 /* Pattern.swift */; }; OBJ_58 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_17 /* Utils.swift */; }; OBJ_65 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; }; OBJ_76 /* InterpreterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_21 /* InterpreterTests.swift */; }; OBJ_77 /* TemplateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_22 /* TemplateTests.swift */; }; OBJ_78 /* DataTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_24 /* DataTypeTests.swift */; }; OBJ_79 /* FunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_25 /* FunctionTests.swift */; }; OBJ_80 /* InterpreterContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_26 /* InterpreterContextTests.swift */; }; OBJ_81 /* KeywordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_27 /* KeywordTests.swift */; }; OBJ_82 /* LiteralTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_28 /* LiteralTests.swift */; }; OBJ_83 /* MatchResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_29 /* MatchResultTests.swift */; }; OBJ_84 /* MatchStatementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_30 /* MatchStatementTests.swift */; }; OBJ_85 /* MatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_31 /* MatcherTests.swift */; }; OBJ_86 /* PatternTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_32 /* PatternTests.swift */; }; OBJ_87 /* TemplateInterpreterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_33 /* TemplateInterpreterTests.swift */; }; OBJ_88 /* TypedInterpreterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_34 /* TypedInterpreterTests.swift */; }; OBJ_89 /* UtilTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_35 /* UtilTests.swift */; }; OBJ_90 /* VariableProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_36 /* VariableProcessor.swift */; }; OBJ_91 /* VariableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_37 /* VariableTests.swift */; }; OBJ_92 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_38 /* Utils.swift */; }; OBJ_94 /* Eval.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "Eval::Eval::Product" /* Eval.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 3A0B80062294367A008925A6 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = OBJ_1 /* Project object */; proxyType = 1; remoteGlobalIDString = "Eval::Eval"; remoteInfo = Eval; }; 3A0B80072294367B008925A6 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = OBJ_1 /* Project object */; proxyType = 1; remoteGlobalIDString = "Eval::EvalTests"; remoteInfo = EvalTests; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ 3AED32E9232D382D00FA2596 /* PerformanceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformanceTest.swift; sourceTree = ""; }; 3AED32EB232D3D3500FA2596 /* Suffix.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Suffix.swift; sourceTree = ""; }; "Eval::Eval::Product" /* Eval.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Eval.framework; sourceTree = BUILT_PRODUCTS_DIR; }; "Eval::EvalTests::Product" /* EvalTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; path = EvalTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; OBJ_10 /* Elements.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Elements.swift; sourceTree = ""; }; OBJ_11 /* TemplateInterpreter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateInterpreter.swift; sourceTree = ""; }; OBJ_12 /* TypedInterpreter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedInterpreter.swift; sourceTree = ""; }; OBJ_14 /* MatchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchResult.swift; sourceTree = ""; }; OBJ_15 /* Matcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Matcher.swift; sourceTree = ""; }; OBJ_16 /* Pattern.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pattern.swift; sourceTree = ""; }; OBJ_17 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; OBJ_21 /* InterpreterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpreterTests.swift; sourceTree = ""; }; OBJ_22 /* TemplateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateTests.swift; sourceTree = ""; }; OBJ_24 /* DataTypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataTypeTests.swift; sourceTree = ""; }; OBJ_25 /* FunctionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FunctionTests.swift; sourceTree = ""; }; OBJ_26 /* InterpreterContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpreterContextTests.swift; sourceTree = ""; }; OBJ_27 /* KeywordTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordTests.swift; sourceTree = ""; }; OBJ_28 /* LiteralTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiteralTests.swift; sourceTree = ""; }; OBJ_29 /* MatchResultTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchResultTests.swift; sourceTree = ""; }; OBJ_30 /* MatchStatementTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchStatementTests.swift; sourceTree = ""; }; OBJ_31 /* MatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatcherTests.swift; sourceTree = ""; }; OBJ_32 /* PatternTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatternTests.swift; sourceTree = ""; }; OBJ_33 /* TemplateInterpreterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateInterpreterTests.swift; sourceTree = ""; }; OBJ_34 /* TypedInterpreterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedInterpreterTests.swift; sourceTree = ""; }; OBJ_35 /* UtilTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilTests.swift; sourceTree = ""; }; OBJ_36 /* VariableProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VariableProcessor.swift; sourceTree = ""; }; OBJ_37 /* VariableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VariableTests.swift; sourceTree = ""; }; OBJ_38 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; OBJ_39 /* Documentation */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Documentation; sourceTree = SOURCE_ROOT; }; OBJ_40 /* Eval.xcworkspace */ = {isa = PBXFileReference; lastKnownFileType = wrapper.workspace; path = Eval.xcworkspace; sourceTree = SOURCE_ROOT; }; OBJ_41 /* Examples */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Examples; sourceTree = SOURCE_ROOT; }; OBJ_42 /* Scripts */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Scripts; sourceTree = SOURCE_ROOT; }; OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; OBJ_9 /* Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Common.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ OBJ_59 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 0; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; OBJ_93 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 0; files = ( OBJ_94 /* Eval.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ OBJ_13 /* Utilities */ = { isa = PBXGroup; children = ( OBJ_14 /* MatchResult.swift */, OBJ_15 /* Matcher.swift */, OBJ_16 /* Pattern.swift */, OBJ_17 /* Utils.swift */, ); path = Utilities; sourceTree = ""; }; OBJ_18 /* Tests */ = { isa = PBXGroup; children = ( OBJ_19 /* EvalTests */, ); name = Tests; sourceTree = SOURCE_ROOT; }; OBJ_19 /* EvalTests */ = { isa = PBXGroup; children = ( OBJ_20 /* IntegrationTests */, OBJ_23 /* UnitTests */, OBJ_38 /* Utils.swift */, ); name = EvalTests; path = Tests/EvalTests; sourceTree = SOURCE_ROOT; }; OBJ_20 /* IntegrationTests */ = { isa = PBXGroup; children = ( OBJ_21 /* InterpreterTests.swift */, OBJ_22 /* TemplateTests.swift */, 3AED32E9232D382D00FA2596 /* PerformanceTest.swift */, 3AED32EB232D3D3500FA2596 /* Suffix.swift */, ); path = IntegrationTests; sourceTree = ""; }; OBJ_23 /* UnitTests */ = { isa = PBXGroup; children = ( OBJ_24 /* DataTypeTests.swift */, OBJ_25 /* FunctionTests.swift */, OBJ_26 /* InterpreterContextTests.swift */, OBJ_27 /* KeywordTests.swift */, OBJ_28 /* LiteralTests.swift */, OBJ_29 /* MatchResultTests.swift */, OBJ_30 /* MatchStatementTests.swift */, OBJ_31 /* MatcherTests.swift */, OBJ_32 /* PatternTests.swift */, OBJ_33 /* TemplateInterpreterTests.swift */, OBJ_34 /* TypedInterpreterTests.swift */, OBJ_35 /* UtilTests.swift */, OBJ_36 /* VariableProcessor.swift */, OBJ_37 /* VariableTests.swift */, ); path = UnitTests; sourceTree = ""; }; OBJ_43 /* Products */ = { isa = PBXGroup; children = ( "Eval::EvalTests::Product" /* EvalTests.xctest */, "Eval::Eval::Product" /* Eval.framework */, ); name = Products; sourceTree = BUILT_PRODUCTS_DIR; }; OBJ_5 = { isa = PBXGroup; children = ( OBJ_6 /* Package.swift */, OBJ_7 /* Sources */, OBJ_18 /* Tests */, OBJ_39 /* Documentation */, OBJ_40 /* Eval.xcworkspace */, OBJ_41 /* Examples */, OBJ_42 /* Scripts */, OBJ_43 /* Products */, ); sourceTree = ""; }; OBJ_7 /* Sources */ = { isa = PBXGroup; children = ( OBJ_8 /* Eval */, ); name = Sources; sourceTree = SOURCE_ROOT; }; OBJ_8 /* Eval */ = { isa = PBXGroup; children = ( OBJ_9 /* Common.swift */, OBJ_10 /* Elements.swift */, OBJ_11 /* TemplateInterpreter.swift */, OBJ_12 /* TypedInterpreter.swift */, OBJ_13 /* Utilities */, ); name = Eval; path = Sources/Eval; sourceTree = SOURCE_ROOT; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ "Eval::Eval" /* Eval */ = { isa = PBXNativeTarget; buildConfigurationList = OBJ_47 /* Build configuration list for PBXNativeTarget "Eval" */; buildPhases = ( OBJ_50 /* Sources */, OBJ_59 /* Frameworks */, ); buildRules = ( ); dependencies = ( ); name = Eval; productName = Eval; productReference = "Eval::Eval::Product" /* Eval.framework */; productType = "com.apple.product-type.framework"; }; "Eval::EvalTests" /* EvalTests */ = { isa = PBXNativeTarget; buildConfigurationList = OBJ_72 /* Build configuration list for PBXNativeTarget "EvalTests" */; buildPhases = ( OBJ_75 /* Sources */, OBJ_93 /* Frameworks */, ); buildRules = ( ); dependencies = ( OBJ_95 /* PBXTargetDependency */, ); name = EvalTests; productName = EvalTests; productReference = "Eval::EvalTests::Product" /* EvalTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; "Eval::SwiftPMPackageDescription" /* EvalPackageDescription */ = { isa = PBXNativeTarget; buildConfigurationList = OBJ_61 /* Build configuration list for PBXNativeTarget "EvalPackageDescription" */; buildPhases = ( OBJ_64 /* Sources */, ); buildRules = ( ); dependencies = ( ); name = EvalPackageDescription; productName = EvalPackageDescription; productType = "com.apple.product-type.framework"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ OBJ_1 /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 1020; TargetAttributes = { "Eval::EvalTests" = { LastSwiftMigration = 1020; }; }; }; buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "Eval" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = OBJ_5; productRefGroup = OBJ_43 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( "Eval::Eval" /* Eval */, "Eval::SwiftPMPackageDescription" /* EvalPackageDescription */, "Eval::EvalPackageTests::ProductTarget" /* EvalPackageTests */, "Eval::EvalTests" /* EvalTests */, ); }; /* End PBXProject section */ /* Begin PBXSourcesBuildPhase section */ OBJ_50 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 0; files = ( OBJ_51 /* Common.swift in Sources */, OBJ_52 /* Elements.swift in Sources */, OBJ_53 /* TemplateInterpreter.swift in Sources */, OBJ_54 /* TypedInterpreter.swift in Sources */, OBJ_55 /* MatchResult.swift in Sources */, OBJ_56 /* Matcher.swift in Sources */, OBJ_57 /* Pattern.swift in Sources */, OBJ_58 /* Utils.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; OBJ_64 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 0; files = ( OBJ_65 /* Package.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; OBJ_75 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 0; files = ( OBJ_76 /* InterpreterTests.swift in Sources */, OBJ_77 /* TemplateTests.swift in Sources */, OBJ_78 /* DataTypeTests.swift in Sources */, 3AED32ED232D3D4D00FA2596 /* Suffix.swift in Sources */, OBJ_79 /* FunctionTests.swift in Sources */, OBJ_80 /* InterpreterContextTests.swift in Sources */, OBJ_81 /* KeywordTests.swift in Sources */, 3AED32EA232D382D00FA2596 /* PerformanceTest.swift in Sources */, OBJ_82 /* LiteralTests.swift in Sources */, OBJ_83 /* MatchResultTests.swift in Sources */, OBJ_84 /* MatchStatementTests.swift in Sources */, OBJ_85 /* MatcherTests.swift in Sources */, OBJ_86 /* PatternTests.swift in Sources */, OBJ_87 /* TemplateInterpreterTests.swift in Sources */, OBJ_88 /* TypedInterpreterTests.swift in Sources */, OBJ_89 /* UtilTests.swift in Sources */, OBJ_90 /* VariableProcessor.swift in Sources */, OBJ_91 /* VariableTests.swift in Sources */, OBJ_92 /* Utils.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ OBJ_70 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = "Eval::EvalTests" /* EvalTests */; targetProxy = 3A0B80072294367B008925A6 /* PBXContainerItemProxy */; }; OBJ_95 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = "Eval::Eval" /* Eval */; targetProxy = 3A0B80062294367A008925A6 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ OBJ_3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_NS_ASSERTIONS = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.10; ONLY_ACTIVE_ARCH = YES; OTHER_SWIFT_FLAGS = "-DXcode"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "SWIFT_PACKAGE DEBUG"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; USE_HEADERMAP = NO; }; name = Debug; }; OBJ_4 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = s; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.10; OTHER_SWIFT_FLAGS = "-DXcode"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; USE_HEADERMAP = NO; }; name = Release; }; OBJ_48 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ENABLE_TESTABILITY = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PLATFORM_DIR)/Developer/Library/Frameworks", ); HEADER_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = Eval.xcodeproj/Eval_Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = Eval; PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; SWIFT_VERSION = 5.0; TARGET_NAME = Eval; }; name = Debug; }; OBJ_49 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ENABLE_TESTABILITY = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PLATFORM_DIR)/Developer/Library/Frameworks", ); HEADER_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = Eval.xcodeproj/Eval_Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = Eval; PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; SWIFT_VERSION = 5.0; TARGET_NAME = Eval; }; name = Release; }; OBJ_62 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { LD = /usr/bin/true; OTHER_SWIFT_FLAGS = "-swift-version 4.2 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk"; SWIFT_VERSION = 5.0; }; name = Debug; }; OBJ_63 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { LD = /usr/bin/true; OTHER_SWIFT_FLAGS = "-swift-version 4.2 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk"; SWIFT_VERSION = 5.0; }; name = Release; }; OBJ_68 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { }; name = Debug; }; OBJ_69 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { }; name = Release; }; OBJ_73 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ENABLE_MODULES = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PLATFORM_DIR)/Developer/Library/Frameworks", ); HEADER_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = Eval.xcodeproj/EvalTests_Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks"; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; SWIFT_VERSION = 5.0; TARGET_NAME = EvalTests; }; name = Debug; }; OBJ_74 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ENABLE_MODULES = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PLATFORM_DIR)/Developer/Library/Frameworks", ); HEADER_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = Eval.xcodeproj/EvalTests_Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks"; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; SWIFT_VERSION = 5.0; TARGET_NAME = EvalTests; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ OBJ_2 /* Build configuration list for PBXProject "Eval" */ = { isa = XCConfigurationList; buildConfigurations = ( OBJ_3 /* Debug */, OBJ_4 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; OBJ_47 /* Build configuration list for PBXNativeTarget "Eval" */ = { isa = XCConfigurationList; buildConfigurations = ( OBJ_48 /* Debug */, OBJ_49 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; OBJ_61 /* Build configuration list for PBXNativeTarget "EvalPackageDescription" */ = { isa = XCConfigurationList; buildConfigurations = ( OBJ_62 /* Debug */, OBJ_63 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; OBJ_67 /* Build configuration list for PBXAggregateTarget "EvalPackageTests" */ = { isa = XCConfigurationList; buildConfigurations = ( OBJ_68 /* Debug */, OBJ_69 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; OBJ_72 /* Build configuration list for PBXNativeTarget "EvalTests" */ = { isa = XCConfigurationList; buildConfigurations = ( OBJ_73 /* Debug */, OBJ_74 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = OBJ_1 /* Project object */; } ================================================ FILE: Eval.xcodeproj/xcshareddata/xcschemes/Eval-Package.xcscheme ================================================ ================================================ FILE: Eval.xcodeproj/xcshareddata/xcschemes/xcschememanagement.plist ================================================ SchemeUserState Eval-Package.xcscheme SuppressBuildableAutocreation ================================================ FILE: Eval.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Eval.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: Examples/.swiftlint.yml ================================================ disabled_rules: - file_header ================================================ FILE: Examples/AttributedStringExample/.gitignore ================================================ .DS_Store /.build /Packages /*.xcodeproj ================================================ FILE: Examples/AttributedStringExample/Package.swift ================================================ // swift-tools-version:4.2 import PackageDescription let package = Package( name: "AttributedStringExample", products: [ .library( name: "AttributedStringExample", targets: ["AttributedStringExample"]) ], dependencies: [ .package(url: "../../", from: "1.4.0") ], targets: [ .target( name: "AttributedStringExample", dependencies: ["Eval"]), .testTarget( name: "AttributedStringExampleTests", dependencies: ["AttributedStringExample"]) ] ) ================================================ FILE: Examples/AttributedStringExample/README.md ================================================ # TemplateExample A description of this package. ================================================ FILE: Examples/AttributedStringExample/Sources/AttributedStringExample/TemplateExample.swift ================================================ import AppKit @_exported import Eval import Foundation @_exported import class Eval.Pattern // swiftlint:disable:next type_name public class AttributedStringTemplateInterpreter: TemplateInterpreter { typealias EvaluatedType = NSAttributedString override public func evaluate(_ expression: String, context: Context = Context()) -> NSAttributedString { return evaluate(expression, context: context, reducer: (initialValue: NSAttributedString(), reduceValue: { existing, next in existing.appending(next) }, reduceCharacter: { existing, next in existing.appending(NSAttributedString(string: String(next))) })) } } // swiftlint:disable:next type_name public class AttributedStringInterpreter: EvaluatorWithLocalContext { public typealias EvaluatedType = NSAttributedString let interpreter: AttributedStringTemplateInterpreter init() { let context = Context() let center = NSMutableParagraphStyle() center.alignment = .center interpreter = AttributedStringTemplateInterpreter(statements: [AttributedStringInterpreter.attributeMatcher(name: "bold", attributes: [.font: NSFont.boldSystemFont(ofSize: 12)]), AttributedStringInterpreter.attributeMatcher(name: "red", attributes: [.foregroundColor: NSColor.red]), AttributedStringInterpreter.attributeMatcher(name: "center", attributes: [.paragraphStyle: center])], interpreter: TypedInterpreter(context: context), context: context) } public func evaluate(_ expression: String) -> AttributedStringInterpreter.EvaluatedType { return interpreter.evaluate(expression) } public func evaluate(_ expression: String, context: Context) -> AttributedStringInterpreter.EvaluatedType { return interpreter.evaluate(expression, context: context) } static func attributeMatcher(name: String, attributes: [NSAttributedString.Key: Any]) -> Pattern> { return Pattern([OpenKeyword("<\(name)>"), GenericVariable("body", options: .notInterpreted), CloseKeyword("")]) { guard let body = $0.variables["body"] as? String else { return nil } return NSAttributedString(string: body, attributes: attributes) } } } public extension NSAttributedString { func appending(_ other: NSAttributedString) -> NSAttributedString { let mutable = NSMutableAttributedString(attributedString: self) mutable.append(other) return mutable } } ================================================ FILE: Examples/AttributedStringExample/Tests/.swiftlint.yml ================================================ disabled_rules: - force_cast - force_try - type_name - file_header - explicit_top_level_acl ================================================ FILE: Examples/AttributedStringExample/Tests/AttributedStringExampleTests/AttributedStringExampleTests.swift ================================================ @testable import AttributedStringExample import Eval import XCTest class AttributedStringExampleTests: XCTestCase { let interpreter: AttributedStringInterpreter = AttributedStringInterpreter() func testExample() { let interpreter = AttributedStringInterpreter() XCTAssertEqual(interpreter.evaluate("Hello"), NSAttributedString(string: "Hello", attributes: [.font: NSFont.boldSystemFont(ofSize: 12)])) XCTAssertEqual(interpreter.evaluate("It's red"), NSAttributedString(string: "It's ").appending(NSAttributedString(string: "red", attributes: [.foregroundColor: NSColor.red]))) let style = interpreter.evaluate("
Centered text
").attribute(.paragraphStyle, at: 0, effectiveRange: nil) as! NSParagraphStyle XCTAssertEqual(style.alignment, .center) } } ================================================ FILE: Examples/AttributedStringExample/Tests/LinuxMain.swift ================================================ @testable import TemplateExampleTests import XCTest XCTMain([ testCase(TemplateExampleTests.allTests) ]) ================================================ FILE: Examples/ColorParserExample/.gitignore ================================================ .DS_Store /.build /Packages /*.xcodeproj ================================================ FILE: Examples/ColorParserExample/Package.swift ================================================ // swift-tools-version:4.2 import PackageDescription let package = Package( name: "ColorParserExample", products: [ .library( name: "ColorParserExample", targets: ["ColorParserExample"]) ], dependencies: [ .package(url: "../../", from: "1.4.0") ], targets: [ .target( name: "ColorParserExample", dependencies: ["Eval"]), .testTarget( name: "ColorParserExampleTests", dependencies: ["ColorParserExample"]) ] ) ================================================ FILE: Examples/ColorParserExample/README.md ================================================ # TemplateExample A description of this package. ================================================ FILE: Examples/ColorParserExample/Sources/ColorParserExample/ColorParserExample.swift ================================================ import AppKit @_exported import Eval @_exported import class Eval.Pattern import Foundation public class ColorParser: EvaluatorWithLocalContext { let interpreter: TypedInterpreter init() { interpreter = TypedInterpreter(dataTypes: [ColorParser.colorDataType()], functions: [ColorParser.mixFunction()]) } static func colorDataType() -> DataType { let hex = Literal { guard $0.value.first == "#", $0.value.count == 7, let red = Int($0.value[1...2], radix: 16), let green = Int($0.value[3...4], radix: 16), let blue = Int($0.value[5...6], radix: 16) else { return nil } return NSColor(calibratedRed: CGFloat(red), green: CGFloat(green), blue: CGFloat(blue), alpha: 1) } let red = Literal("red", convertsTo: NSColor.red) return DataType(type: NSColor.self, literals: [hex, red]) { $0.value.description } } static func mixFunction() -> Function { return Function([Variable("lhs"), Keyword("mixed with"), Variable("rhs")]) { guard let lhs = $0.variables["lhs"] as? NSColor, let rhs = $0.variables["rhs"] as? NSColor else { return nil } return lhs.blend(with: rhs) } } public func evaluate(_ expression: String) -> Any? { return interpreter.evaluate(expression) } public func evaluate(_ expression: String, context: Context) -> Any? { return interpreter.evaluate(expression, context: context) } } extension String { subscript (range: CountableClosedRange) -> Substring { return self[index(startIndex, offsetBy: range.lowerBound) ..< index(startIndex, offsetBy: range.upperBound)] } } extension NSColor { func blend(with other: NSColor, using factor: CGFloat = 0.5) -> NSColor { let inverseFactor = 1.0 - factor var leftRed: CGFloat = 0 var leftGreen: CGFloat = 0 var leftBlue: CGFloat = 0 var leftAlpha: CGFloat = 0 getRed(&leftRed, green: &leftGreen, blue: &leftBlue, alpha: &leftAlpha) var rightRed: CGFloat = 0 var rightGreen: CGFloat = 0 var rightBlue: CGFloat = 0 var rightAlpha: CGFloat = 0 other.getRed(&rightRed, green: &rightGreen, blue: &rightBlue, alpha: &rightAlpha) return NSColor(calibratedRed: leftRed * factor + rightRed * inverseFactor, green: leftGreen * factor + rightGreen * inverseFactor, blue: leftBlue * factor + rightBlue * inverseFactor, alpha: leftAlpha * factor + rightAlpha * inverseFactor) } } ================================================ FILE: Examples/ColorParserExample/Tests/.swiftlint.yml ================================================ disabled_rules: - force_cast - force_try - type_name - file_header - explicit_top_level_acl ================================================ FILE: Examples/ColorParserExample/Tests/ColorParserExampleTests/ColorParserExampleTests.swift ================================================ @testable import ColorParserExample import Eval import XCTest class ColorParserExampleTests: XCTestCase { let colorParser: ColorParser = ColorParser() func testExample() { XCTAssertEqual(colorParser.evaluate("#00ff00") as! NSColor, NSColor(calibratedRed: 0, green: 1, blue: 0, alpha: 1)) XCTAssertEqual(colorParser.evaluate("red") as! NSColor, .red) XCTAssertEqual(colorParser.evaluate("#ff0000 mixed with #0000ff") as! NSColor, NSColor(calibratedRed: 0.5, green: 0, blue: 0.5, alpha: 1)) } } ================================================ FILE: Examples/ColorParserExample/Tests/LinuxMain.swift ================================================ @testable import TemplateExampleTests import XCTest XCTMain([ testCase(TemplateExampleTests.allTests) ]) ================================================ FILE: Examples/TemplateExample/.gitignore ================================================ .DS_Store /.build /Packages /*.xcodeproj ================================================ FILE: Examples/TemplateExample/Package.swift ================================================ // swift-tools-version:4.2 import PackageDescription let package = Package( name: "TemplateExample", products: [ .library( name: "TemplateExample", targets: ["TemplateExample"]) ], dependencies: [ .package(url: "../../", from: "1.4.0") ], targets: [ .target( name: "TemplateExample", dependencies: ["Eval"]), .testTarget( name: "TemplateExampleTests", dependencies: ["TemplateExample"]) ] ) ================================================ FILE: Examples/TemplateExample/README.md ================================================ # TemplateExample A description of this package. ================================================ FILE: Examples/TemplateExample/Sources/TemplateExample/TemplateExample.swift ================================================ @_exported import Eval @_exported import class Eval.Pattern import Foundation public class TemplateLanguage: EvaluatorWithLocalContext { public typealias EvaluatedType = String let language: StringTemplateInterpreter let macroReplacer: StringTemplateInterpreter init(dataTypes: [DataTypeProtocol] = StandardLibrary.dataTypes, functions: [FunctionProtocol] = StandardLibrary.functions, templates: [Pattern>] = TemplateLibrary.templates, context: Context = Context()) { TemplateLanguage.preprocess(context) let interpreter = TypedInterpreter(dataTypes: dataTypes, functions: functions, context: context) let language = StringTemplateInterpreter(statements: templates, interpreter: interpreter, context: context) self.language = language let block = Pattern>([OpenKeyword("{{{"), TemplateVariable("name", options: .notInterpreted), CloseKeyword("}}}")]) { guard let name = $0.variables["name"] as? String else { return nil } return language.context.blocks[name]?.last?(language.context) } macroReplacer = StringTemplateInterpreter(statements: [block]) } public func evaluate(_ expression: String) -> String { return evaluate(expression, context: Context()) } public func evaluate(_ expression: String, context: Context) -> String { TemplateLanguage.preprocess(context) let input = replaceWhitespaces(expression) let result = language.evaluate(input, context: context) let finalResult = macroReplacer.evaluate(result) return finalResult.contains(TemplateLibrary.tagPrefix) ? language.evaluate(finalResult, context: context) : finalResult } public func evaluate(template from: URL) throws -> String { let expression = try String(contentsOf: from) return evaluate(expression) } public func evaluate(template from: URL, context: Context) throws -> String { let expression = try String(contentsOf: from) return evaluate(expression, context: context) } static func preprocess(_ context: Context) { context.variables = context.variables.mapValues { value in convert(value) { if let integerValue = $0 as? Int { return Double(integerValue) } return $0 } } } static func convert(_ value: Any, recursively: Bool = true, convert: @escaping (Any) -> Any) -> Any { if recursively, let array = value as? [Any] { return array.map { convert($0) } } if recursively, let dictionary = value as? [String: Any] { return dictionary.mapValues { convert($0) } } return convert(value) } func replaceWhitespaces(_ input: String) -> String { let tag = "{-}" var input = input repeat { if var range = input.range(of: tag) { searchForward: while true { if range.upperBound < input.index(before: input.endIndex) { let nextIndex = range.upperBound if let unicodeScalar = input[nextIndex].unicodeScalars.first, CharacterSet.whitespacesAndNewlines.contains(unicodeScalar) { range = Range(uncheckedBounds: (lower: range.lowerBound, upper: input.index(after: range.upperBound))) } else { break searchForward } } else { break searchForward } } searchBackward: while true { if range.lowerBound > input.startIndex { let nextIndex = input.index(before: range.lowerBound) if let unicodeScalar = input[nextIndex].unicodeScalars.first, CharacterSet.whitespacesAndNewlines.contains(unicodeScalar) { range = Range(uncheckedBounds: (lower: input.index(before: range.lowerBound), upper: range.upperBound)) } else { break searchBackward } } else { break searchBackward } } input.replaceSubrange(range, with: "") } } while input.contains(tag) return input } } internal typealias Macro = (arguments: [String], body: String) internal typealias BlockRenderer = (_ context: Context) -> String extension Context { static let macrosKey: String = "__macros" var macros: [String: Macro] { get { return variables[Context.macrosKey] as? [String: Macro] ?? [:] } set { variables[Context.macrosKey] = macros.merging(newValue) { _, new in new } } } static let blocksKey: String = "__blocks" var blocks: [String: [BlockRenderer]] { get { return variables[Context.blocksKey] as? [String: [BlockRenderer]] ?? [:] } set { variables[Context.blocksKey] = blocks.merging(newValue) { _, new in new } } } } public class TemplateLibrary { public static var standardLibrary: StandardLibrary = StandardLibrary() public static var templates: [Pattern>] { return [ ifElseStatement, ifStatement, printStatement, forInStatement, setUsingBodyStatement, setStatement, blockStatement, macroStatement, commentStatement, importStatement, spacelessStatement ] } public static var tagPrefix: String = "{%" public static var tagSuffix: String = "%}" public static var ifStatement: Pattern> { return Pattern([Keyword(tagPrefix + " if"), Variable("condition"), Keyword(tagSuffix), TemplateVariable("body", options: .notTrimmed) { guard let content = $0.value as? String, !content.contains(tagPrefix + " else " + tagSuffix) else { return nil } return content }, Keyword("{%"), Keyword("endif"), Keyword("%}")]) { guard let condition = $0.variables["condition"] as? Bool, let body = $0.variables["body"] as? String else { return nil } return condition ? body : "" } } public static var ifElseStatement: Pattern> { return Pattern([OpenKeyword(tagPrefix + " if"), Variable("condition"), Keyword(tagSuffix), TemplateVariable("body", options: .notTrimmed) { guard let content = $0.value as? String, !content.contains(tagPrefix + " else " + tagSuffix) else { return nil } return content }, Keyword(tagPrefix + " else " + tagSuffix), TemplateVariable("else", options: .notTrimmed) { guard let content = $0.value as? String, !content.contains(tagPrefix + " else " + tagSuffix) else { return nil } return content }, CloseKeyword(tagPrefix + " endif " + tagSuffix)]) { guard let condition = $0.variables["condition"] as? Bool, let body = $0.variables["body"] as? String else { return nil } return condition ? body : $0.variables["else"] as? String } } public static var printStatement: Pattern> { return Pattern([OpenKeyword("{{"), Variable("body"), CloseKeyword("}}")]) { guard let body = $0.variables["body"] else { return nil } return $0.interpreter.typedInterpreter.print(body) } } public static var forInStatement: Pattern> { return Pattern([OpenKeyword(tagPrefix + " for"), GenericVariable("variable", options: .notInterpreted), Keyword("in"), Variable<[Any]>("items"), Keyword(tagSuffix), GenericVariable("body", options: [.notInterpreted, .notTrimmed]), CloseKeyword(tagPrefix + " endfor " + tagSuffix)]) { guard let variableName = $0.variables["variable"] as? String, let items = $0.variables["items"] as? [Any], let body = $0.variables["body"] as? String else { return nil } var result = "" $0.context.push() $0.context.variables["__loop"] = items for (index, item) in items.enumerated() { $0.context.variables["__first"] = index == items.startIndex $0.context.variables["__last"] = index == items.index(before: items.endIndex) $0.context.variables[variableName] = item result += $0.interpreter.evaluate(body, context: $0.context) } $0.context.pop() return result } } public static var setStatement: Pattern> { return Pattern([OpenKeyword(tagPrefix + " set"), TemplateVariable("variable"), Keyword(tagSuffix), TemplateVariable("body"), CloseKeyword(tagPrefix + " endset " + tagSuffix)]) { guard let variableName = $0.variables["variable"] as? String, let body = $0.variables["body"] as? String else { return nil } $0.interpreter.context.variables[variableName] = body return "" } } public static var setUsingBodyStatement: Pattern> { return Pattern([OpenKeyword(tagPrefix + " set"), TemplateVariable("variable"), Keyword("="), Variable("value"), CloseKeyword(tagSuffix)]) { guard let variableName = $0.variables["variable"] as? String else { return nil } $0.interpreter.context.variables[variableName] = $0.variables["value"] return "" } } public static var blockStatement: Pattern> { return Pattern([OpenKeyword(tagPrefix + " block"), GenericVariable("name", options: .notInterpreted), Keyword(tagSuffix), GenericVariable("body", options: .notInterpreted), CloseKeyword(tagPrefix + " endblock " + tagSuffix)]) { match in guard let name = match.variables["name"] as? String, let body = match.variables["body"] as? String else { return nil } let block: BlockRenderer = { context in context.push() context.merge(with: match.context) { existing, _ in existing } context.variables["__block"] = name if let last = context.blocks[name] { context.blocks[name] = Array(last.dropLast()) } let result = match.interpreter.evaluate(body, context: context) context.pop() return result } if let last = match.interpreter.context.blocks[name] { match.interpreter.context.blocks[name] = last + [block] return "" } else { match.interpreter.context.blocks[name] = [block] return "{{{\(name)}}}" } } } public static var macroStatement: Pattern> { return Pattern([OpenKeyword(tagPrefix + " macro"), GenericVariable("name", options: .notInterpreted), Keyword("("), GenericVariable<[String], StringTemplateInterpreter>("arguments", options: .notInterpreted) { guard let arguments = $0.value as? String else { return nil } return arguments.split(separator: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } }, Keyword(")"), Keyword(tagSuffix), GenericVariable("body", options: .notInterpreted), CloseKeyword(tagPrefix + " endmacro " + tagSuffix)]) { guard let name = $0.variables["name"] as? String, let arguments = $0.variables["arguments"] as? [String], let body = $0.variables["body"] as? String else { return nil } $0.interpreter.context.macros[name] = (arguments: arguments, body: body) return "" } } public static var commentStatement: Pattern> { return Pattern([OpenKeyword("{#"), GenericVariable("body", options: .notInterpreted), CloseKeyword("#}")]) { _ in "" } } public static var importStatement: Pattern> { return Pattern([OpenKeyword(tagPrefix + " import"), Variable("file"), CloseKeyword(tagSuffix)]) { guard let file = $0.variables["file"] as? String, let url = Bundle.allBundles.compactMap({ $0.url(forResource: file, withExtension: nil) }).first, let expression = try? String(contentsOf: url) else { return nil } return $0.interpreter.evaluate(expression, context: $0.context) } } public static var spacelessStatement: Pattern> { return Pattern([OpenKeyword(tagPrefix + " spaceless " + tagSuffix), TemplateVariable("body"), CloseKeyword(tagPrefix + " endspaceless " + tagSuffix)]) { guard let body = $0.variables["body"] as? String else { return nil } return body.self.components(separatedBy: .whitespacesAndNewlines).filter { !$0.isEmpty }.joined() } } } // swiftlint:disable:next type_body_length public class StandardLibrary { public static var dataTypes: [DataTypeProtocol] { return [ stringType, booleanType, arrayType, dictionaryType, dateType, numericType, emptyType ] } public static var functions: [FunctionProtocol] { return [ parentheses, macro, blockParent, ternaryOperator, rangeFunction, rangeOfStringFunction, rangeBySteps, loopIsFirst, loopIsLast, loopIsNotFirst, loopIsNotLast, startsWithOperator, endsWithOperator, containsOperator, matchesOperator, capitalise, lowercase, uppercase, lowercaseFirst, uppercaseFirst, trim, urlEncode, urlDecode, escape, nl2br, stringConcatenationOperator, multiplicationOperator, divisionOperator, additionOperator, subtractionOperator, moduloOperator, powOperator, lessThanOperator, lessThanOrEqualsOperator, moreThanOperator, moreThanOrEqualsOperator, equalsOperator, notEqualsOperator, stringEqualsOperator, stringNotEqualsOperator, inNumericArrayOperator, inStringArrayOperator, incrementOperator, decrementOperator, negationOperator, notOperator, orOperator, andOperator, absoluteValue, defaultValue, isEvenOperator, isOddOperator, minFunction, maxFunction, sumFunction, sqrtFunction, roundFunction, averageFunction, arraySubscript, arrayCountFunction, arrayMapFunction, arrayFilterFunction, arraySortFunction, arrayReverseFunction, arrayMinFunction, arrayMaxFunction, arrayFirstFunction, arrayLastFunction, arrayJoinFunction, arraySplitFunction, arrayMergeFunction, arraySumFunction, arrayAverageFunction, dictionarySubscript, dictionaryCountFunction, dictionaryFilterFunction, dictionaryKeys, dictionaryValues, dateFactory, dateFormat, stringFactory ] } // MARK: Types public static var numericType: DataType { let numberLiteral = Literal { Double($0.value) } let piLiteral = Literal("pi", convertsTo: Double.pi) return DataType(type: Double.self, literals: [numberLiteral, piLiteral]) { String(format: "%g", $0.value) } } public static var stringType: DataType { let singleQuotesLiteral = literal(opening: "'", closing: "'") { $0.value } return DataType(type: String.self, literals: [singleQuotesLiteral]) { $0.value } } public static var dateType: DataType { let dateFormatter = DateFormatter(with: "yyyy-MM-dd HH:mm:ss") let now = Literal("now", convertsTo: Date()) return DataType(type: Date.self, literals: [now]) { dateFormatter.string(from: $0.value) } } public static var arrayType: DataType<[CustomStringConvertible]> { let arrayLiteral = literal(opening: "[", closing: "]") { literal -> [CustomStringConvertible]? in literal.value .split(separator: ",") .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .map { literal.interpreter.evaluate(String($0)) as? CustomStringConvertible ?? String($0) } } return DataType(type: [CustomStringConvertible].self, literals: [arrayLiteral]) { dataType in dataType.value.map { dataType.printer.print($0) }.joined(separator: ",") } } public static var dictionaryType: DataType<[String: CustomStringConvertible?]> { let dictionaryLiteral = literal(opening: "{", closing: "}") { body -> [String: CustomStringConvertible?]? in let values = body.value .split(separator: ",") .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } let parsedValues : [(key: String, value: CustomStringConvertible?)] = values .map { $0.split(separator: ":").map { body.interpreter.evaluate(String($0)) } } .compactMap { guard let first = $0.first, let key = first as? String, let value = $0.last else { return nil } return (key: key, value: value as? CustomStringConvertible) } return Dictionary(grouping: parsedValues) { $0.key }.mapValues { $0.first?.value } } return DataType(type: [String: CustomStringConvertible?].self, literals: [dictionaryLiteral]) { dataType in let items = dataType.value.map { key, value in if let value = value { return "\(dataType.printer.print(key)): \(dataType.printer.print(value))" } else { return "\(dataType.printer.print(key)): nil" } }.sorted().joined(separator: ", ") return "[\(items)]" } } public static var booleanType: DataType { let trueLiteral = Literal("true", convertsTo: true) let falseLiteral = Literal("false", convertsTo: false) return DataType(type: Bool.self, literals: [trueLiteral, falseLiteral]) { $0.value ? "true" : "false" } } public static var emptyType: DataType { let nullLiteral = Literal("null", convertsTo: nil) let nilLiteral = Literal("nil", convertsTo: nil) return DataType(type: Any?.self, literals: [nullLiteral, nilLiteral]) { _ in "null" } } // MARK: Functions public static var parentheses: Function { return Function([OpenKeyword("("), Variable("body"), CloseKeyword(")")]) { $0.variables["body"] as? Double } } public static var macro: Function { return Function([Variable("name", options: .notInterpreted) { guard let value = $0.value as? String else { return nil } return $0.interpreter.context.macros.keys.contains(value) ? value : nil }, Keyword("("), Variable("arguments", options: .notInterpreted), Keyword(")")]) { match in guard let arguments = match.variables["arguments"] as? String, let name = match.variables["name"] as? String, let macro = match.interpreter.context.macros[name.trimmingCharacters(in: .whitespacesAndNewlines)] else { return nil } let interpretedArguments = arguments.split(separator: ",").compactMap { match.interpreter.evaluate(String($0).trimmingCharacters(in: .whitespacesAndNewlines)) } match.context.push() for (key, value) in zip(macro.arguments, interpretedArguments) { match.context.variables[key] = value } let result = match.interpreter.evaluate(macro.body, context: match.context) match.context.pop() return result } } public static var blockParent: Function { return Function([Keyword("parent"), Keyword("("), Variable("arguments", options: .notInterpreted), Keyword(")")]) { guard let arguments = $0.variables["arguments"] as? String else { return nil } var interpretedArguments: [String: Any] = [:] for argument in arguments.split(separator: ",") { let parts = String(argument).trimmingCharacters(in: .whitespacesAndNewlines).split(separator: "=") if let key = parts.first, let value = parts.last { interpretedArguments[String(key)] = $0.interpreter.evaluate(String(value)) } } guard let name = $0.context.variables["__block"] as? String, let block = $0.context.blocks[name]?.last else { return nil } $0.context.push() $0.context.variables.merge(interpretedArguments) { _, new in new } let result = block($0.context) $0.context.pop() return result } } public static var ternaryOperator: Function { return Function([Variable("condition"), Keyword("?"), Variable("body"), Keyword(": "), Variable("else")]) { guard let condition = $0.variables["condition"] as? Bool else { return nil } return condition ? $0.variables["body"] : $0.variables["else"] } } public static var rangeFunction: Function<[Double]> { return infixOperator("...") { (lhs: Double, rhs: Double) in CountableClosedRange(uncheckedBounds: (lower: Int(lhs), upper: Int(rhs))).map { Double($0) } } } public static var rangeOfStringFunction: Function<[String]> { return infixOperator("...") { (lhs: String, rhs: String) in CountableClosedRange(uncheckedBounds: (lower: Character(lhs), upper: Character(rhs))).map { String($0) } } } public static var startsWithOperator: Function { return infixOperator("starts with") { (lhs: String, rhs: String) in lhs.hasPrefix(rhs) } } public static var endsWithOperator: Function { return infixOperator("ends with") { (lhs: String, rhs: String) in lhs.hasSuffix(rhs) } } public static var containsOperator: Function { return infixOperator("contains") { (lhs: String, rhs: String) in lhs.contains(rhs) } } public static var matchesOperator: Function { return infixOperator("matches") { (lhs: String, rhs: String) in if let regex = try? NSRegularExpression(pattern: rhs) { let matches = regex.numberOfMatches(in: lhs, range: NSRange(lhs.startIndex..., in: lhs)) return matches > 0 } return false } } public static var capitalise: Function { return objectFunction("capitalise") { (value: String) -> String? in value.capitalized } } public static var lowercase: Function { return objectFunction("lower") { (value: String) -> String? in value.lowercased() } } public static var uppercase: Function { return objectFunction("upper") { (value: String) -> String? in value.uppercased() } } public static var lowercaseFirst: Function { return objectFunction("lowerFirst") { (value: String) -> String? in guard let first = value.first else { return nil } return String(first).lowercased() + value[value.index(value.startIndex, offsetBy: 1)...] } } public static var uppercaseFirst: Function { return objectFunction("upperFirst") { (value: String) -> String? in guard let first = value.first else { return nil } return String(first).uppercased() + value[value.index(value.startIndex, offsetBy: 1)...] } } public static var trim: Function { return objectFunction("trim") { (value: String) -> String? in value.trimmingCharacters(in: .whitespacesAndNewlines) } } public static var urlEncode: Function { return objectFunction("urlEncode") { (value: String) -> String? in value.addingPercentEncoding(withAllowedCharacters: .alphanumerics) } } public static var urlDecode: Function { return objectFunction("urlDecode") { (value: String) -> String? in value.removingPercentEncoding } } public static var escape: Function { return objectFunction("escape") { (value: String) -> String? in value.html } } public static var nl2br: Function { return objectFunction("nl2br") { (value: String) -> String? in value .replacingOccurrences(of: "\r\n", with: "
") .replacingOccurrences(of: "\n", with: "
") } } public static var stringConcatenationOperator: Function { return infixOperator("+") { (lhs: String, rhs: String) in lhs + rhs } } public static var additionOperator: Function { return infixOperator("+") { (lhs: Double, rhs: Double) in lhs + rhs } } public static var subtractionOperator: Function { return infixOperator("-") { (lhs: Double, rhs: Double) in lhs - rhs } } public static var multiplicationOperator: Function { return infixOperator("*") { (lhs: Double, rhs: Double) in lhs * rhs } } public static var divisionOperator: Function { return infixOperator("/") { (lhs: Double, rhs: Double) in lhs / rhs } } public static var moduloOperator: Function { return infixOperator("%") { (lhs: Double, rhs: Double) in Double(Int(lhs) % Int(rhs)) } } public static var powOperator: Function { return infixOperator("**") { (lhs: Double, rhs: Double) in pow(lhs, rhs) } } public static var lessThanOperator: Function { return infixOperator("<") { (lhs: Double, rhs: Double) in lhs < rhs } } public static var moreThanOperator: Function { return infixOperator("<=") { (lhs: Double, rhs: Double) in lhs <= rhs } } public static var lessThanOrEqualsOperator: Function { return infixOperator(">") { (lhs: Double, rhs: Double) in lhs > rhs } } public static var moreThanOrEqualsOperator: Function { return infixOperator(">=") { (lhs: Double, rhs: Double) in lhs >= rhs } } public static var equalsOperator: Function { return infixOperator("==") { (lhs: Double, rhs: Double) in lhs == rhs } } public static var notEqualsOperator: Function { return infixOperator("!=") { (lhs: Double, rhs: Double) in lhs != rhs } } public static var stringEqualsOperator: Function { return infixOperator("==") { (lhs: String, rhs: String) in lhs == rhs } } public static var stringNotEqualsOperator: Function { return infixOperator("!=") { (lhs: String, rhs: String) in lhs != rhs } } public static var inStringArrayOperator: Function { return infixOperator("in") { (lhs: String, rhs: [String]) in rhs.contains(lhs) } } public static var inNumericArrayOperator: Function { return infixOperator("in") { (lhs: Double, rhs: [Double]) in rhs.contains(lhs) } } public static var negationOperator: Function { return prefixOperator("!") { (expression: Bool) in !expression } } public static var notOperator: Function { return prefixOperator("not") { (expression: Bool) in !expression } } public static var andOperator: Function { return infixOperator("and") { (lhs: Bool, rhs: Bool) in lhs && rhs } } public static var orOperator: Function { return infixOperator("or") { (lhs: Bool, rhs: Bool) in lhs || rhs } } public static var absoluteValue: Function { return objectFunction("abs") { (value: Double) -> Double? in abs(value) } } public static var defaultValue: Function { return Function([Variable("lhs"), Keyword("."), Variable("rhs", options: .notInterpreted) { guard let value = $0.value as? String, value == "default" else { return nil } return value }, Keyword("("), Variable("fallback"), Keyword(")")], options: .backwardMatch) { guard let value = $0.variables["lhs"], $0.variables["rhs"] != nil else { return nil } return isNilOrWrappedNil(value: value) ? $0.variables["fallback"] : value } } public static var incrementOperator: Function { return suffixOperator("++") { (expression: Double) in expression + 1 } } public static var decrementOperator: Function { return suffixOperator("--") { (expression: Double) in expression - 1 } } public static var isEvenOperator: Function { return suffixOperator("is even") { (expression: Double) in Int(expression) % 2 == 0 } } public static var isOddOperator: Function { return suffixOperator("is odd") { (expression: Double) in abs(Int(expression) % 2) == 1 } } public static var minFunction: Function { return function("min") { (arguments: [Any]) -> Double? in guard let arguments = arguments as? [Double] else { return nil } return arguments.min() } } public static var maxFunction: Function { return function("max") { (arguments: [Any]) -> Double? in guard let arguments = arguments as? [Double] else { return nil } return arguments.max() } } public static var arraySortFunction: Function<[Double]> { return objectFunction("sort") { (object: [Double]) -> [Double]? in object.sorted() } } public static var arrayReverseFunction: Function<[Double]> { return objectFunction("reverse") { (object: [Double]) -> [Double]? in object.reversed() } } public static var arrayMinFunction: Function { return objectFunction("min") { (object: [Double]) -> Double? in object.min() } } public static var arrayMaxFunction: Function { return objectFunction("max") { (object: [Double]) -> Double? in object.max() } } public static var arrayFirstFunction: Function { return objectFunction("first") { (object: [Double]) -> Double? in object.first } } public static var arrayLastFunction: Function { return objectFunction("last") { (object: [Double]) -> Double? in object.last } } public static var arrayJoinFunction: Function { return objectFunctionWithParameters("join") { (object: [String], arguments: [Any]) -> String? in guard let separator = arguments.first as? String else { return nil } return object.joined(separator: separator) } } public static var arraySplitFunction: Function<[String]> { return Function([Variable("lhs"), Keyword("."), Variable("rhs", options: .notInterpreted) { guard let value = $0.value as? String, value == "split" else { return nil } return value }, Keyword("("), Variable("separator"), Keyword(")")]) { guard let object = $0.variables["lhs"] as? String, $0.variables["rhs"] != nil, let separator = $0.variables["separator"] as? String else { return nil } return object.split(separator: Character(separator)).map { String($0) } } } public static var arrayMergeFunction: Function<[Any]> { return Function([Variable<[Any]>("lhs"), Keyword("."), Variable("rhs", options: .notInterpreted) { guard let value = $0.value as? String, value == "merge" else { return nil } return value }, Keyword("("), Variable<[Any]>("other"), Keyword(")")]) { guard let object = $0.variables["lhs"] as? [Any], $0.variables["rhs"] != nil, let other = $0.variables["other"] as? [Any] else { return nil } return object + other } } public static var arraySumFunction: Function { return objectFunction("sum") { (object: [Double]) -> Double? in object.reduce(0, +) } } public static var arrayAverageFunction: Function { return objectFunction("avg") { (object: [Double]) -> Double? in object.reduce(0, +) / Double(object.count) } } public static var arrayCountFunction: Function { return objectFunction("count") { (object: [Double]) -> Double? in Double(object.count) } } public static var dictionaryCountFunction: Function { return objectFunction("count") { (object: [String: Any]) -> Double? in Double(object.count) } } public static var arrayMapFunction: Function<[Any]> { return Function([Variable<[Any]>("lhs"), Keyword("."), Variable("rhs", options: .notInterpreted) { guard let value = $0.value as? String, value == "map" else { return nil } return value }, Keyword("("), Variable("variable", options: .notInterpreted), Keyword("=>"), Variable("body", options: .notInterpreted), Keyword(")")]) { match in guard let object = match.variables["lhs"] as? [Any], match.variables["rhs"] != nil, let variable = match.variables["variable"] as? String, let body = match.variables["body"] as? String else { return nil } match.context.push() let result: [Any] = object.compactMap { item in match.context.variables[variable] = item return match.interpreter.evaluate(body, context: match.context) } match.context.pop() return result } } public static var arrayFilterFunction: Function<[Any]> { return Function([Variable<[Any]>("lhs"), Keyword("."), Variable("rhs", options: .notInterpreted) { guard let value = $0.value as? String, value == "filter" else { return nil } return value }, Keyword("("), Variable("variable", options: .notInterpreted), Keyword("=>"), Variable("body", options: .notInterpreted), Keyword(")")]) { match in guard let object = match.variables["lhs"] as? [Any], match.variables["rhs"] != nil, let variable = match.variables["variable"] as? String, let body = match.variables["body"] as? String else { return nil } match.context.push() let result: [Any] = object.filter { item in match.context.variables[variable] = item if let result = match.interpreter.evaluate(body, context: match.context) as? Bool { return result } return false } match.context.pop() return result } } public static var dictionaryFilterFunction: Function<[String: Any]> { return Function([Variable<[String: Any]>("lhs"), Keyword("."), Variable("rhs", options: .notInterpreted) { guard let value = $0.value as? String, value == "filter" else { return nil } return value }, Keyword("("), Variable("key", options: .notInterpreted), Keyword(","), Variable("value", options: .notInterpreted), Keyword("=>"), Variable("body", options: .notInterpreted), Keyword(")")]) { match in guard let object = match.variables["lhs"] as? [String: Any], match.variables["rhs"] != nil, let keyVariable = match.variables["key"] as? String, let valueVariable = match.variables["value"] as? String, let body = match.variables["body"] as? String else { return nil } match.context.push() let result: [String: Any] = object.filter { key, value in match.context.variables[keyVariable] = key match.context.variables[valueVariable] = value if let result = match.interpreter.evaluate(body, context: match.context) as? Bool { return result } return false } match.context.pop() return result } } public static var sumFunction: Function { return function("sum") { (arguments: [Any]) -> Double? in guard let arguments = arguments as? [Double] else { return nil } return arguments.reduce(0, +) } } public static var averageFunction: Function { return function("avg") { (arguments: [Any]) -> Double? in guard let arguments = arguments as? [Double] else { return nil } return arguments.reduce(0, +) / Double(arguments.count) } } public static var sqrtFunction: Function { return function("sqrt") { (arguments: [Any]) -> Double? in guard let value = arguments.first as? Double else { return nil } return sqrt(value) } } public static var roundFunction: Function { return function("round") { (arguments: [Any]) -> Double? in guard let value = arguments.first as? Double else { return nil } return round(value) } } public static var dateFactory: Function { return function("Date") { (arguments: [Any]) -> Date? in guard let arguments = arguments as? [Double], arguments.count >= 3 else { return nil } var components = DateComponents() components.calendar = Calendar(identifier: .gregorian) components.year = Int(arguments[0]) components.month = Int(arguments[1]) components.day = Int(arguments[2]) components.hour = arguments.count > 3 ? Int(arguments[3]) : 0 components.minute = arguments.count > 4 ? Int(arguments[4]) : 0 components.second = arguments.count > 5 ? Int(arguments[5]) : 0 return components.date } } public static var stringFactory: Function { return function("String") { (arguments: [Any]) -> String? in guard let argument = arguments.first as? Double else { return nil } return String(format: "%g", argument) } } public static var rangeBySteps: Function<[Double]> { return functionWithNamedParameters("range") { (arguments: [String: Any]) -> [Double]? in guard let start = arguments["start"] as? Double, let end = arguments["end"] as? Double, let step = arguments["step"] as? Double else { return nil } var result = [start] var value = start while value <= end - step { value += step result.append(value) } return result } } public static var loopIsFirst: Function { return Function([Variable("value"), Keyword("is first")]) { $0.context.variables["__first"] as? Bool } } public static var loopIsLast: Function { return Function([Variable("value"), Keyword("is last")]) { $0.context.variables["__last"] as? Bool } } public static var loopIsNotFirst: Function { return Function([Variable("value"), Keyword("is not first")]) { guard let isFirst = $0.context.variables["__first"] as? Bool else { return nil } return !isFirst } } public static var loopIsNotLast: Function { return Function([Variable("value"), Keyword("is not last")]) { guard let isLast = $0.context.variables["__last"] as? Bool else { return nil } return !isLast } } public static var dateFormat: Function { return objectFunctionWithParameters("format") { (object: Date, arguments: [Any]) -> String? in guard let format = arguments.first as? String else { return nil } let dateFormatter = DateFormatter(with: format) return dateFormatter.string(from: object) } } public static var arraySubscript: Function { return Function([Variable<[Any]>("array"), Keyword("."), Variable("index")]) { guard let array = $0.variables["array"] as? [Any], let index = $0.variables["index"] as? Double, index > 0, Int(index) < array.count else { return nil } return array[Int(index)] } } public static var dictionarySubscript: Function { return Function([Variable<[String: Any]>("dictionary"), Keyword("."), Variable("key", options: .notInterpreted)]) { guard let dictionary = $0.variables["dictionary"] as? [String: Any], let key = $0.variables["key"] as? String else { return nil } return dictionary[key] } } public static var dictionaryKeys: Function<[String]> { return objectFunction("keys") { (object: [String: Any?]) -> [String] in object.keys.sorted() } } public static var dictionaryValues: Function<[Any?]> { return objectFunction("values") { (object: [String: Any?]) -> [Any?] in if let values = object as? [String: Double] { return values.values.sorted() } if let values = object as? [String: String] { return values.values.sorted() } return Array(object.values) } } public static var methodCallWithIntResult: Function { return Function([Variable("lhs"), Keyword("."), Variable("rhs", options: .notInterpreted)]) { if let lhs = $0.variables["lhs"] as? NSObjectProtocol, let rhs = $0.variables["rhs"] as? String, let result = lhs.perform(Selector(rhs)) { return Double(Int(bitPattern: result.toOpaque())) } return nil } } // MARK: Literal helpers public static func literal(opening: String, closing: String, convert: @escaping (_ literal: LiteralBody) -> T?) -> Literal { return Literal { literal -> T? in guard literal.value.hasPrefix(opening), literal.value.hasSuffix(closing), literal.value.count > 1 else { return nil } let inputWithoutOpening = String(literal.value.suffix(from: literal.value.index(literal.value.startIndex, offsetBy: opening.count))) let inputWithoutSides = String(inputWithoutOpening.prefix(upTo: inputWithoutOpening.index(inputWithoutOpening.endIndex, offsetBy: -closing.count))) guard !inputWithoutSides.contains(opening) && !inputWithoutSides.contains(closing) else { return nil } return convert(LiteralBody(value: inputWithoutSides, interpreter: literal.interpreter)) } } // MARK: Operator helpers public static func infixOperator(_ symbol: String, body: @escaping (A, B) -> T) -> Function { return Function([Variable
("lhs"), Keyword(symbol), Variable("rhs")], options: .backwardMatch) { guard let lhs = $0.variables["lhs"] as? A, let rhs = $0.variables["rhs"] as? B else { return nil } return body(lhs, rhs) } } public static func prefixOperator(_ symbol: String, body: @escaping (A) -> T) -> Function { return Function([Keyword(symbol), Variable("value")]) { guard let value = $0.variables["value"] as? A else { return nil } return body(value) } } public static func suffixOperator(_ symbol: String, body: @escaping (A) -> T) -> Function { return Function([Variable("value"), Keyword(symbol)]) { guard let value = $0.variables["value"] as? A else { return nil } return body(value) } } // MARK: Function helpers public static func function(_ name: String, body: @escaping ([Any]) -> T?) -> Function { return Function([Keyword(name), OpenKeyword("("), Variable("arguments", options: .notInterpreted), CloseKeyword(")")]) { match in guard let arguments = match.variables["arguments"] as? String else { return nil } let interpretedArguments = arguments.split(separator: ",").compactMap { match.interpreter.evaluate(String($0).trimmingCharacters(in: .whitespacesAndNewlines)) } return body(interpretedArguments) } } public static func functionWithNamedParameters(_ name: String, body: @escaping ([String: Any]) -> T?) -> Function { return Function([Keyword(name), OpenKeyword("("), Variable("arguments", options: .notInterpreted), CloseKeyword(")")]) { guard let arguments = $0.variables["arguments"] as? String else { return nil } var interpretedArguments: [String: Any] = [:] for argument in arguments.split(separator: ",") { let parts = String(argument).trimmingCharacters(in: .whitespacesAndNewlines).split(separator: "=") if let key = parts.first, let value = parts.last { interpretedArguments[String(key)] = $0.interpreter.evaluate(String(value)) } } return body(interpretedArguments) } } public static func objectFunction(_ name: String, body: @escaping (O) -> T?) -> Function { return Function([Variable("lhs"), Keyword("."), Variable("rhs", options: .notInterpreted) { guard let value = $0.value as? String, value == name else { return nil } return value }], options: .backwardMatch) { guard let object = $0.variables["lhs"] as? O, $0.variables["rhs"] != nil else { return nil } return body(object) } } public static func objectFunctionWithParameters(_ name: String, body: @escaping (O, [Any]) -> T?) -> Function { return Function([Variable("lhs"), Keyword("."), Variable("rhs", options: .notInterpreted) { guard let value = $0.value as? String, value == name else { return nil } return value }, Keyword("("), Variable("arguments", options: .notInterpreted), Keyword(")")]) { match in guard let object = match.variables["lhs"] as? O, match.variables["rhs"] != nil, let arguments = match.variables["arguments"] as? String else { return nil } let interpretedArguments = arguments.split(separator: ",").compactMap { match.interpreter.evaluate(String($0).trimmingCharacters(in: .whitespacesAndNewlines)) } return body(object, interpretedArguments) } } public static func objectFunctionWithNamedParameters(_ name: String, body: @escaping (O, [String: Any]) -> T?) -> Function { return Function([Variable("lhs"), Keyword("."), Variable("rhs", options: .notInterpreted) { guard let value = $0.value as? String, value == name else { return nil } return value }, OpenKeyword("("), Variable("arguments", options: .notInterpreted), CloseKeyword(")")]) { match in guard let object = match.variables["lhs"] as? O, match.variables["rhs"] != nil, let arguments = match.variables["arguments"] as? String else { return nil } var interpretedArguments: [String: Any] = [:] for argument in arguments.split(separator: ",") { let parts = String(argument).trimmingCharacters(in: .whitespacesAndNewlines).split(separator: "=") if let key = parts.first, let value = parts.last { interpretedArguments[String(key)] = match.interpreter.evaluate(String(value)) } } return body(object, interpretedArguments) } } } public extension DateFormatter { convenience init(with format: String) { self.init() self.calendar = Calendar(identifier: .gregorian) self.dateFormat = format } } extension Character: Strideable { public typealias Stride = Int var value: UInt32 { return unicodeScalars.first?.value ?? 0 } public func distance(to other: Character) -> Int { return Int(other.value) - Int(self.value) } public func advanced(by offset: Int) -> Character { let advancedValue = offset + Int(self.value) guard let advancedScalar = UnicodeScalar(advancedValue) else { fatalError("\(String(advancedValue, radix: 16)) does not represent a valid unicode scalar value.") } return Character(advancedScalar) } } extension String { static let enc: [Character: String] = [" ": " ", " ": " ", " ": " ", " ": " ", "‾": "‾", "–": "–", "—": "—", "¡": "¡", "¿": "¿", "…": "…", "·": "·", "'": "'", "‘": "‘", "’": "’", "‚": "‚", "‹": "‹", "›": "›", "‎": "‎", "‏": "‏", "­": "­", "‍": "‍", "‌": "‌", "\"": """, "“": "“", "”": "”", "„": "„", "«": "«", "»": "»", "⌈": "⌈", "⌉": "⌉", "⌊": "⌊", "⌋": "⌋", "〈": "⟨", "〉": "⟩", "§": "§", "¶": "¶", "&": "&", "‰": "‰", "†": "†", "‡": "‡", "•": "•", "′": "′", "″": "″", "´": "´", "˜": "˜", "¯": "¯", "¨": "¨", "¸": "¸", "ˆ": "ˆ", "°": "°", "©": "©", "®": "®", "℘": "℘", "←": "←", "→": "→", "↑": "↑", "↓": "↓", "↔": "↔", "↵": "↵", "⇐": "⇐", "⇑": "⇑", "⇒": "⇒", "⇓": "⇓", "⇔": "⇔", "∀": "∀", "∂": "∂", "∃": "∃", "∅": "∅", "∇": "∇", "∈": "∈", "∉": "∉", "∋": "∋", "∏": "∏", "∑": "∑", "±": "±", "÷": "÷", "×": "×", "<": "<", "≠": "≠", ">": ">", "¬": "¬", "¦": "¦", "−": "−", "⁄": "⁄", "∗": "∗", "√": "√", "∝": "∝", "∞": "∞", "∠": "∠", "∧": "∧", "∨": "∨", "∩": "∩", "∪": "∪", "∫": "∫", "∴": "∴", "∼": "∼", "≅": "≅", "≈": "≈", "≡": "≡", "≤": "≤", "≥": "≥", "⊄": "⊄", "⊂": "⊂", "⊃": "⊃", "⊆": "⊆", "⊇": "⊇", "⊕": "⊕", "⊗": "⊗", "⊥": "⊥", "⋅": "⋅", "◊": "◊", "♠": "♠", "♣": "♣", "♥": "♥", "♦": "♦", "¤": "¤", "¢": "¢", "£": "£", "¥": "¥", "€": "€", "¹": "¹", "½": "½", "¼": "¼", "²": "²", "³": "³", "¾": "¾", "á": "á", "Á": "Á", "â": "â", "Â": "Â", "à": "à", "À": "À", "å": "å", "Å": "Å", "ã": "ã", "Ã": "Ã", "ä": "ä", "Ä": "Ä", "ª": "ª", "æ": "æ", "Æ": "Æ", "ç": "ç", "Ç": "Ç", "ð": "ð", "Ð": "Ð", "é": "é", "É": "É", "ê": "ê", "Ê": "Ê", "è": "è", "È": "È", "ë": "ë", "Ë": "Ë", "ƒ": "ƒ", "í": "í", "Í": "Í", "î": "î", "Î": "Î", "ì": "ì", "Ì": "Ì", "ℑ": "ℑ", "ï": "ï", "Ï": "Ï", "ñ": "ñ", "Ñ": "Ñ", "ó": "ó", "Ó": "Ó", "ô": "ô", "Ô": "Ô", "ò": "ò", "Ò": "Ò", "º": "º", "ø": "ø", "Ø": "Ø", "õ": "õ", "Õ": "Õ", "ö": "ö", "Ö": "Ö", "œ": "œ", "Œ": "Œ", "ℜ": "ℜ", "š": "š", "Š": "Š", "ß": "ß", "™": "™", "ú": "ú", "Ú": "Ú", "û": "û", "Û": "Û", "ù": "ù", "Ù": "Ù", "ü": "ü", "Ü": "Ü", "ý": "ý", "Ý": "Ý", "ÿ": "ÿ", "Ÿ": "Ÿ", "þ": "þ", "Þ": "Þ", "α": "α", "Α": "Α", "β": "β", "Β": "Β", "γ": "γ", "Γ": "Γ", "δ": "δ", "Δ": "Δ", "ε": "ε", "Ε": "Ε", "ζ": "ζ", "Ζ": "Ζ", "η": "η", "Η": "Η", "θ": "θ", "Θ": "Θ", "ϑ": "ϑ", "ι": "ι", "Ι": "Ι", "κ": "κ", "Κ": "Κ", "λ": "λ", "Λ": "Λ", "µ": "µ", "μ": "μ", "Μ": "Μ", "ν": "ν", "Ν": "Ν", "ξ": "ξ", "Ξ": "Ξ", "ο": "ο", "Ο": "Ο", "π": "π", "Π": "Π", "ϖ": "ϖ", "ρ": "ρ", "Ρ": "Ρ", "σ": "σ", "Σ": "Σ", "ς": "ς", "τ": "τ", "Τ": "Τ", "ϒ": "ϒ", "υ": "υ", "Υ": "Υ", "φ": "φ", "Φ": "Φ", "χ": "χ", "Χ": "Χ", "ψ": "ψ", "Ψ": "Ψ", "ω": "ω", "Ω": "Ω", "ℵ": "ℵ"] var html: String { var html = "" for character in self { if let entity = String.enc[character] { html.append(entity) } else { html.append(character) } } return html } } internal func isNilOrWrappedNil(value: Any) -> Bool { let mirror = Mirror(reflecting: value) if mirror.displayStyle == .optional { if let first = mirror.children.first { return isNilOrWrappedNil(value: first.value) } else { return true } } return false } // swiftlint:disable:this file_length ================================================ FILE: Examples/TemplateExample/Tests/.swiftlint.yml ================================================ disabled_rules: - force_cast - force_try - force_unwrapping - type_name - file_header - explicit_top_level_acl ================================================ FILE: Examples/TemplateExample/Tests/LinuxMain.swift ================================================ @testable import TemplateExampleTests import XCTest XCTMain([ testCase(TemplateExampleTests.allTests) ]) ================================================ FILE: Examples/TemplateExample/Tests/TemplateExampleTests/TemplateExampleComponentTests.swift ================================================ import Eval @testable import TemplateExample import XCTest class TemplateExampleComponentTests: XCTestCase { let interpreter: TemplateLanguage = TemplateLanguage() func testComplexExample() { XCTAssertEqual(eval( """ {% if greet %}Hello{% else %}Bye{% endif %} {{ name }}! {% set works = true %} {% for i in [3,2,1] %}{{ i }}, {% endfor %}go! This template engine {% if !works %}does not {% endif %}work{% if works %}s{% endif %}! """, ["greet": true, "name": "Laszlo"]), """ Hello Laszlo! 3, 2, 1, go! This template engine works! """) } // MARK: Helpers func eval(_ template: String, _ variables: [String: Any] = [:]) -> String { let context = Context(variables: variables) let result = interpreter.evaluate(template, context: context) if !context.debugInfo.isEmpty { print(context.debugInfo) } return result } } ================================================ FILE: Examples/TemplateExample/Tests/TemplateExampleTests/TemplateExampleTests.swift ================================================ import Eval @testable import TemplateExample import XCTest class TemplateExampleTests: XCTestCase { let interpreter: TemplateLanguage = TemplateLanguage() // MARK: Statements func testIfElseStatement() { XCTAssertEqual(eval("{% if x in [1,2,3] %}Hello{% else %}Bye{% endif %} {{ name }}!", ["x": 2, "name": "Teve"]), "Hello Teve!") } func testIfStatement() { XCTAssertEqual(eval("{% if true %}Hello{% endif %} {{ name }}!", ["name": "Teve"]), "Hello Teve!") } func testEmbeddedIfStatement() { XCTAssertEqual(eval("Result: {% if x > 1 %}{% if x < 5 %}1 1 %}{% if x < 5 %}1= 5 %}x>=5{% else %}{% if x > 1 %}1 1 %}{% if x < 5 %}1=5{% endif %}{% else %}x<=1{% endif %}", ["x": 2]), "Result: 1= 5 %}x>=5{% else %}{% if x > 1 %}1 3 }}"), "false") XCTAssertEqual(eval("{{ 3 > 2 }}"), "true") XCTAssertEqual(eval("{{ 2 > 2 }}"), "false") } func testGreaterThanOrEqual() { XCTAssertEqual(eval("{{ 2 >= 3 }}"), "false") XCTAssertEqual(eval("{{ 3 >= 2 }}"), "true") XCTAssertEqual(eval("{{ 2 >= 2 }}"), "true") } func testEquals() { XCTAssertEqual(eval("{{ 2 == 3 }}"), "false") XCTAssertEqual(eval("{{ 2 == 2 }}"), "true") } func testNotEquals() { XCTAssertEqual(eval("{{ 2 != 2 }}"), "false") XCTAssertEqual(eval("{{ 2 != 3 }}"), "true") } func testInNumericArray() { XCTAssertEqual(eval("{{ 2 in [1,2,3] }}"), "true") XCTAssertEqual(eval("{{ 5 in [1,2,3] }}"), "false") } func testInStringArray() { XCTAssertEqual(eval("{{ 'a' in ['a', 'b', 'c'] }}"), "true") XCTAssertEqual(eval("{{ 'z' in ['a', 'b', 'c'] }}"), "false") } func testIncrement() { XCTAssertEqual(eval("{{ 2++ }}"), "3") XCTAssertEqual(eval("{{ -1++ }}"), "0") } func testDecrement() { XCTAssertEqual(eval("{{ 7-- }}"), "6") XCTAssertEqual(eval("{{ -7-- }}"), "-8") } func testNegation() { XCTAssertEqual(eval("{{ not true }}"), "false") XCTAssertEqual(eval("{{ not false }}"), "true") XCTAssertEqual(eval("{{ !true }}"), "false") XCTAssertEqual(eval("{{ !false }}"), "true") } func testAnd() { XCTAssertEqual(eval("{{ true and true }}"), "true") XCTAssertEqual(eval("{{ false and false }}"), "false") XCTAssertEqual(eval("{{ true and false }}"), "false") XCTAssertEqual(eval("{{ false and true }}"), "false") } func testOr() { XCTAssertEqual(eval("{{ true or true }}"), "true") XCTAssertEqual(eval("{{ false or false }}"), "false") XCTAssertEqual(eval("{{ true or false }}"), "true") XCTAssertEqual(eval("{{ false or true }}"), "true") } func testIsEven() { XCTAssertEqual(eval("{{ 8 is even }}"), "true") XCTAssertEqual(eval("{{ 1 is even }}"), "false") XCTAssertEqual(eval("{{ -1 is even }}"), "false") } func testIsOdd() { XCTAssertEqual(eval("{{ 8 is odd }}"), "false") XCTAssertEqual(eval("{{ 1 is odd }}"), "true") XCTAssertEqual(eval("{{ -1 is odd }}"), "true") } func testMax() { XCTAssertEqual(eval("{{ [5,3,7,1].max }}"), "7") XCTAssertEqual(eval("{{ max(5,3,7,1) }}"), "7") XCTAssertEqual(eval("{{ [-5,-3,-7,-1].max }}"), "-1") XCTAssertEqual(eval("{{ max(-5,-3,-7,-1) }}"), "-1") } func testMin() { XCTAssertEqual(eval("{{ [5,3,7,1].min }}"), "1") XCTAssertEqual(eval("{{ min(5,3,7,1) }}"), "1") XCTAssertEqual(eval("{{ [-5,-3,-7,-1].min }}"), "-7") XCTAssertEqual(eval("{{ min(-5,-3,-7,-1) }}"), "-7") } func testCount() { XCTAssertEqual(eval("{{ [5,3,7,1].count }}"), "4") XCTAssertEqual(eval("{{ [].count }}"), "0") XCTAssertEqual(eval("{{ {'a': 5, 'b': 2}.count }}"), "2") XCTAssertEqual(eval("{{ {}.count }}"), "0") } func testAverage() { XCTAssertEqual(eval("{{ [1,2,3,4].avg }}"), "2.5") XCTAssertEqual(eval("{{ [2,2].avg }}"), "2") XCTAssertEqual(eval("{{ avg(1,2,3,4) }}"), "2.5") XCTAssertEqual(eval("{{ avg(2,2) }}"), "2") } func testSum() { XCTAssertEqual(eval("{{ [1,2,3,4].sum }}"), "10") XCTAssertEqual(eval("{{ sum(1,2,3,4) }}"), "10") } func testSqrt() { XCTAssertEqual(eval("{{ sqrt(225) }}"), "15") XCTAssertEqual(eval("{{ sqrt(4) }}"), "2") } func testFirst() { XCTAssertEqual(eval("{{ [1,2,3].first }}"), "1") XCTAssertEqual(eval("{{ [].first }}"), "null") } func testLast() { XCTAssertEqual(eval("{{ [1,2,3].last }}"), "3") XCTAssertEqual(eval("{{ [].last }}"), "null") } func testDefault() { XCTAssertEqual(eval("{{ null.default('fallback') }}"), "fallback") XCTAssertEqual(eval("{{ array.last.default('none') }}", ["array": [1]]), "1") XCTAssertEqual(eval("{{ array.last.default('none') }}", ["array": []]), "none") XCTAssertEqual(eval("{{ array.last.default(2) }}", ["array": []]), "2") } func testJoin() { XCTAssertEqual(eval("{{ ['1','2','3'].join('-') }}"), "1-2-3") XCTAssertEqual(eval("{{ [].join('-') }}"), "") } func testSplit() { XCTAssertEqual(eval("{{ 'a,b,c'.split(',') }}"), "a,b,c") XCTAssertEqual(eval("{{ 'a'.split('-') }}"), "a") } func testMerge() { XCTAssertEqual(eval("{{ [1,2,3].merge([4,5]) }}"), "1,2,3,4,5") XCTAssertEqual(eval("{{ [].merge([1]) }}"), "1") } func testArraySubscript() { XCTAssertEqual(eval("{{ array.1 }}", ["array": [1, 2, 3]]), "2") XCTAssertEqual(eval("{{ [1,2,3].1 }}"), "2") XCTAssertEqual(eval("{{ ['a', 'b', 'c'].1 }}"), "b") } func testArrayMap() { XCTAssertEqual(eval("{{ [1,2,3].map(i => i * 2) }}"), "2,4,6") } func testArrayFilter() { XCTAssertEqual(eval("{{ [1,2,3].filter(i => i % 2 == 1) }}"), "1,3") } func testDictionaryFilter() { XCTAssertEqual(eval("{{ {'a': 1, 'b': 2}.filter(k,v => k == 'a') }}"), "[a: 1]") } func testDictionarySubscript() { XCTAssertEqual(eval("{{ dict.b }}", ["dict": ["a": 1, "b": 2]]), "2") XCTAssertEqual(eval("{{ {'a': 1, 'b': 2}.b }}"), "2") } func testDictionaryKeys() { XCTAssertEqual(eval("{{ {'a': 1, 'b': 2}.keys }}"), "a,b") } func testDictionaryValues() { XCTAssertEqual(eval("{{ {'a': 1, 'b': 2}.values }}"), "1,2") } func testAbsolute() { XCTAssertEqual(eval("{{ 1.abs }}"), "1") XCTAssertEqual(eval("{{ -1.abs }}"), "1") } func testRound() { XCTAssertEqual(eval("{{ round(2.5) }}"), "3") XCTAssertEqual(eval("{{ round(1.2) }}"), "1") } func testTrim() { XCTAssertEqual(eval("{{ ' a '.trim }}"), "a") } func testEscape() { XCTAssertEqual(eval("{{ ' ?&:/'.escape }}"), " ?&:/") } func testUrlEncode() { XCTAssertEqual(eval("{{ ' ?&:/'.urlEncode }}"), "%20%3F%26%3A%2F") } func testUrlDecode() { XCTAssertEqual(eval("{{ '%20%3F%26%3A%2F'.urlDecode }}"), " ?&:/") } func testNl2br() { XCTAssertEqual(eval("{{ 'a\nb'.nl2br }}"), "a
b") } func testCapitalise() { XCTAssertEqual(eval("{{ 'hello there'.capitalise }}"), "Hello There") } func testUpper() { XCTAssertEqual(eval("{{ 'hello there'.upper }}"), "HELLO THERE") } func testLower() { XCTAssertEqual(eval("{{ 'HELLO THERE'.lower }}"), "hello there") } func testUpperFirst() { XCTAssertEqual(eval("{{ 'hello there'.upperFirst }}"), "Hello there") } func testLowerFirst() { XCTAssertEqual(eval("{{ 'HELLO THERE'.lowerFirst }}"), "hELLO THERE") } func testUpperCapitalise() { XCTAssertEqual(eval("{{ 'hello there'.capitalise.upperFirst }}"), "Hello There") } func testLowerCapitalise() { XCTAssertEqual(eval("{{ 'HELLO THERE'.capitalise.lowerFirst }}"), "hello There") } // MARK: Whitespace truncation func testSpacelessTag() { XCTAssertEqual(eval("{% spaceless %} {% if true %} Hello {% endif %} {% endspaceless %}"), "Hello") } // MARK: Template file func testTemplateFile() { let result = try! interpreter.evaluate(template: Bundle(for: type(of: self)).url(forResource: "template", withExtension: "txt")!, context: Context(variables: ["name": "Laszlo"])) XCTAssertEqual(result, "Hello Laszlo!") } func testTemplateWithImportFile() { let result = try! interpreter.evaluate(template: Bundle(for: type(of: self)).url(forResource: "import", withExtension: "txt")!, context: Context(variables: ["name": "Laszlo"])) XCTAssertEqual(result, "Hello Laszlo!\nBye!") } // MARK: Helpers func eval(_ template: String, _ variables: [String: Any] = [:]) -> String { let context = Context(variables: variables) let result = interpreter.evaluate(template, context: context) if !context.debugInfo.isEmpty { // print(context.debugInfo) } return result } } ================================================ FILE: Examples/TemplateExample/Tests/TemplateExampleTests/import.txt ================================================ {% import 'template.txt' %} Bye! ================================================ FILE: Examples/TemplateExample/Tests/TemplateExampleTests/template.txt ================================================ Hello {{name}}! ================================================ FILE: Gemfile ================================================ source 'https://rubygems.org' gem 'jazzy', '>=0.9' gem 'xcpretty' gem 'xcpretty-json-formatter' gem 'cocoapods' gem 'danger' gem 'danger-auto_label' gem 'danger-commit_lint' gem 'danger-mention' gem 'danger-pronto' gem 'danger-prose' gem 'danger-shellcheck' # gem 'danger-slather' gem 'danger-swiftlint' gem 'danger-tailor' gem 'danger-welcome_message' gem 'danger-xcode_summary' gem 'danger-xcodebuild' gem 'danger-xcov' ================================================ FILE: LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Package.swift ================================================ // swift-tools-version:5.0 import PackageDescription let package = Package( name: "Eval", products: [ .library( name: "Eval", targets: ["Eval"]), ], dependencies: [ ], targets: [ .target( name: "Eval", dependencies: []), .testTarget( name: "EvalTests", dependencies: ["Eval"]), ] ) ================================================ FILE: README.md ================================================ # { Eval } [![Travis CI status](https://travis-ci.org/tevelee/Eval.svg?branch=master)](https://travis-ci.org/tevelee/Eval) [![Framework version](https://img.shields.io/badge/Version-1.5.0-yellow.svg)]() [![Swift version](https://img.shields.io/badge/Swift-5.0-orange.svg)]() [![Code Documentation Coverage](https://tevelee.github.io/Eval/badge.svg)](https://tevelee.github.io/Eval) [![Code Test Coverage](https://codecov.io/gh/tevelee/Eval/branch/master/graph/badge.svg)](https://codecov.io/gh/tevelee/Eval) [![Platforms](https://img.shields.io/badge/Platforms-iOS%20|%20macOS%20|%20Linux-blue.svg)]() [![Lincese](https://img.shields.io/badge/License-Apache%202.0-green.svg)](https://github.com/tevelee/Eval/tree/master/LICENSE.txt) ##### Dependency Managers [![CocoaPods compatible](https://img.shields.io/badge/CococaPods-Compatible-blue.svg)](http://cocoapods.org/pods/Eval) [![Swift Package Manager compatible](https://img.shields.io/badge/Swift%20Package%20Manager-Compatible-red.svg)](https://github.com/apple/swift-package-manager) [![Carthage compatible](https://img.shields.io/badge/Carthage-Compatible-brightgreen.svg?style=flat)](https://github.com/Carthage/Carthage) --- - [👨🏻‍💻 About](#-about) - [📈 Getting Started](#-getting-started) - [🤓 Short Example](#-short-example) - [⚡️ Installation](#%EF%B8%8F-installation) - [⁉️ How does it work?](#%EF%B8%8F-how-does-it-work) - [🏃🏻 Status](#-status) - [💡 Motivation](#-motivation) - [📚 Examples](#-examples) - [🙋 Contribution](#-contribution) - [👀 Details](#-details) - [👤 Author](#-author) - [⚖️ License](#%EF%B8%8F-license) ## 👨🏻‍💻 About **Eval** is a lightweight interpreter framework written in Swift, for 📱iOS, 🖥 macOS, and 🐧Linux platforms. It evaluates expressions at runtime, with operators and data types you define. 🍏 Pros | 🍎 Cons ------- | -------- 🐥 Lightweight - the whole engine is really just a few hundred lines of code | 🤓 Creating custom operators and data types, on the other hand, can take a few extra lines - depending on your needs ✅ Easy to use API - create new language elements in just a matter of seconds | ♻️ The evaluated result of the expressions must be strongly typed, so you can only accept what type you expect the result is going to be 🎢 Fun - Since it is really easy to play with, it's joyful to add - even complex - language features | - 🚀 Fast execution - I'm trying to optimise as much as possible. Has its limitations though | 🌧 Since it is a really generic concept, some optimisations cannot be made, compared to native interpreters The framework currently supports two different types of execution modes: - **Strongly typed expressions**: like a programming language - **Template languages**: evaluating expressions in arbitrary string environments *Let's see just a few examples:* It's extremely easy to formulate expressions (and evaluate them at runtime), like - `5 in 1...3` evaluates to `false` Bool type - `'Eval' starts with 'E'` evaluates to `true` Bool type - `'b' in ['a','c','d']` evaluates to `false` Bool type - `x < 2 ? 'a' : 'b'` evaluates to `"a"` or `"b"` String type, based on the `x` Int input variable - `Date(2018, 12, 13).format('yyyy-MM-dd')` evaluates to `"2018-12-13"` string - `'hello'.length` evaluates to `5` Integer - `now` evaluates to `Date()` And templates, such as - `{% if name != nil %}Hello{% else %}Bye{% endif %} {{ name|default('user') }}!`, whose output is `Hello Adam!` or `Bye User!` - `Sequence: {% for i in 1...5 %}{{ 2 * i }} {% endfor %}` which is `2 4 6 8 10 ` And so on... The result of these expressions depends on the content, determined by the evaluation. It can be any type which is returned by the functions (String, [Double], Date, or even custom types of your own.) You can find various ways of usage in the examples section below. ## 🏃🏻 Status - [x] Library implementation - [x] API finalisation - [x] Swift Package Manager support - [x] Initial documentation - [x] Example project (template engine) - [x] CocoaPods support - [x] CI - [x] Code test-coverage - [x] v1.0 - [x] Fully detailed documentation - [x] Contribution guides - [x] Further example projects - [x] Debugging helpers - [x] v1.1 This is a really early stage of the project, I'm still deep in the process of all the open-sourcing related tasks, such as firing up a CI, creating a beautiful documentation page, managing administrative tasks around stability. Please stay tuned for the updates! ## 📈 Getting started For the expressions to work, you'll need to create an interpreter instance, providing your data types and expressions you aim to support, and maybe some input variables - if you need any. ```swift let interpreter = TypedInterpreter(dataTypes: [number, string, boolean, array, date], functions: [multipication, addition, ternary], context: Context(variables: ["x": 2.0])) ``` And call it with a string expression, as follows. ```swift let result = interpreter.evaluate("2 * x + 1") as? Double ``` ### 🤓 Short example Let's check out a fairly complex example, and build it from scratch! Let's implement a language which can parse the following expression: ```swift x != 0 ? 5 * x : pi + 1 ``` There's a ternary operator `?:` in there, which we will need. Also, supporting number literals (`0`, `5`, and `1`) and boolean types (`true/false`). There's also a not equal operator `!=` and a `pi` constant. Let's not forget about the addition `+` and multiplication `*` as well! First, here are the data types. ```swift let numberLiteral = Literal { value,_ in Double(value) } //Converts every number literal, if it can be represented with a Double instance let piConstant = Literal("pi", convertsTo: Double.pi) let number = DataType(type: Double.self, literals: [numberLiteral, piConstant]) { String(describing: $0) } ``` ```swift let trueLiteral = Literal("true", convertsTo: true) let falseLiteral = Literal("false", convertsTo: false) let boolean = DataType(type: Bool.self, literals: [trueLiteral, falseLiteral]) { $0 ? "true" : "false" } ``` (The last parameter, expressed as a block, tells the framework how to formulise this type of data as a String for debug messages or other purposes) Now, let's build the operators: ```swift let multiplication = Function(Variable("lhs") + Keyword("*") + Variable("rhs")) { arguments in guard let lhs = arguments["lhs"] as? Double, let rhs = arguments["rhs"] as? Double else { return nil } return lhs * rhs } ``` ```swift let addition = Function(Variable("lhs") + Keyword("+") + Variable("rhs")) { arguments in guard let lhs = arguments["lhs"] as? Double, let rhs = arguments["rhs"] as? Double else { return nil } return lhs + rhs } ``` ```swift let notEquals = Function(Variable("lhs") + Keyword("!=") + Variable("rhs")) { arguments in guard let lhs = arguments["lhs"] as? Double, let rhs = arguments["rhs"] as? Double else { return nil } return lhs != rhs } ``` ```swift let ternary = Function(Variable("condition") + Keyword("?") + Variable("true") + Keyword(":") + Variable("false")) { arguments in guard let condition = arguments["condition"] as? Bool else { return nil } if condition { return arguments["true"] } else { return arguments["false"] } } ``` Looks like, we're all set. Let's evaluate our expression! ```swift let interpreter = TypedInterpreter(dataTypes: [number, boolean], functions: [multipication, addition, notEquals, ternary]) let result : Double = interpreter.evaluate("x != 0 ? 5 * x : pi + 1", context: Context(variables: ["x": 3.0])) XCTAssertEqual(result, 15.0) //Pass! ``` Now, that we have operators and data types, we can also evaluate anything using these data types: * `interpreter.evaluate("3 != 4") as Bool` * `interpreter.evaluate("2 + 1.5 * 6") as Double` (since multiplication is defined earlier in the array, it has a higher precedence, as expected) * `interpreter.evaluate("true ? 1 : 2.5") as Double` As you have seen, it's really easy and intuitive to build custom languages, using simple building blocks. With just a few custom data types and functions, the possibilities are endless. Operators, functions, string, arrays, dates... The motto of the framework: Build your own (mini) language! ### ⚡️ Installation You have a few options to include the library in your app. - Swift Package Manager - CocoaPods - Carthage - Manually #### Swift Package Manager Just add the following line to your dependencies: ```swift .package(url: "https://github.com/tevelee/Eval.git", from: "1.5.0"), ``` And reference it by name in your targets: ```swift targets: [ .target(name: "MyAwesomeApp", dependencies: ["Eval"]), ] ``` And finally, run the integration command: ```bash swift package resolve ``` #### CocoaPods Just add the following line to your `Podfile`: ```ruby pod 'Eval', '~> 1.5.0' ``` And install the new dependency: ```bash pod install ``` #### Carthage Just add the following line to your `Cartfile`: ```ruby github "tevelee/Eval" >= 1.5.0 ``` And install the new dependency: ```bash carthage update ``` #### Manually (Not recommended! Please use a package manager instead to keep your dependencies up to date.) Clone the repository content and copy the files into a new target in your app. ### ⁉️ How does it work? The interpreter itself does not define anything or any way to deal with the input string on its own. All it does is recognising patterns. By creating data types, you provide literals to the framework, which it can interpret as an element or a result of the expression. These types are transformed to real Swift types. By defining functions, you provide patterns to the framework to recognise. Functions are also typed, they return Swift types as a result of their evaluation. Functions consist of keywords and variables, nothing more. - Keywords are static strings which should not be interpreted as data (such as `if`, or `{`, `}`). - Variables, on the other hand, are typed values, recursively evaluated. For example, if a variable recognises something, that proves to be a further pattern, it recursively evaluates their body, until they find context-variables or literals of any given data type. Functions also have blocks, which provide the recognised variables in a key-value dictionary parameter, and you can do whatever you want with them: print them, convert them, modify or assign them to context-variables. The addition function above, for example, consists of two variables on each side, and the `+` keyword in the middle. It also requires a block, where both sides are given in a `[String:Any]`, so the closure can get the values of the placeholders and add them together. There's one interesting aspect of this solution: Unlike traditional - native - interpreters or compilers, this one recognises patterns from top to bottom. Meaning, that it looks at the input string, your expression, and recognises patterns in priority order, and recursively go deeper and deeper until the most basic expressions are met. A traditional interpreter, however, parses expressions character by character, feeding the results to a lexer, the tokeniser, then builds up an abstract syntax tree (which is highly optimisable), and finally converts it to a binary (compiler) or evaluates it at runtime (interpreter), in one word: bottom-up. The two solutions can be compared in various ways. The two main differences are in ease of use, and performance. This version of an interpreter provides an effortless way to define patterns, types, etc., but has its cost! It cannot parse as optimally as a traditional compiler could, as it doesn't have an internal graph of expressions (AST), but still performs in a much more than acceptable way. Definition-wise, this framework provides an easily understandable way of language-elements, but the traditional one really lacks behind, because the lexer is usually an ugly, hardly understandable state machine, or regular expression, BAKED INTO the interpreter code itself. ## 💡 Motivation I have another project, in which I'm generating Objective-C and Swift model objects with loads of utils, based on really short templates. This project was not possible currently in Swift, as there is no template language - capable enough - to create my templates. (I ended up using a third party PHP framework, called [Twig](https://github.com/twigphp/Twig)). So finally, I created one for Swift! It turned out, that making it a little more generic - here and there - makes the whole thing really capable and flexible of using in different use-cases. The pattern matching was there, but soon I realised, that I'm going to need expressions as well, for printing, evaluating in if/while statements and so on. First, I was looking at an excellent library, [Expression](https://github.com/nicklockwood/Expression), created by Nick Lockwood, which is capable of evaluating numeric expressions. Unfortunately, I wanted a bit more, defining strings, dates, array, and further types and expressions, so I used my existing pattern matching solution to bring this capability to life. It ended up quite positively after I discovered the capabilities of a generic solution like this. The whole thing just blew my mind, language features could have been defined in a matter of seconds, and I wanted to share this discovery with the world, so here you are :) ## 📚 Examples ​ ​I included a few use-cases, which bring significant improvements on how things are processed before - at least in my previous projects. ​ ### [Template language](https://github.com/tevelee/Eval/blob/master/Examples/TemplateExample/Tests/TemplateExampleTests/TemplateExampleTests.swift) I was able to create a full-blown template language, completely, using this framework and nothing else. It's almost like a competitor of the one I mentioned ([Twig](https://github.com/twigphp/Twig)). This is the most advanced example of them all! I created a standard library with all the possible operators you can imagine. With helpers, each operator is a small, one-liner addition. Added the important data types, such as arrays, strings, numbers, booleans, dates, etc., and a few functions, to be more awesome. [Take a look for inspiration!](https://github.com/tevelee/Eval/tree/master/Examples/TemplateExample/Sources/TemplateExample/TemplateExample.swift) Together, it makes an excellent addition to my model-object generation project, and **REALLY useful for server-side Swift development as well**! ### [Attributed string parser](https://github.com/tevelee/Eval/blob/master/Examples/AttributedStringExample/Tests/AttributedStringExampleTests/AttributedStringExampleTests.swift) I created another small example, parsing attribtuted strings from simple expressions using XML style tags, such as bold, italic, underlined, colored, etc. With just a few operators, this solution can deliver attributed strings from basic APIs, which otherwise would be hard to manage. My connected project is an iOS application, using the Spotify [HUB framework](https://github.com/spotify/HubFramework), in which I can now provide rich strings with my view-models and parse them from the JSON string results. ### [Color parser](https://github.com/tevelee/Eval/blob/master/Examples/ColorParserExample/Tests/ColorParserExampleTests/ColorParserExampleTests.swift) A color parser is also used by the BFF (Backend For Frontend, not 👭) project I mentioned before. It can parse Swift Color objects from many different styles of strings, such as `#ffddee`, or `red`, or `rgba(1,0.5,0.4,1)`. I included this basic example in the repository as well. ## 🙋 Contribution Anyone is more than welcome to contribute to **Eval**! It can even be an addition to the docs or to the code directly, by [raising an issue](https://github.com/tevelee/Interpreter/issues/new) or in the form of a pull request. Both are equally valuable to me! Happy to assist anyone! In case you need help or want to report a bug - please file an issue. Make sure to provide as much information as you can; sample code also makes it a lot easier for me to help you. Check out the [contribution guidelines](https://github.com/tevelee/Eval/tree/master/CONTRIBUTING.md) for further information. I collected some use cases, and great opportunities for beginner tasks if anybody is motivated to bring this project to a more impressive state! ## 👀 Details Please check out [https://tevelee.github.io/Eval](https://tevelee.github.io/Eval) for the more detailed documentation pages! ## 👤 Author I am Laszlo Teveli, software engineer, iOS evangelist. In my free time I like to work on my hobby projects and open sourcing them 😉 Feel free to reach out to me anytime via `tevelee [at] gmail [dot] com`, or `@tevelee` on Twitter. ## ⚖️ License **Eval** is available under the Apache 2.0 licensing rules. See the [LICENSE](https://github.com/tevelee/Eval/tree/master/LICENSE.txt) file for more information. ================================================ FILE: Scripts/.gitignore ================================================ .DS_Store /.build /Packages /*.xcodeproj ================================================ FILE: Scripts/.swiftlint.yml ================================================ disabled_rules: - force_try - file_header - file_length - explicit_top_level_acl - function_body_length ================================================ FILE: Scripts/Package.swift ================================================ // swift-tools-version:4.2 import PackageDescription let package = Package( name: "Automation", dependencies: [ .package(url: "https://github.com/xcodeswift/xcproj.git", from: "4.0.0"), ], targets: [ .target( name: "Automation", dependencies: ["xcproj"]), ] ) ================================================ FILE: Scripts/Sources/Automation/Error.swift ================================================ import Foundation enum CIError: Error { case invalidExitCode(statusCode: Int32, errorOutput: String?) case timeout case logicalError(message: String) } ================================================ FILE: Scripts/Sources/Automation/Eval.swift ================================================ import Foundation import PathKit import xcproj class Eval { static func main() { print("💁🏻‍♂️ Job type: \(TravisCI.jobType().description)") if isSpecificJob() { return } if TravisCI.isPullRquestJob() || Shell.nextArg("--env") == "pr" { runPullRequestLane() } else { runContinousIntegrationLane() } } static func runPullRequestLane() { runCommands("Building Pull Request") { try prepareForBuild() try build() try runTests() try prepareExamplesForBuild() try buildExamples() try runTestsOnExamples() try runLinter() try runDanger() } } static func runContinousIntegrationLane() { runCommands("Building CI") { try prepareForBuild() try build() try runTests() try prepareExamplesForBuild() try buildExamples() try runTestsOnExamples() try generateDocs() try publishDocs() try runLinter() try runCocoaPodsLinter() try testCoverage() try runDanger() try releaseNewVersion() } } static func isSpecificJob() -> Bool { guard let jobsString = Shell.nextArg("--jobs") else { return false } let jobsToRun = jobsString.split(separator: ",").map { String($0) } let jobsFound = jobsToRun.compactMap { job in jobs.first { $0.key == job } } runCommands("Executing jobs: \(jobsString)") { if let job = jobsToRun.first(where: { !self.jobs.keys.contains($0) }) { throw CIError.logicalError(message: "Job not found: \(job)") } try jobsFound.forEach { print("🏃🏻 Running job \($0.key)") try $0.value() } } return !jobsFound.isEmpty } static func runCommands(_ title: String, commands: () throws -> Void) { do { if !TravisCI.isRunningLocally() { print("travis_fold:start: \(title)") } print("ℹ️ \(title)") try commands() if !TravisCI.isRunningLocally() { print("travis_fold:end: \(title)") } print("🎉 Finished successfully") } catch let CIError.invalidExitCode(statusCode, errorOutput) { print("😢 Error happened: [InsufficientExitCode] ", errorOutput ?? "unknown error") exit(statusCode) } catch let CIError.logicalError(message) { print("😢 Error happened: [LogicalError] ", message) exit(-1) } catch CIError.timeout { print("🕙 Timeout") exit(-1) } catch { print("😢 Error happened [General]") exit(-1) } } // MARK: Tasks static let jobs: [String: () throws -> Void] = [ "prepareForBuild": prepareForBuild, "prepareExamplesForBuild": prepareExamplesForBuild, "build": build, "buildExamples": buildExamples, "runTests": runTests, "runTestsOnExamples": runTestsOnExamples, "runLinter": runLinter, "generateDocs": generateDocs, "publishDocs": publishDocs, "runCocoaPodsLinter": runCocoaPodsLinter, "testCoverage": testCoverage, "runDanger": runDanger ] static func prepareForBuild() throws { if TravisCI.isRunningLocally() { print("🔦 Install dependencies") try Shell.executeAndPrint("rm -f Package.resolved") try Shell.executeAndPrint("rm -rf .build") try Shell.executeAndPrint("rm -rf build") try Shell.executeAndPrint("rm -rf Eval.xcodeproj") try Shell.executeAndPrint("bundle install") } print("🤖 Generating project file") try Shell.executeAndPrint("swift package generate-xcodeproj --enable-code-coverage") } static func build() throws { print("♻️ Building") try Shell.executeAndPrint("swift build", timeout: 120) try Shell.executeAndPrint("xcodebuild clean build -configuration Release -scheme Eval-Package | bundle exec xcpretty --color", timeout: 120) } static func runTests() throws { print("👀 Running automated tests") try Shell.executeAndPrint("swift test", timeout: 120) try Shell.executeAndPrint("xcodebuild test -configuration Release -scheme Eval-Package -enableCodeCoverage YES | bundle exec xcpretty --color", timeout: 120) } static func runLinter() throws { print("👀 Running linter") try Shell.executeAndPrint("swiftlint lint", timeout: 60) } static func generateDocs() throws { print("📚 Generating documentation") try Shell.executeAndPrint("bundle exec jazzy --config .jazzy.yml", timeout: 120) } static func publishDocs() throws { print("📦 Publishing documentation") let dir = "gh-pages" let file = "github_rsa" defer { print("📦 ✨ Cleaning up") try! Shell.executeAndPrint("rm -f \(file)") try! Shell.executeAndPrint("rm -rf \(dir)") try! Shell.executeAndPrint("rm -rf Documentation/Output") } if TravisCI.isRunningLocally() { print("📦 ✨ Preparing") try Shell.executeAndPrint("rm -rf \(dir)") } if let repo = currentRepositoryUrl()?.replacingOccurrences(of: "https://github.com/", with: "git@github.com:") { let branch = "gh-pages" print("📦 📥 Fetching previous docs") try Shell.executeAndPrint("git clone --depth 1 -b \(branch) \(repo) \(dir)", timeout: 30) print("📦 📄 Updating to the new one") try Shell.executeAndPrint("cp -Rf Documentation/Output/ \(dir)") print("📦 👉 Committing") try Shell.executeAndPrint("git -C \(dir) add .") try Shell.executeAndPrint("git -C \(dir) commit -m 'Automatic documentation update'") try Shell.executeAndPrint("git -C \(dir) add .") print("📦 📤 Pushing") let remote = "origin" try Shell.executeAndPrint("git -C \(dir) push --force \(remote) \(branch)", timeout: 30) } else { throw CIError.logicalError(message: "Repository URL not found") } } static func runCocoaPodsLinter() throws { print("🔮 Validating CocoaPods support") let flags = TravisCI.isRunningLocally() ? "--verbose" : "" try Shell.executeAndPrint("export EXPANDED_CODE_SIGN_IDENTITY=-", timeout: 10) try Shell.executeAndPrint("export EXPANDED_CODE_SIGN_IDENTITY_NAME=-", timeout: 10) try Shell.executeAndPrint("bundle exec pod lib lint \(flags)", timeout: 300) } static func testCoverage() throws { defer { print("📦 ✨ Cleaning up") try! Shell.executeAndPrint("rm -f Eval.framework.coverage.txt") try! Shell.executeAndPrint("rm -f EvalTests.xctest.coverage.txt") } print("☝🏻 Uploading code test coverage data") try Shell.executeAndPrint("bash <(curl -s https://codecov.io/bash) -J Eval", timeout: 120) } static func runDanger() throws { if TravisCI.isRunningLocally() { print("⚠️ Running Danger in local mode") try Shell.executeAndPrint("bundle exec danger pr --verbose || true", timeout: 120) } else if TravisCI.isPullRquestJob() { print("⚠️ Running Danger") try Shell.executeAndPrint("bundle exec danger --verbose || true", timeout: 120) } } static func releaseNewVersion() throws { guard case .travisPushOnBranch(_) = TravisCI.jobType() else { return } if let message = try commitMessage() { let message = message.trimmingCharacters(in: .whitespacesAndNewlines) let regex = try NSRegularExpression(pattern: "^Version (\\d{1,2}\\.\\d{1,2}\\.\\d{1,2})$") let matches = regex.numberOfMatches(in: message, range: NSRange(message.startIndex..., in: message)) if matches > 0, let currentTag = try Shell.execute("git show HEAD~1:.version")?.output { let currentTag = currentTag.trimmingCharacters(in: .whitespacesAndNewlines) let tag = message.replacingOccurrences(of: "Version ", with: "") guard let tags = try Shell.execute("git tag -l")?.output?.components(separatedBy: .whitespacesAndNewlines), !tags.contains(tag) else { return } print("🤖 Applying new version \(tag) in project") let files = ["README.md", ".version", "Eval.podspec"] for file in files { try Shell.executeAndPrint("sed -i '' 's/\(currentTag)/\(tag)/g' \(file)") try Shell.executeAndPrint("git add \(file)") } try Shell.executeAndPrint("git commit --amend --no-edit") print("🔖 Tagging \(tag)") try Shell.executeAndPrint("git tag \(tag) HEAD") print("💁🏻 Pushing changes") try Shell.executeAndPrint("git remote add ssh_origin git@github.com:tevelee/Eval.git") try Shell.executeAndPrint("git push ssh_origin HEAD:master --force") try Shell.executeAndPrint("git push ssh_origin HEAD:master --force --tags") print("📦 Releasing package managers") try Shell.executeAndPrint("pod trunk push . || true", timeout: 600) } } } static func prepareExamplesForBuild() throws { print("🤖 Generating project files for Examples") try onAllExamples { _ in let cleanup = [ "rm -f Package.resolved", "rm -rf .build", "rm -rf build" ] let build = [ "swift package generate-xcodeproj" ] return (cleanup + build).joined(separator: " && ") } try performManualSteps() } static func buildExamples() throws { print("♻️ Building Examples") try onAllExamples { example in "xcodebuild clean build -scheme \(example)-Package | bundle exec xcpretty --color" } } static func runTestsOnExamples() throws { print("👀 Running automated tests on Examples") try onAllExamples { example in "xcodebuild test -scheme \(example)-Package | bundle exec xcpretty --color" } } // MARK: Helpers static func onAllExamples(_ command: (String) throws -> String) throws { for (name, directory) in try examples() { let commands = [ "pushd \(directory)", try command(name), "popd" ] try Shell.executeAndPrint(commands.joined(separator: " && "), timeout: 120) } } static func examples() throws -> [(name: String, directory: String)] { let directory = "Examples" return try FileManager.default.contentsOfDirectory(atPath: directory).map { ($0, "\(directory)/\($0)") }.filter { !$0.name.hasPrefix(".") } } static func currentRepositoryUrl(dir: String = ".") -> String? { if let command = try? Shell.execute("git -C \(dir) config --get remote.origin.url"), let output = command?.output?.trimmingCharacters(in: .whitespacesAndNewlines), !output.isEmpty { return output } return nil } static func currentBranch(dir: String = ".") -> String? { if let command = try? Shell.execute("git -C \(dir) rev-parse --abbrev-ref HEAD"), let output = command?.output?.trimmingCharacters(in: .whitespacesAndNewlines), !output.isEmpty { return output } return nil } static func commitMessage(dir: String = ".") throws -> String? { if TravisCI.isRunningLocally() { return try Shell.execute("git -C \(dir) log -1 --pretty=%B")?.output } else { return Shell.env(name: "TRAVIS_COMMIT_MESSAGE") } } // MARK: Manual steps static func performManualSteps() throws { try performManualStepsForTemplateExample() } static func performManualStepsForTemplateExample() throws { let example = "TemplateExample" print("⏳ Configuring \(example)") let base = Path("Examples/\(example)/") let path = Path("\(base)/\(example).xcodeproj") let project = try XcodeProj(path: path) let testsGroup = project.pbxproj.objects.groups.first { $0.value.name == "\(example)Tests" } let phase = PBXResourcesBuildPhase() let ref = project.pbxproj.objects.generateReference(phase, "CopyResourcesBuildPhase") project.pbxproj.objects.addObject(phase, reference: ref) if let target = project.pbxproj.objects.targets(named: "\(example)Tests").first { target.object.buildPhases.append(ref) } let tests = Path("\(base)/Tests/\(example)Tests") let files = try tests.children().compactMap { $0.components.last }.filter { $0.hasSuffix("txt") } for file in files { let fileRef = PBXFileReference(sourceTree: .group, name: nil, path: file) fileRef.fileEncoding = 4 //utf8 let ref = project.pbxproj.objects.generateReference(fileRef, file) project.pbxproj.objects.fileReferences.append(fileRef, reference: ref) let buildFile = PBXBuildFile(fileRef: ref) let buildFileRef = project.pbxproj.objects.generateReference(buildFile, file) project.pbxproj.objects.buildFiles.append(buildFile, reference: buildFileRef) testsGroup?.value.children.append(ref) phase.files.append(buildFileRef) } try project.writePBXProj(path: path) print("🤖 Generated project file") } } ================================================ FILE: Scripts/Sources/Automation/Shell.swift ================================================ import Foundation class Shell { static func executeAndPrint(_ command: String, timeout: Double = 10, allowFailure: Bool = false) throws { print("$ \(command)") let output = try executeShell(commandPath: "/bin/bash", arguments: ["-c", command], timeout: timeout, allowFailure: allowFailure) { print($0, separator: "", terminator: "") } if let error = output?.error { print(error) } } static func execute(_ command: String, timeout: Double = 10, allowFailure: Bool = false) throws -> (output: String?, error: String?)? { return try executeShell(commandPath: "/bin/bash", arguments: ["-c", command], timeout: timeout, allowFailure: allowFailure) } static func bash(commandName: String, arguments: [String] = [], timeout: Double = 10, allowFailure: Bool = false) throws -> (output: String?, error: String?)? { guard let execution = try? executeShell(commandPath: "/bin/bash" , arguments: [ "-l", "-c", "/usr/bin/which \(commandName)" ], timeout: 1), var whichPathForCommand = execution?.output else { return nil } whichPathForCommand = whichPathForCommand.trimmingCharacters(in: NSCharacterSet.whitespacesAndNewlines) return try executeShell(commandPath: whichPathForCommand, arguments: arguments, timeout: timeout, allowFailure: allowFailure) } static func executeShell(commandPath: String, arguments: [String] = [], timeout: Double = 10, allowFailure: Bool = false, stream: @escaping (String) -> Void = { _ in }) throws -> (output: String?, error: String?)? { let task = Process() task.launchPath = commandPath task.arguments = arguments let pipeForOutput = Pipe() task.standardOutput = pipeForOutput let pipeForError = Pipe() task.standardError = pipeForError task.launch() let fileHandle = pipeForOutput.fileHandleForReading fileHandle.waitForDataInBackgroundAndNotify() var outputData = Data() func process(data: Data) { outputData.append(data) if let output = String(data: data, encoding: .utf8) { stream(output) } } let observer = NotificationCenter.default.addObserver(forName: Notification.Name.NSFileHandleDataAvailable, object: fileHandle, queue: nil) { notification in if let noitificationFileHandle = notification.object as? FileHandle { process(data: noitificationFileHandle.availableData) noitificationFileHandle.waitForDataInBackgroundAndNotify() } } defer { NotificationCenter.default.removeObserver(observer) } var shouldTimeout = false DispatchQueue.main.asyncAfter(deadline: .now() + timeout) { if task.isRunning { shouldTimeout = true task.terminate() } } task.waitUntilExit() process(data: fileHandle.readDataToEndOfFile()) if shouldTimeout { throw CIError.timeout } let output = String(data: outputData, encoding: .utf8) let errorData = pipeForError.fileHandleForReading.readDataToEndOfFile() let error = String(data: errorData, encoding: .utf8) let exitCode = task.terminationStatus if exitCode > 0 && !allowFailure { throw CIError.invalidExitCode(statusCode: exitCode, errorOutput: error) } return (output, error) } static func env(name: String) -> String? { return ProcessInfo.processInfo.environment[name] } static func args() -> [String] { return ProcessInfo.processInfo.arguments } static func nextArg(_ arg: String) -> String? { if let index = Shell.args().index(of: arg), Shell.args().count > index + 1 { return Shell.args()[index.advanced(by: 1)] } return nil } } ================================================ FILE: Scripts/Sources/Automation/Travis.swift ================================================ import Foundation class TravisCI { enum JobType: CustomStringConvertible { case local case travisAPI case travisCron case travisPushOnBranch(branch: String) case travisPushOnTag(name: String) case travisPullRequest(branch: String, sha: String, slug: String) var description: String { switch self { case .local: return "Local" case .travisAPI: return "Travis (API)" case .travisCron: return "Travis (Cron job)" case .travisPushOnBranch(let branch): return "Travis (Push on branch '\(branch)')" case .travisPushOnTag(let name): return "Travis (Push of tag '\(name)')" case .travisPullRequest(let branch): return "Travis (Pull Request on branch '\(branch)')" } } } static func isPullRquestJob() -> Bool { return Shell.env(name: "TRAVIS_EVENT_TYPE") == "pull_request" } static func isRunningLocally() -> Bool { return Shell.env(name: "TRAVIS") != "true" } static func isCIJob() -> Bool { return !isRunningLocally() && !isPullRquestJob() } static func jobType() -> JobType { if isRunningLocally() { return .local } else if isPullRquestJob() { return .travisPullRequest(branch: Shell.env(name: "TRAVIS_PULL_REQUEST_BRANCH") ?? "", sha: Shell.env(name: "TRAVIS_PULL_REQUEST_SHA") ?? "", slug: Shell.env(name: "TRAVIS_PULL_REQUEST_SLUG") ?? "") } else if Shell.env(name: "TRAVIS_EVENT_TYPE") == "cron" { return .travisCron } else if Shell.env(name: "TRAVIS_EVENT_TYPE") == "api" { return .travisAPI } else if let tag = Shell.env(name: "TRAVIS_TAG"), !tag.isEmpty { return .travisPushOnTag(name: tag) } else if let branch = Shell.env(name: "TRAVIS_BRANCH"), !branch.isEmpty { return .travisPushOnBranch(branch: branch) } else { fatalError("Cannot identify job type") } } } ================================================ FILE: Scripts/Sources/Automation/main.swift ================================================ import Foundation Eval.main() ================================================ FILE: Scripts/ci.sh ================================================ #!/bin/bash echo "🤖 Assembling automation process" root=`git rev-parse --show-toplevel` cd "$root/Scripts" swift build echo "🏃 Running automation process" output=`swift build --show-bin-path` cd "$root" "$output/automation" ================================================ FILE: Scripts/git_auth.sh ================================================ #!/bin/bash openssl aes-256-cbc -K $encrypted_f50468713ad3_key -iv $encrypted_f50468713ad3_iv -in github_rsa.enc -out github_rsa -d chmod 600 github_rsa ssh-add github_rsa ssh -o StrictHostKeyChecking=no git@github.com || true git config --global user.email tevelee@gmail.com git config --global user.name 'Travis CI' ================================================ FILE: Sources/Eval/Common.swift ================================================ /* * Copyright (c) 2018 Laszlo Teveli. * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import Foundation /// A protocol which is capable of evaluating string expressions to a strongly typed object public protocol Evaluator { /// The type of the evaluation result associatedtype EvaluatedType /// The only method in `Evaluator` protocol which does the evaluation of a string expression, and returns a strongly typed object /// - parameter expression: The input /// - returns: The evaluated value func evaluate(_ expression: String) -> EvaluatedType } /// A special kind of evaluator which uses an `InterpreterContext` instance to evaluate expressions /// The context contains variables which can be used during the evaluation public protocol EvaluatorWithLocalContext: Evaluator { /// Evaluates the provided string expression with the help of the context parameter, and returns a strongly typed object /// - parameter expression: The input /// - parameter context: The local context if there is something expression specific needs to be provided /// - returns: The evaluated value func evaluate(_ expression: String, context: Context) -> EvaluatedType } /// The base protocol of interpreters, that are context-aware, and capable of recursively evaluating variables. They use the evaluate method as their main input public protocol Interpreter: EvaluatorWithLocalContext { /// The evaluator type to use when interpreting variables associatedtype VariableEvaluator: EvaluatorWithLocalContext /// The stored context object for helping evaluation and providing persistency var context: Context { get } /// Sometimes interpreters don't use themselves to evaluate variables by default, maybe a third party, or another contained interpreter. For example, the `StringTemplateInterpreter` class uses `TypedInterpreter` instance to evaluate its variables. var interpreterForEvaluatingVariables: VariableEvaluator { get } } /// A protocol which is able to express custom values as Strings public protocol Printer { /// Converts its input parameter to a String value /// - parameter input: The value to print /// - returns: The converted String instance func print(_ input: Any) -> String } /// Detailed information about recognised expressions public struct ExpressionInfo { /// The raw String input of the expression var input: String /// The generated output of the expression var output: Any /// A stringified version of the elements of the `Matcher` object var pattern: String /// The name of the pattern var patternName: String /// All the variables computed during the evaluation var variables: [String: Any] } /// The only responsibility of the `InterpreterContext` class is to store variables, and keep them during the execution, where multiple expressions might use the same set of variables. public class Context { /// The stored variables public var variables: [String: Any] /// Debug information for recognised patterns public var debugInfo: [String: ExpressionInfo] = [:] /// Context can behave as a stack. If `push` is called, it saves a snapshot of the current state of variables to a stack and lets you modify the content, while the previous values are stored, safely. /// When `pop` is called, it restores the last snapshot, destorying all the changes that happened after the last snapshot. /// Useful for temporal variables! var stack : [(variables: [String: Any], debugInfo: [String: ExpressionInfo])] = [] /// Users of the context may optionally provide an initial set of variables /// - parameter variables: Variable names and values public init(variables: [String: Any] = [:]) { self.variables = variables } /// Context can behave as a stack. If `push` is called, it saves a snapshot of the current state of variables to a stack and lets you modify the content, while the previous values are stored, safely. /// When `pop` is called, it restores the last snapshot, destorying all the changes that happened after the last snapshot. /// Useful for temporal variables! It should be called before setting the temporal variables public func push() { stack.append((variables: variables, debugInfo: debugInfo)) } /// Context can behave as a stack. If `push` is called, it saves a snapshot of the current state of variables to a stack and lets you modify the content, while the previous values are stored, safely. /// When `pop` is called, it restores the last snapshot, destorying all the changes that happened after the last snapshot. /// Useful for temporal variables! It should be called when the temporal variables are not needed anymore public func pop() { if let last = stack.popLast() { variables = last.variables debugInfo = last.debugInfo } } /// Creates a new context instance by merging their variable dictionaries. The one in the parameter overrides the duplicated items of the existing one /// - parameter with: The other context to merge with /// - returns: A new `InterpreterContext` instance with the current and the parameter variables merged inside public func merging(with other: Context?) -> Context { if let other = other { return Context(variables: other.variables.merging(self.variables) { eixstingValue, _ in eixstingValue }) } else { return self } } /// Modifies the current context instance by merging its variable dictionary with the parameter. The one in the parameter overrides the duplicated items of the existing one /// - parameter with: The other context to merge with /// - parameter existing: During the merge the parameter on the existing dictionary (same terminolody with Dictionary.merge) /// - parameter new: During the merge the parameter on the merged dictionary (same terminolody with Dictionary.merge) /// - returns: The same `InterpreterContext` instance after merging the variables dictionary with the variables in the context given as parameter public func merge(with other: Context?, merge: (_ existing: Any, _ new: Any) throws -> Any) { if let other = other { try? variables.merge(other.variables, uniquingKeysWith: merge) } } } /// This is where the `Matcher` is able to determine the `MatchResult` for a given input inside the provided substring range /// - parameter amongst: All the `Matcher` instances to evaluate, in priority order /// - parameter in: The input /// - parameter from: The start of the checked range /// - parameter interpreter: An interpreter instance - if variables need any further evaluation /// - parameter context: The context - if variables need any contextual information /// - parameter connectedRanges: Ranges of string indices that are connected with opening-closing tag pairs, respectively /// - returns: The result of the match operation internal func matchStatement(amongst statements: [Pattern], in input: String, from start: String.Index? = nil, interpreter: E, context: Context, connectedRanges: [ClosedRange] = []) -> MatchResult { let results = statements.lazy.map { statement -> (element: Pattern, result: MatchResult) in let result = statement.matches(string: input, from: start, interpreter: interpreter, context: context, connectedRanges: connectedRanges) return (element: statement, result: result) } if let matchingElement = results.first(where: { $0.result.isMatch() }) { return matchingElement.result } else if results.contains(where: { $0.result.isPossibleMatch() }) { return .possibleMatch } return .noMatch } /// Independent helper function that determines the pairs of opening and closing keywords /// - parameter input: The input string to search ranges in /// - parameter statements: Patterns that contain the opening and closing keyword types that should be matched /// - returns: The ranges of opening-closing pairs, keeping logical hierarchy internal func collectConnectedRanges(input: String, statements: [Pattern]) -> [ClosedRange] { return statements.compactMap { pattern -> [ClosedRange] in let keywords = pattern.elements.compactMap { $0 as? Keyword } let openingKeywords = keywords.filter { $0.type == .openingStatement } let closingKeywords = keywords.filter { $0.type == .closingStatement } guard !openingKeywords.isEmpty && !closingKeywords.isEmpty else { return [] } var ranges: [ClosedRange] = [] var rangeStart: [String.Index] = [] var position = input.startIndex repeat { let relevantInput = input[position...] let start = openingKeywords .first { relevantInput.contains($0.name) } .flatMap { relevantInput.range(of: $0.name)?.lowerBound } let end = closingKeywords .first { relevantInput.contains($0.name) } .flatMap { relevantInput.range(of: $0.name)?.lowerBound } if let start = start, let end = end { if start < end { rangeStart.append(start) } else { let lastOpening = rangeStart.removeLast() ranges.append(lastOpening...end) } position = input.index(after: min(start, end)) } else if let start = start { rangeStart.append(start) position = input.index(after: start) } else if let end = end { if rangeStart.isEmpty { return [] } let lastOpening = rangeStart.removeLast() ranges.append(lastOpening...end) position = input.index(after: end) } else { break } } while position < input.endIndex return ranges }.reduce([], +) } ================================================ FILE: Sources/Eval/Elements.swift ================================================ /* * Copyright (c) 2018 Laszlo Teveli. * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import Foundation /// `MatchElement`s are used by `Matcher` instances to be able to recognise patterns. /// Currently, the two main kind of `MatchElement` classes are `Keyword`s and `Variable`s public protocol PatternElement { /// Using this method, an element returns how much the String provided in the `prefix` parameter matches the current element /// - parameter prefix: The input /// - parameter options: Options that modify the matching algorithm /// - returns: The result of the match operation func matches(prefix: String, options: PatternOptions) -> MatchResult } /// `Keyword` instances are used to provide static points in match sequences so that they can be used as pillars of the expressions the developer tries to match public class Keyword: PatternElement, Equatable { /// The type of the Keyword determines whether the item holds some special purpose, or it's just an ordinary static String public enum KeywordType: Equatable { /// By default, `Keyword` is created as a generic type, meaning, that there is no special requirement, that they need to fulfil case generic /// If a pattern contains two, semantically paired `Keyword`s, they often represent opening and closing parentheses or any special enclosing characters. /// This case represents the first one of the pair, needs to be matched. Often these are expressed as opening parentheses, e.g. `(` case openingStatement /// If a pattern contains two, semantically paired `Keyword`s, they often represent opening and closing parentheses or any special enclosing characters. /// This case represents the second (and last) one of the pair, needs to be matched. Often these are expressed as closing parentheses, e.g. `)` case closingStatement } /// Name (and value) of the `Keyword` let name: String /// Type of the keyword, which gives the framework some extra semantics about its nature let type: KeywordType /// `Keyword` initialiser /// - parameter name: The name (and value) of the `Keyword` /// - parameter type: Type of the keyword, which gives the framework some extra semantics about its nature. Defaults to `KeywordType.generic` public init(_ name: String, type: KeywordType = .generic) { self.name = name.trim() self.type = type } /// `Keyword` instances are returning exactMatch, when they are equal to the `prefix` input. /// If the input is really just a prefix of the keyword, possible metch is returned. noMatch otherwise. /// - parameter prefix: The input /// - parameter options: Options that modify the matching algorithm /// - returns: The result of the match operation public func matches(prefix: String, options: PatternOptions = []) -> MatchResult { let checker = options.contains(.backwardMatch) ? String.hasSuffix : String.hasPrefix if name == prefix || checker(prefix)(name) { return .exactMatch(length: name.count, output: name, variables: [:]) } else if checker(name)(prefix) { return .possibleMatch } return .noMatch } /// `Keyword` instances are `Equatable`s /// - parameter lhs: Left hand side /// - parameter rhs: Right hand side /// - returns: Whether the names and types are equal in `lhs` and `rhs` public static func == (lhs: Keyword, rhs: Keyword) -> Bool { return lhs.name == rhs.name && lhs.type == rhs.type } } /// A special subclass of the `Keyword` class, which initialises a `Keyword` with an opening type. /// Usually used for opening parentheses: `OpenKeyword("[")` public class OpenKeyword: Keyword { /// The initialiser uses the opening type, but the `name` still must be provided /// - parameter name: The name (and value) of the `Keyword` public init(_ name: String) { super.init(name, type: .openingStatement) } } /// A special subclass of the `Keyword` class, which initialises a `Keyword` with an closing type. /// Usually used for closing parentheses: `CloseKeyword("]")` public class CloseKeyword: Keyword { /// The initialiser uses the closing type, but the `name` still must be provided /// - parameter name: The name (and value) of the `Keyword` public init(_ name: String) { super.init(name, type: .closingStatement) } } /// Options that modify the behaviour of the variable matching, and the output that the framework provides public struct VariableOptions: OptionSet { /// Integer representation of the option public let rawValue: Int /// Basic initialiser with the integer representation public init(rawValue: Int) { self.rawValue = rawValue } /// If set, the value of the recognised placeholder will not be processed. Otherwise, it will be evaluated, using the `interpreterForEvaluatingVariables` property of the interpreter instance public static let notInterpreted: VariableOptions = VariableOptions(rawValue: 1 << 0) /// Whether the processed variable should be or not to be trimmed (removing whitespaces from both sides) public static let notTrimmed: VariableOptions = VariableOptions(rawValue: 1 << 1) /// Provides information whether the match should be exhaustive or just use the shortest possible matching string (even zero characters in some edge cases). This depends on the surrounding `Keyword` instances in the containing collection. public static let exhaustiveMatch: VariableOptions = VariableOptions(rawValue: 1 << 2) /// If interpreted and the result of the evaluation is `nil`, then `acceptsNilValue` determines if the current match result should be instant noMatch, or `nil` is an accepted value, so the matching should be continued public static let acceptsNilValue: VariableOptions = VariableOptions(rawValue: 1 << 3) /// In order to avoid double negatives in the source code (e.g. !notInterpreted), this helper checks the lack of .notInterpreted value in the optionset var interpreted: Bool { return !contains(.notInterpreted) } /// In order to avoid double negatives in the source code (e.g. !notTrimmed), this helper checks the lack of .notTrimmed value in the optionset var trimmed: Bool { return !contains(.notTrimmed) } } /// Protocol for all Variables internal protocol VariableProtocol { /// Unique identifier of the variable that is used when matching and returning them in the matcher. var name: String { get } /// Options that modify the behaviour of the variable matching, and the output that the framework provides var options: VariableOptions { get } /// The result of the evaluated variable will be ran through this map function, transforming its value. By default the map tries to convert the matched value to the expected type, using the `as?` operator. /// - parameter input: The first parameter is the value is going to be transformed /// - parameter interpreter: Helps the mapper function to parse and interpret the contents /// - returns: The transformed value or nil - if the value was validated with a negative result func performMap(input: Any, interpreter: Any) -> Any? } /// It's a data transfer object passed in the `Variable` matcher block public struct VariableBody { /// The raw value to match public var value: Any /// An interpreter instance if the raw value needs any further evaluation public var interpreter: I } /// Generic superclass of `Variable`s which are aware of their `Interpreter` classes, /// as they use it when mapping their values public class GenericVariable: VariableProtocol, PatternElement, Equatable { /// Maps and validates the variable value to another /// - parameter body: Struct containing the raw matched value and an interpreter object /// - returns: The transformed value or nil, if the value was validated with a negative result public typealias VariableMapper = (_ body: VariableBody) -> T? /// Unique identifier of the variable that is used when matching and returning them in the matcher. let name: String /// Options that modify the behaviour of the variable matching, and the output that the framework provides let options: VariableOptions /// The result of the evaluated variable will be running through this map function, transforming its value. By default the map tries to convert the matched value to the expected type, using the `as?` operator. let map: VariableMapper /// Initialiser for all the properties /// - parameter name: `GenericVariable`s have a name (unique identifier), that is used when matching and returning them in the matcher. /// - parameter options: Options that modify the behaviour of the variable matching, and the output that the framework provides /// - parameter map: If provided, then the result of the evaluated variable will be running through this map function. By default the map tries to convert the matched value to the expected type, using the `as?` operator. Defaults to identical map, using the `as?` operator for value transformation public init(_ name: String, options: VariableOptions = [], map: @escaping VariableMapper = { $0.value as? T }) { self.name = name self.options = options self.map = map } /// `GenericVariables` always return anyMatch MatchResult, forwarding the shortest argument, provided during initialisation /// - parameter prefix: The input /// - returns: The result of the match operation. Always `anyMatch` with the shortest argument, provided during initialisation public func matches(prefix: String, options: PatternOptions = []) -> MatchResult { return .anyMatch(exhaustive: self.options.contains(.exhaustiveMatch)) } /// A helper method to map the value of the current variable to another type /// - parameter map: The transformation function /// - parameter value: The value to be mapped /// - returns: A new variable instance using the value mapper block public func mapped(_ map: @escaping (_ value: T) -> K?) -> GenericVariable { return GenericVariable(name, options: options) { guard let value = self.map($0) else { return nil } return map(value) } } /// The result of the evaluated variable will be ran through this map function, transforming its value. By default the map tries to convert the matched value to the expected type, using the `as?` operator. /// - parameter input: The first parameter is the value is going to be transformed /// - parameter interpreter: Helps the mapper function to parse and interpret the contents /// - returns: The transformed value or nil - if the value was validated with a negative result func performMap(input: Any, interpreter: Any) -> Any? { guard let interpreter = interpreter as? I else { return nil } return map(VariableBody(value: input, interpreter: interpreter)) } public static func == (lhs: GenericVariable, rhs: GenericVariable) -> Bool { return lhs.name == rhs.name && lhs.options == rhs.options } } ================================================ FILE: Sources/Eval/TemplateInterpreter.swift ================================================ /* * Copyright (c) 2018 Laszlo Teveli. * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import Foundation /// This interpreter is used to evaluate string expressions and return a transformed string, replacing the content where it matches certain patterns. /// Typically used in web applications, where the rendering of an HTML page is provided as a template, and the application replaces certain statements, based on input parameters. open class TemplateInterpreter: Interpreter { /// The statements (patterns) registered to the interpreter. If found, these are going to be processed and replaced with the evaluated value public let statements: [Pattern>] /// The context used when evaluating the expressions. These context variables are global, used in every evaluation processed with this instance. public let context: Context /// The `StringTemplateInterpreter` contains a `TypedInterpreter`, as it is quite common practice to evaluate strongly typed expression as s support for the template language. /// Common examples are: condition part of an if statement, or body of a print statement public let typedInterpreter: TypedInterpreter /// The evaluator type that is being used to process variables. By default, the TypedInterpreter is being used public typealias VariableEvaluator = TypedInterpreter /// The result type of a template evaluation public typealias EvaluatedType = T /// The evaluator, that is being used to process variables public lazy var interpreterForEvaluatingVariables: TypedInterpreter = { typedInterpreter }() /// The statements, and context parameters are optional, but highly recommended to use with actual values. /// In order to properly initialise a `StringTemplateInterpreter`, you'll need a `TypedInterpreter` instance as well. /// - parameter statements: The patterns that the interpreter should recognise /// - parameter interpreter: A `TypedInterpreter` instance to evaluate typed expressions appearing in the template /// - parameter context: Global context that is going to be used with every expression evaluated with the current instance. Defaults to empty context public init(statements: [Pattern>] = [], interpreter: TypedInterpreter = TypedInterpreter(), context: Context = Context()) { self.statements = statements self.typedInterpreter = interpreter self.context = context } /// The main part of the evaluation happens here. In this case, only the global context variables are going to be used /// - parameter expression: The input /// - returns: The output of the evaluation public func evaluate(_ expression: String) -> T { return evaluate(expression, context: Context()) } /// The main part of the evaluation happens here. In this case, the global context variables merged with the provided context are going to be used. /// - parameter expression: The input /// - parameter context: Local context that is going to be used with this expression only /// - returns: The output of the evaluation open func evaluate(_ expression: String, context: Context) -> T { fatalError("Shouldn't instantiate `TemplateInterpreter` directly. Please subclass with a dedicated type instead") } /// Reduce block can convet a stream of values into one, by calling this block for every element, returning a single value at the end. The concept is usually used in functional environments /// - parameter existing: The previously computed value. In case the current iteration is the first, it's the inital value. /// - parameter next: The value of the current element in the iteration /// - returns: The a combined value based on the previous and the new value public typealias Reducer = (_ existing: T, _ next: K) -> T /// In order to support generic types, not just plain String objects, a reducer helps to convert the output to the dedicated output type /// - parameter initialValue: based on the type, an initial value must to be provided which can serve as a base of the output /// - parameter reduceValue: during template execution, if there is some template to replace, the output value can be used to append to the previously existing output /// - parameter reduceCharacter: during template execution, if there is nothing to replace, the value is computed by the character-by-character iteration, appending to the previously existing output public typealias TemplateReducer = (initialValue: T, reduceValue: Reducer, reduceCharacter: Reducer) /// The main part of the evaluation happens here. In this case, the global context variables merged with the provided context are going to be used. /// - parameter expression: The input /// - parameter context: Local context that is going to be used with this expression only /// - parameter reducer: In order to support generic types, not just plain String objects, a reducer helps to convert the output to the dedicated output type /// - returns: The output of the evaluation public func evaluate(_ expression: String, context: Context = Context(), reducer: TemplateReducer) -> T { context.merge(with: self.context) { existing, _ in existing } var output = reducer.initialValue var position = expression.startIndex repeat { let result = matchStatement(amongst: statements, in: expression, from: position, interpreter: self, context: context) switch result { case .noMatch, .possibleMatch: output = reducer.reduceCharacter(output, expression[position]) position = expression.index(after: position) case let .exactMatch(length, matchOutput, _): output = reducer.reduceValue(output, matchOutput) position = expression.index(position, offsetBy: length) default: assertionFailure("Invalid result") } } while position < expression.endIndex return output } } /// This interpreter is used to evaluate string expressions and return a transformed string, replacing the content where it matches certain patterns. /// Typically used in web applications, where the rendering of an HTML page is provided as a template, and the application replaces certain statements, based on input parameters. public class StringTemplateInterpreter: TemplateInterpreter { /// The result of a template evaluation is a String public typealias EvaluatedType = String /// The main part of the evaluation happens here. In this case, the global context variables merged with the provided context are going to be used. /// - parameter expression: The input /// - parameter context: Local context that is going to be used with this expression only /// - returns: The output of the evaluation public override func evaluate(_ expression: String, context: Context) -> String { guard !expression.isEmpty else { return "" } return evaluate(expression, context: context, reducer: (initialValue: "", reduceValue: { existing, next in existing + next }, reduceCharacter: { existing, next in existing + String(next) })) } } /// A special kind of variable that is used in case of `StringTemplateInterpreter`s. It does not convert its content using the `interpreterForEvaluatingVariables` but always uses the `StringTemplateInterpreter` instance. /// It's perfect for expressions, that have a body, that needs to be further interpreted, such as an if or while statement. public class TemplateVariable: GenericVariable { /// No changes compared to the initialiser of the superclass `Variable`, uses the same parameters /// - parameter name: `GenericVariable`s have a name (unique identifier), that is used when matching and returning them in the matcher. /// - parameter options: Options that modify the behaviour of the variable matching, and the output that the framework provides /// - parameter map: If provided, then the result of the evaluated variable will be running through this map function /// Whether the processed variable sould be trimmed (removing whitespaces from both sides). Defaults to `true` public override init(_ name: String, options: VariableOptions = [], map: @escaping VariableMapper = { $0.value as? String }) { super.init(name, options: options.union(.notInterpreted)) { guard let stringValue = $0.value as? String else { return "" } let result = options.interpreted ? $0.interpreter.evaluate(stringValue) : stringValue return map(VariableBody(value: result, interpreter: $0.interpreter)) } } } ================================================ FILE: Sources/Eval/TypedInterpreter.swift ================================================ /* * Copyright (c) 2018 Laszlo Teveli. * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import Foundation /// A type of interpreter implementation that is capable of evaluating arbitrary string expressions to strongly typed variables public class TypedInterpreter: Interpreter, Printer { /// The result is a strongly typed value or `nil` (if it cannot be properly processed) public typealias EvaluatedType = Any? /// The global context used for every evaluation with this instance public let context: Context /// The interpreter used for evaluating variable values. In case of the `TypedInterpreter`, it's itself public lazy var interpreterForEvaluatingVariables: TypedInterpreter = { [unowned self] in self }() /// The data types that the expression is capable of recognise public let dataTypes: [DataTypeProtocol] /// The list of functions that are available during the evaluation to process the recognised data types public let functions: [FunctionProtocol] /// A cache of functions where expressions have matched before. This improves the performance a lot, when computing already established functions var functionCache: [String: FunctionProtocol] = [:] /// A cache of data types where expressions have matched before. This improves the performance a lot, when computing already established data types var dataTypeCache: [String: DataTypeProtocol] = [:] /// Each item of the input list (data types, functions and the context) is optional, but strongly recommended to provide them. It's usual that for every data type, there are a few functions provided, so the list can occasionally be pretty long. /// - parameter dataTypes: The types the interpreter should recognise the work with /// - parameter functions: The functions that can operate on the dataTypes /// - context: Global context that is going to be used with every expression evaluated with the current instance. Defaults to empty context public init(dataTypes: [DataTypeProtocol] = [], functions: [FunctionProtocol] = [], context: Context = Context()) { self.dataTypes = dataTypes self.functions = functions self.context = context } /// The evaluation method, that produces the strongly typed results. In this case, only the globally available context can be used /// - parameter expression: The input /// - returns: The output of the evaluation public func evaluate(_ expression: String) -> Any? { return evaluate(expression, context: Context()) } /// The evaluation method, that produces the strongly typed results. In this case, only the context is a result of merging the global context and the one provided in the parameter /// - parameter expression: The input /// - parameter context: Local context that is going to be used with this expression only /// - returns: The output of the evaluation public func evaluate(_ expression: String, context: Context) -> Any? { context.merge(with: self.context) { existing, _ in existing } let expression = expression.trim() let patterns = functions.compactMap { ($0 as? Function)?.patterns }.reduce([Pattern]()) { list, item in list + item } let connectedRanges = collectConnectedRanges(input: expression, statements: patterns) return functionFromCache(for: expression, using: context, connectedRanges: connectedRanges) ?? dataTypeFromCache(for: expression) ?? dataType(for: expression) ?? variable(for: expression, using: context) ?? function(for: expression, using: context, connectedRanges: connectedRanges) } /// If the expression belongs to a cached function, it uses the function converter to evaluate it /// - parameter expression: The expression to evaluate /// - parameter context: The context to be using when the evaluation happens /// - parameter connectedRanges: Ranges of string indices that are connected with opening-closing tag pairs, respectively /// - returns: The value - if the expression is interpreted. `nil` otherwise func functionFromCache(for expression: String, using context: Context, connectedRanges: [ClosedRange]) -> Any? { guard let cachedFunction = functionCache[expression], let value = cachedFunction.convert(input: expression, interpreter: self, context: context, connectedRanges: connectedRanges) else { return nil } return value } /// If the expression belongs to a cached data type, it uses the data type converter to evaluate it /// - parameter expression: The expression to evaluate /// - returns: The value - if the expression is interpreted. `nil` otherwise func dataTypeFromCache(for expression: String) -> Any? { guard let cachedDataType = dataTypeCache[expression], let value = cachedDataType.convert(input: expression, interpreter: self) else { return nil } return value } /// If the expression is recognised as a function, it uses that function to evaluate the value /// - parameter expression: The expression to evaluate /// - parameter context: The context to be using when the evaluation happens /// - parameter connectedRanges: Ranges of string indices that are connected with opening-closing tag pairs, respectively /// - returns: The value - if the expression is interpreted. `nil` otherwise func function(for expression: String, using context: Context, connectedRanges: [ClosedRange]) -> Any? { for function in functions.reversed() { if let value = function.convert(input: expression, interpreter: self, context: context, connectedRanges: connectedRanges) { functionCache[expression] = function return value } } return nil } /// If the expression is recognised as a data type, it uses that data type to convert its value /// - parameter expression: The expression to evaluate /// - parameter context: The context to be using when the evaluation happens /// - returns: The value - if the expression is interpreted. `nil` otherwise func dataType(for expression: String) -> Any? { for dataType in dataTypes { if let value = dataType.convert(input: expression, interpreter: self) { dataTypeCache[expression] = dataType return value } } return nil } /// If the expression is recognised as a variable, it uses that variable to replace its value /// - parameter expression: The expression to evaluate /// - parameter context: The context where the variables are stored /// - returns: The value - if the expression is interpreted. `nil` otherwise func variable(for expression: String, using context: Context) -> Any? { for variable in context.variables where expression == variable.key { return variable.value } return nil } /// A helper to be able to effectively print any result, coming out of the evaluation. The `print` method recognises the used data type and uses its string conversion block /// - parameter input: Any value that is a valid `DataType` or a `CustomStringConvertible` instance /// - returns: The string representation of the value or empty string if it cannot be processed public func print(_ input: Any) -> String { for dataType in dataTypes { if let value = dataType.print(value: input, printer: self) { return value } } if let input = input as? CustomStringConvertible { return input.description } return "" } } /// Data types tell the framework which kind of data can be parsed in the expressions public protocol DataTypeProtocol { /// If the framework meets with some static value that hasn't been processed before, it tries to convert it with every registered data type. /// This method returns nil if the conversion could not have been processed with any of the type's literals. /// - parameter input: The input to convert as a `DataType` value /// - parameter interpreter: An interpreter instance if the content needs any further evaluation /// - returns: The value of the `DataType` or `nil` if it cannot be processed func convert(input: String, interpreter: TypedInterpreter) -> Any? /// This is a convenience method, for debugging and value printing purposes, which can return a string from the current data type. /// It does not need to be unique or always the same for the same input values. /// - parameter input: Any value that is a valid `DataType` /// - parameter printer: An interpreter instance if the content recursively contains further data types to print /// - returns: The string representation of the value or `nil` if it cannot be processed func print(value input: Any, printer: Printer) -> String? } /// It's a data transfer object passed in the `DataType` converter block public struct DataTypeBody { /// Value of the data type to match public var value: T /// A printer instance to use to Stringify the value public var printer: Printer } /// The implementation of a `DataType` uses the `DataTypeProtocol` to convert input to a strongly typed data and print it if needed public class DataType: DataTypeProtocol { /// The existing type to map to an internal one let type: T.Type /// Array of literals that tell the framework how to transform certain types to an internal `DataType` representation let literals: [Literal] /// A method to convert an internal representation to strings - for debugging and output representation purposes /// - parameter body: Struct containing the value and a printer instance /// - parameter printer: An interpreter instance if the content recursively contains further data types to print private let print: (_ body: DataTypeBody) -> String /// To be able to bridge the outside world effectively, it needs to provide an already existing Swift or user-defined type. This can be class, struct, enum, or anything else, for example, block or function (which is not recommended). /// The literals tell the framework which strings can be represented in the given data type /// The last print block is used to convert the value of any DataType to a string value. It does not need to be unique or always the same for the same input values. /// - parameter type: The existing type to map to an internal one /// - parameter literals: Array of literals that tell the framework how to transform certain types to an internal `DataType` representation /// - parameter print: A method to convert an internal representation to strings - for debugging and output representation purposes /// - parameter body: Struct containing the value and a printer instance public init (type: T.Type, literals: [Literal], print: @escaping (_ body: DataTypeBody) -> String) { self.type = type self.literals = literals self.print = print } /// For the conversion it uses the registered literals, to be able to process the input and return an existing type /// - parameter input: The input to convert as a `DataType` value /// - parameter interpreter: An interpreter instance if the content needs any further evaluation /// - returns: The value of the `DataType` or `nil` if it cannot be processed public func convert(input: String, interpreter: TypedInterpreter) -> Any? { return literals.compactMap { $0.convert(input: input, interpreter: interpreter) }.first } /// This is a convenience method, for debugging and value printing purposes, which can return a string from the current data type. /// It does not need to be unique or always the same for the same input values. /// - parameter value: Any value that is a valid `DataType` /// - parameter printer: An interpreter instance if the content recursively contains further data types to print /// - returns: The string representation of the value or `nil` if it cannot be processed public func print(value input: Any, printer: Printer) -> String? { guard let input = input as? T else { return nil } return self.print(DataTypeBody(value: input, printer: printer)) } } /// It's a data transfer object passed in the `Literal` converter block public struct LiteralBody { /// Value of the literal to match public var value: String /// An interpreter instance if the raw value needs any further evaluation public var interpreter: TypedInterpreter /// - parameter value: Value of the literal to match /// - parameter interpreter: An interpreter instance if the raw value needs any further evaluation public init(value: String, interpreter: TypedInterpreter) { self.value = value self.interpreter = interpreter } } /// `Literal`s are used by `DataType`s to be able to recognise static values, that can be expressed as a given type public class Literal { /// For the conversion it uses the registered literals, to be able to process the input and return an existing type /// - parameter body: Struct ontaining the raw matched value and an interpreter /// - returns: The value of the `DataType` or `nil` if it cannot be processed let convert: (_ body: LiteralBody) -> T? /// In case of more complicated expression, this initialiser accepts a `convert` block, which can be used to process any value. Return nil, if the input cannot be accepted and converted. /// - parameter convert: The conversion block to process values /// - parameter body: Struct ontaining the raw matched value and an interpreter public init(convert: @escaping (_ body: LiteralBody) -> T?) { self.convert = convert } /// In case the literals are easily expressed, static keywords, then this initialiser is the best to use. /// - parameter check: The string to check for in the input string (with exact match) /// - parameter convertsTo: Statically typed associated value. As it is expressed as an autoclosure, the provided expression will be evaluated at recognition time, not initialisation time. For example, Date() is perfectly acceptable to use here. public init(_ check: String, convertsTo value: @autoclosure @escaping () -> T) { self.convert = { check == $0.value ? value() : nil } } /// For the conversion it uses the registered literals, to be able to process the input and return an existing type /// - parameter input: The input to convert as a `DataType` value /// - parameter interpreter: An interpreter instance if the content needs any further evaluation /// - returns: The value of the `DataType` or `nil` if it cannot be processed func convert(input: String, interpreter: TypedInterpreter) -> T? { return convert(LiteralBody(value: input, interpreter: interpreter)) } } /// `Function`s can process values in given `DataType`s, allowing the expressions to be feature-rich public protocol FunctionProtocol { /// Functions use similar conversion methods as `DataType`s. If they return `nil`, the function does not apply to the given input. Otherwise, the result is expressed as an instance of a given `DataType` /// It uses the interpreter the and parsing context to be able to effectively process the content /// - parameter input: The input to convert as a `DataType` value /// - parameter interpreter: An interpreter instance if the content needs any further evaluation /// - parameter context: The context - if vaiables need any contextual information /// - parameter connectedRanges: Ranges of string indices that are connected with opening-closing tag pairs, respectively /// - returns: A valid value of any `DataType` or `nil` if it cannot be processed func convert(input: String, interpreter: TypedInterpreter, context: Context, connectedRanges: [ClosedRange]) -> Any? } /// `Function`s can process values in given `DataType`s, allowing the expressions to be feature-rich public class Function: FunctionProtocol { /// Although `Function`s typically contain only one pattern, multiple ones can be added, for semantic grouping purposes public let patterns: [Pattern] /// If multiple patterns are provided use this initialiser. Otherwise, for only one, there is `init(_,matcher:)` /// - parameter patterns: The array of patterns to be able to recognise public init(patterns: [Pattern]) { self.patterns = patterns } /// In case there is only one pattern, this initialiser is the preferred one to use /// - parameter elements: Contains the pattern that needs to be recognised /// - parameter options: Options that modify the pattern matching algorithm /// - parameter matcher: Ending closure that transforms and processes the recognised value public init(_ elements: [PatternElement], options: PatternOptions = [], matcher: @escaping MatcherBlock) { self.patterns = [Pattern(elements, options: options, matcher: matcher)] } /// The matching of the input expression of a given `Function` happens in this method. It only accepts matches from the matcher, that are exact matches. /// - parameter input: The input to convert as a `DataType` value /// - parameter interpreter: An interpreter instance if the content needs any further evaluation /// - parameter context: The context - if vaiables need any contextual information /// - parameter connectedRanges: Ranges of string indices that are connected with opening-closing tag pairs, respectively /// - returns: A valid value of any `DataType` or `nil` if it cannot be processed public func convert(input: String, interpreter: TypedInterpreter, context: Context, connectedRanges: [ClosedRange] = []) -> Any? { guard case let .exactMatch(_, output, _) = matchStatement(amongst: patterns, in: input, interpreter: interpreter, context: context, connectedRanges: connectedRanges) else { return nil } return output } } /// `Variable` represents a named placeholder, so when the matcher recognises a pattern, the values of the variables are passed to them in a block. public class Variable: GenericVariable { } ================================================ FILE: Sources/Eval/Utilities/MatchResult.swift ================================================ /* * Copyright (c) 2018 Laszlo Teveli. * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import Foundation /// Whenever a match operation is performed, the result is going to be a `MatchResult` instance. public enum MatchResult { /// The input could not be matched case noMatch /// The input can match, if it were continued. (It's the prefix of the matching expression) case possibleMatch /// The input matches the expression. It provides information about the `length` of the matched input, the `output` after the evaluation, and the `variables` that were processed during the process. /// - parameter length: The length of the match in the input string /// - parameter output: The interpreted content /// - parameter variables: The key-value pairs of the found `Variable` instances along the way case exactMatch(length: Int, output: T, variables: [String: Any]) /// In case the matching sequence only consists of one variable, the result is going to be anyMatch /// - parameter exhaustive: Whether the matching should be exaustive or just return the shortest matching result case anyMatch(exhaustive: Bool) /// Shorter syntax for pattern matching `MatchResult.exactMatch` /// - returns: Whether the case of the current instance is `exactMatch` func isMatch() -> Bool { if case .exactMatch(_, _, _) = self { return true } return false } /// Shorter syntax for pattern matching `MatchResult.anyMatch` /// - parameter exhaustive: If the result is `anyMatch`, this one filter the content by its exhaustive parameter - if provided. Uses `false` otherwise /// - returns: Whether the case of the current instance is `anyMatch` func isAnyMatch(exhaustive: Bool = false) -> Bool { if case .anyMatch(let parameter) = self { return exhaustive == parameter } return false } /// Shorter syntax for pattern matching `MatchResult.noMatch` /// - returns: Whether the case of the current instance is `noMatch` func isNoMatch() -> Bool { if case .noMatch = self { return true } return false } /// Shorter syntax for pattern matching `MatchResult.anypossibleMatch` /// - returns: Whether the case of the current instance is `possibleMatch` func isPossibleMatch() -> Bool { if case .possibleMatch = self { return true } return false } } /// `MatchResult` with Equatable objects are also Equatable public extension MatchResult where T: Equatable { /// `MatchResult` with Equatable objects are also Equatable /// - parameter lhs: Left hand side /// - parameter rhs: Right hand side /// - returns: Whether the `MatchResult` have the same values, including the contents of their associated objects static func == (lhs: MatchResult, rhs: MatchResult) -> Bool { switch (lhs, rhs) { case (.noMatch, .noMatch), (.possibleMatch, .possibleMatch): return true case let (.anyMatch(lhsShortest), .anyMatch(rhsShortest)): return lhsShortest == rhsShortest case let (.exactMatch(lhsLength, lhsOutput, lhsVariables), .exactMatch(rhsLength, rhsOutput, rhsVariables)): return lhsLength == rhsLength && lhsOutput == rhsOutput && (lhsVariables as NSDictionary).isEqual(to: rhsVariables) default: return false } } } ================================================ FILE: Sources/Eval/Utilities/Matcher.swift ================================================ /* * Copyright (c) 2018 Laszlo Teveli. * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import Foundation /// A tuple with the variable metadata and its value /// - parameter metadata: Name, options, mapping information /// - parameter value: The value of the variable internal typealias VariableValue = (metadata: VariableProtocol, value: String) /// A processor that can process a raw value with extra information, such as interpreter and context internal protocol VariableProcessorProtocol { /// The method that can process the variable /// - parameter variable: The raw value to process /// - returns: The computed value of the variable func process(_ variable: VariableValue) -> Any? } /// A processor that can process a raw value with extra information, such as interpreter and context internal class VariableProcessor: VariableProcessorProtocol { /// An interpreter instance to use during the processing let interpreter: E /// The context to use during the processing let context: Context /// Initialiser of the processor /// - parameter interpreter: An interpreter instance to use during the processing /// - parameter context: The context to use during the processing init(interpreter: E, context: Context) { self.interpreter = interpreter self.context = context } /// Maps and evaluates variable content, based on its interpretation settings /// - parameter variable: The variable to process /// - returns: The result of the matching operation func process(_ variable: VariableValue) -> Any? { let value = variable.metadata.options.trimmed ? variable.value.trim() : variable.value if variable.metadata.options.interpreted { let variableInterpreter = interpreter.interpreterForEvaluatingVariables let output = variableInterpreter.evaluate(value, context: context) return variable.metadata.performMap(input: output, interpreter: variableInterpreter) } return variable.metadata.performMap(input: value, interpreter: interpreter) } } /// This class provides the main logic of the `Eval` framework, performing the pattern matching details internal class Matcher { /// The pattern to match against let pattern: PatternProtocol /// A processor that is able to evaluate the variables with extra information, such as context and interpreter let processor: VariableProcessorProtocol /// Initialiser of the matcher /// - parameter pattern: The pattern to match against /// - parameter processor: A processor that is able to evaluate the variables with extra information, such as context and interpreter init(pattern: PatternProtocol, processor: VariableProcessorProtocol) { self.pattern = pattern self.processor = processor } /// The active variable that can be appended during the execution, used by the helper methods private var currentlyActiveVariable: VariableValue? /// Tries to append the next input character to the currently active variables - if we have any /// - returns: Whether the append was successful private func tryToAppendCurrentVariable(remainder: inout String) -> Bool { if let variable = currentlyActiveVariable { appendNextCharacterToVariable(variable, remainder: &remainder) } return currentlyActiveVariable != nil } /// Appends the next character to the provided variables /// - parameter variable: The variable to append to /// - parameter remainder: The remainder of the evaluated input private func appendNextCharacterToVariable(_ variable: VariableValue, remainder: inout String) { if remainder.isEmpty { currentlyActiveVariable = (variable.metadata, variable.value) } else { if pattern.options.contains(.backwardMatch) { currentlyActiveVariable = (variable.metadata, String(describing: remainder.removeLast()) + variable.value) } else { currentlyActiveVariable = (variable.metadata, variable.value + String(describing: remainder.removeFirst())) } } } /// An element to initialise the variable with /// - parameter element: The variable element private func initialiseVariable(_ element: PatternElement) { if currentlyActiveVariable == nil, let variable = element as? VariableProtocol { currentlyActiveVariable = (variable, "") } } /// When the recognition of a variable arrives to the final stage, function finalises its value and appends the variables array /// - returns: Whether the registration was successful (the finalisation resulted in a valid value) private func registerAndValidateVariable(variables: inout [String: Any]) -> Bool { if let variable = currentlyActiveVariable { let result = processor.process(variable) variables[variable.metadata.name] = result return !variable.metadata.options.contains(.acceptsNilValue) && result != nil } return false } /// Increments the elementIndex value /// - parameter elementIndex: The index to be incremented private func nextElement(_ elementIndex: inout Int) { elementIndex += pattern.options.contains(.backwardMatch) ? -1 : 1 } /// Checks whether the current index is the last one /// - parameter elementIndex: The index to be checked /// - returns: Whether the index is the last one of the elements array private func notFinished(_ elementIndex: Int) -> Bool { if pattern.options.contains(.backwardMatch) { return elementIndex >= pattern.elements.startIndex } else { return elementIndex < pattern.elements.endIndex } } /// Helper method to determine the first index of the collection, based on its options /// - returns: The first index of the collection private func initialIndex() -> Int { if pattern.options.contains(.backwardMatch) { return pattern.elements.index(before: pattern.elements.endIndex) } else { return pattern.elements.startIndex } } /// Removes and returns the next character from the input /// - parameter remainder: The remainder of the input /// - parameter length: The number of characters to be removed /// - returns: The last few characers from the input, defined by the `length` parameter private func drop(_ remainder: String, length: Int) -> String { if pattern.options.contains(.backwardMatch) { return String(remainder.dropLast(length)) } else { return String(remainder.dropFirst(length)) } } /// Removes whitespaces characters from the upcoming consecutive input characters /// - parameter remainder: The input to remove whitespaces from private func skipWhitespaces(_ remainder: inout String) { let whitespaces = CharacterSet.whitespacesAndNewlines repeat { if pattern.options.contains(.backwardMatch), let last = remainder.last?.unicodeScalars.first, whitespaces.contains(last) { _ = remainder.removeLast() } else if let first = remainder.first?.unicodeScalars.first, whitespaces.contains(first) { _ = remainder.removeFirst() } else { break } } while true } /// Removes whitespaces characters from the upcoming consecutive input characters, when the context allows to do so /// - parameter remainder: The input to remove whitespaces from /// - parameter index: The index of the current element private func skipWhitespacesIfNeeded(_ remainder: inout String, index: Int) { var shouldTrim = false if let variable = currentlyActiveVariable { shouldTrim = variable.metadata.options.trimmed } else if index < pattern.elements.endIndex && pattern.elements[index] is Keyword { shouldTrim = true } if shouldTrim && notFinished(index) { skipWhitespaces(&remainder) } } /// This match method provides the main logic of the `Eval` framework, performing the pattern matching, trying to identify, whether the input string is somehow related, or completely matches the pattern. /// - parameter string: The input /// - parameter from: The start of the range to analyse the result in /// - parameter connectedRanges: Ranges of string indices that are connected with opening-closing tag pairs, respectively /// - parameter renderer: If the result is an exactMatch, it uses this renderer block to compute the output based on the matched variables /// - parameter variables: The set of variables collected during the execution /// - returns: The result of the matching operation // swiftlint:disable:next cyclomatic_complexity function_body_length func match(string: String, from start: String.Index?, connectedRanges: [ClosedRange] = [], renderer: @escaping (_ variables: [String: Any]) -> T?) -> MatchResult { let start = start ?? string.startIndex let trimmed = String(string[start...]) var elementIndex = initialIndex() var remainder = trimmed var variables: [String: Any] = [:] repeat { let element = pattern.elements[elementIndex] let result = element.matches(prefix: remainder, options: pattern.options) switch result { case .noMatch: guard tryToAppendCurrentVariable(remainder: &remainder) else { return .noMatch } case .possibleMatch: return .possibleMatch case .anyMatch(let exhaustive): initialiseVariable(element) if exhaustive { _ = tryToAppendCurrentVariable(remainder: &remainder) if remainder.isEmpty { guard registerAndValidateVariable(variables: &variables) else { return .possibleMatch } nextElement(&elementIndex) } } else { nextElement(&elementIndex) } case let .exactMatch(length, _, embeddedVariables): let position = pattern.options.contains(.backwardMatch) ? remainder.endIndex : trimmed.index(trimmed.endIndex, offsetBy: -remainder.count) let isOpeningOrClosingKeyword = (element as? Keyword)?.type != .generic if isEmbedded(element: element, in: String(string[start...]), at: position) { if currentlyActiveVariable != nil { _ = tryToAppendCurrentVariable(remainder: &remainder) } else { nextElement(&elementIndex) } } else if connectedRanges.contains(where: { $0.contains(position) }) && !isOpeningOrClosingKeyword { if currentlyActiveVariable != nil { _ = tryToAppendCurrentVariable(remainder: &remainder) } } else { variables.merge(embeddedVariables) { key, _ in key } if currentlyActiveVariable != nil { guard registerAndValidateVariable(variables: &variables) else { return .noMatch } currentlyActiveVariable = nil } nextElement(&elementIndex) remainder = drop(remainder, length: length) if elementIndex < pattern.elements.count && element is Keyword { skipWhitespacesIfNeeded(&remainder, index: elementIndex) } } } } while notFinished(elementIndex) if let renderedOutput = renderer(variables) { return .exactMatch(length: string.count - string.distance(from: string.startIndex, to: start) - remainder.count, output: renderedOutput, variables: variables) } else { return .noMatch } } /// Determines whether the current character is an `OpenKeyword`, so there might be another embedded match later /// - parameter element: The element to check whether it's an `OpenKeyword` /// - parameter in: The input /// - parameter at: The starting position to check from /// - returns: Whether the element conditions apply and the position is before the last one func isEmbedded(element: PatternElement, in string: String, at currentPosition: String.Index) -> Bool { if let closingTag = element as? Keyword, closingTag.type == .closingStatement, let closingPosition = positionOfClosingTag(in: string, from: string.startIndex), currentPosition < closingPosition { return true } return false } /// Determines whether the current character is an `OpenKeyword` and fins the position of its appropriate `ClosingKeyword` pair /// - parameter in: The input /// - parameter from: The starting position of the checking range /// - returns: `nil` if the `CloseKeyword` pair cannot be found. The position otherwise func positionOfClosingTag(in string: String, from start: String.Index) -> String.Index? { if let opening = pattern.elements.first(where: { ($0 as? Keyword)?.type == .openingStatement }) as? Keyword, let closing = pattern.elements.first(where: { ($0 as? Keyword)?.type == .closingStatement }) as? Keyword { var counter = 0 var position = start repeat { var isCloseTagEarlier = false let relevantString = string[position...] let open = relevantString.range(of: opening.name)?.lowerBound let close = relevantString.range(of: closing.name)?.lowerBound if let open = open, let close = close, close < open { isCloseTagEarlier = true } if let open = open, !isCloseTagEarlier { counter += 1 position = string.index(open, offsetBy: opening.name.count) } else if let close = close { counter -= 1 if counter == 0 { return close } position = string.index(close, offsetBy: closing.name.count) } else { break } } while true } return nil } } ================================================ FILE: Sources/Eval/Utilities/Pattern.swift ================================================ /* * Copyright (c) 2018 Laszlo Teveli. * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import Foundation /// It's a data transfer object passed in the `Pattern` matcher block public struct PatternBody { /// The key-value pairs of the `Variable` instances found along the way public var variables: [String: Any] /// The evaluator instance to help parsing the content public var interpreter: E /// The context if the matcher block needs any contextual information public var context: Context } /// `MatcherBlock` is used by `Matcher` and `Function` classes when the matched expression should be processed in a custom way. It should return a strongly typed object after the evaluations. /// The first parameter contains the values of every matched `Variable` instance. /// The second parameter is the evaluator. If there is a need to process the value of the variable further, creators of this block can use this evaluator, whose value is always the interpreter currently in use /// In its last parameter if provides information about the context, and therefore allows access to read or modify the context variables. /// - parameter body: Struct containing the matched variables, an interpreter, and a context /// - returns: The converted value public typealias MatcherBlock = (_ body: PatternBody) -> T? /// Options that modify the pattern matching algorithm public struct PatternOptions: OptionSet { /// Integer representation of the option public let rawValue: Int /// Basic initialiser with the integer representation public init(rawValue: Int) { self.rawValue = rawValue } /// Searches of the elements of the pattern backward from the end of the output. Othwerise, if not present, it matches from the beginning. public static let backwardMatch: PatternOptions = PatternOptions(rawValue: 1 << 0) } /// Pattern consists of array of elements public protocol PatternProtocol { /// `Matcher` instances are capable of recognising patterns described in the `elements` collection. It only remains effective, if the `Variable` instances are surrounded by `Keyword` instances, so no two `Variable`s should be next to each other. Otherwise, their matching result and value would be undefined. /// This collection should be provided during the initialisation, and cannot be modified once the `Matcher` instance has been created. var elements: [PatternElement] { get } /// Options that modify the pattern matching algorithm var options: PatternOptions { get } /// Optional name to identify the pattern. If not provided during initialisation, it will fall back to the textual representation of the elements array var name: String { get } } /// Pattern consists of array of elements public class Pattern: PatternProtocol { /// `Matcher` instances are capable of recognising patterns described in the `elements` collection. It only remains effective, if the `Variable` instances are surrounded by `Keyword` instances, so no two `Variable`s should be next to each other. Otherwise, their matching result and value would be undefined. /// This collection should be provided during the initialisation, and cannot be modified once the `Matcher` instance has been created. public let elements: [PatternElement] /// The block to process the elements with let matcher: MatcherBlock /// Options that modify the pattern matching algorithm public let options: PatternOptions /// Optional name to identify the pattern. If not provided during initialisation, it will fall back to the textual representation of the elements array public let name: String /// The first parameter is the pattern, that needs to be recognised. The `matcher` ending closure is called whenever the pattern has successfully been recognised and allows the users of this framework to provide custom computations using the matched `Variable` values. /// - parameter elemenets: The pattern to recognise /// - parameter name: Optional identifier for the pattern. Defaults to the string representation of the elements /// - parameter options: Options that modify the pattern matching algorithm /// - parameter matcher: The block to process the input with public init(_ elements: [PatternElement], name: String? = nil, options: PatternOptions = [], matcher: @escaping MatcherBlock) { self.name = name ?? Pattern.stringify(elements: elements) self.matcher = matcher self.options = options self.elements = Pattern.elementsByReplacingTheLastVariableNotToBeShortestMatch(in: elements, options: options) } /// If the last element in the elements pattern is a variable, shortest match will not match until the end of the input string, but just until the first empty character. /// - parameter in: The elements array where the last element should be replaced /// - parameter options: Options that modify the pattern matching algorithm /// - returns: A new collection of elements, where the last element is replaced, whether it's a variable with shortest flag on static func elementsByReplacingTheLastVariableNotToBeShortestMatch(in elements: [PatternElement], options: PatternOptions) -> [PatternElement] { var elements = elements let index = options.contains(.backwardMatch) ? elements.startIndex : elements.index(before: elements.endIndex) /// Replaces the last element in the elements collection with the new one in the parmeter /// - parameter element: The element to be replaced /// - parameter new: The replacement /// - parameter previousOptions: The element to be replaced func replaceLast(_ element: VariableProtocol, with new: (_ previousOptions: VariableOptions) -> PatternElement) { elements.remove(at: index) elements.insert(new(element.options.union(.exhaustiveMatch)), at: index) } if let last = elements[index] as? GenericVariable, !last.options.contains(.exhaustiveMatch) { replaceLast(last) { GenericVariable(last.name, options: $0, map: last.map) } } else if let last = elements[index] as? VariableProtocol, !last.options.contains(.exhaustiveMatch) { //in case it cannot be converted, let's use Any. Losing type information replaceLast(last) { GenericVariable(last.name, options: $0) { last.performMap(input: $0.value, interpreter: $0.interpreter) } } } return elements } /// This matcher provides the main logic of the `Eval` framework, performing the pattern matching, trying to identify, whether the input string is somehow related, or completely matches the pattern of the `Pattern` instance. /// Uses the `Matcher` class for the evaluation /// - parameter string: The input /// - parameter from: The start of the range to analyse the result in /// - parameter interpreter: An interpreter instance - if the variables need any further evaluation /// - parameter context: The context - if the block uses any contextual data /// - parameter connectedRanges: Ranges of string indices that are connected with opening-closing tag pairs, respectively /// - returns: The result of the matching operation func matches(string: String, from start: String.Index? = nil, interpreter: I, context: Context, connectedRanges: [ClosedRange] = []) -> MatchResult { let start = start ?? string.startIndex let processor = VariableProcessor(interpreter: interpreter, context: context) let matcher = Matcher(pattern: self, processor: processor) let result = matcher.match(string: string, from: start, connectedRanges: connectedRanges) { variables in self.matcher(PatternBody(variables: variables, interpreter: interpreter, context: context)) } if case let .exactMatch(_, output, variables) = result { let input = String(string[start...]) context.debugInfo[input] = ExpressionInfo(input: input, output: output, pattern: elementsAsString(), patternName: name, variables: variables) } return result } /// A textual representation of the Pattern's elements array /// - returns: A stringified version of the input elements func elementsAsString() -> String { return Pattern.stringify(elements: elements) } /// A textual representation of the elements array /// - returns: A stringified version of the input elements static func stringify(elements: [PatternElement]) -> String { return elements.map { if let keyword = $0 as? Keyword { return keyword.name } else if let variable = $0 as? VariableProtocol { return "{\(variable.name)}" } return "" }.joined(separator: " ") } } ================================================ FILE: Sources/Eval/Utilities/Utils.swift ================================================ /* * Copyright (c) 2018 Laszlo Teveli. * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import Foundation /// Syntactic sugar for `MatchElement` instances to feel like concatenation, whenever the input requires an array of elements. /// - parameter left: Left hand side /// - parameter right: Right hand side /// - returns: An array with two elements (left and right in this order) public func + (left: PatternElement, right: PatternElement) -> [PatternElement] { return [left, right] } /// Syntactic sugar for appended arrays /// - parameter array: The array to append /// - parameter element: The appended element /// - returns: A new array by appending `array` with `element` internal func +
(array: [A], element: A) -> [A] { return array + [element] } /// Syntactic sugar for appending mutable arrays /// - parameter array: The array to append /// - parameter element: The appended element internal func += (array: inout [A], element: A) { array = array + element //swiftlint:disable:this shorthand_operator } /// Helpers on `String` to provide `Int` based subscription features and easier usage extension String { /// Shorter syntax for trimming /// - returns: The `String` without the prefix and postfix whitespace characters func trim() -> String { return trimmingCharacters(in: .whitespacesAndNewlines) } } ================================================ FILE: Tests/.swiftlint.yml ================================================ disabled_rules: - force_cast - force_unwrapping - file_header - type_name - explicit_top_level_acl ================================================ FILE: Tests/EvalTests/IntegrationTests/InterpreterTests.swift ================================================ @testable import Eval import class Eval.Pattern import XCTest class InterpreterTests: XCTestCase { // swiftlint:disable:next function_body_length func test_whenAddingALotOfFunctions_thenInterpretationWorksCorrectly() { let number = numberDataType() let string = stringDataType() let date = dateDataType() let array = arrayDataType() let boolean = booleanDataType() let methodCall = methodCallFunction() let max = objectFunction("max") { (object: [Double]) -> Double? in object.max() } let min = objectFunction("min") { (object: [Double]) -> Double? in object.min() } let sum = objectFunction("sum") { (object: [Double]) -> Double? in object.reduce(0, +) } let format = dateFomatFunction() let dateFactory = dateFactoryFunction() let not = prefixOperator("!") { (value: Bool) in !value } let not2 = function("not") { (arguments: [Any]) -> Bool? in guard let boolArgument = arguments.first as? Bool else { return nil } return !boolArgument } let sqrtFunction = function("sqrt") { (arguments: [Any]) -> Double? in guard let argument = arguments.first as? Double else { return nil } return sqrt(argument) } let add = function("add") { (arguments: [Any]) -> Double? in guard let arguments = arguments as? [Double] else { return nil } return arguments.reduce(0, +) } let test = functionWithNamedParameters("test") { (arguments: [String: Any]) -> Double? in guard let foo = arguments["foo"] as? Double, let bar = arguments["bar"] as? Double else { return nil } return foo + bar } let parenthesis = Function([Keyword("("), Variable("body"), Keyword(")")]) { $0.variables["body"] } let plusOperator = infixOperator("+") { (lhs: Double, rhs: Double) in lhs + rhs } let concat = infixOperator("+") { (lhs: String, rhs: String) in lhs + rhs } let multipicationOperator = infixOperator("*") { (lhs: Double, rhs: Double) in lhs * rhs } let inArrayNumber = infixOperator("in") { (lhs: Double, rhs: [Double]) in rhs.contains(lhs) } let inArrayString = infixOperator("in") { (lhs: String, rhs: [String]) in rhs.contains(lhs) } let range = infixOperator("...") { (lhs: Double, rhs: Double) in CountableClosedRange(uncheckedBounds: (lower: Int(lhs), upper: Int(rhs))).map { Double($0) } } let prefix = infixOperator("starts with") { (lhs: String, rhs: String) in lhs.hasPrefix(rhs) } let isOdd = suffixOperator("is odd") { (value: Double) in Int(value) % 2 == 1 } let isEven = suffixOperator("is even") { (value: Double) in Int(value) % 2 == 0 } let lessThan = infixOperator("<") { (lhs: Double, rhs: Double) in lhs < rhs } let increment = incrementFunction() let interpreter = TypedInterpreter(dataTypes: [number, string, boolean, array, date], functions: [concat, parenthesis, methodCall, sum, range, sqrtFunction, dateFactory, format, multipicationOperator, plusOperator, inArrayNumber, inArrayString, isOdd, isEven, add, max, min, not, not2, prefix, increment, lessThan, test], context: Context(variables: ["test": 2.0, "name": "Teve"])) XCTAssertEqual(interpreter.evaluate("123") as! Double, 123) XCTAssertEqual(interpreter.evaluate("1 + 2 + 3") as! Double, 6) XCTAssertEqual(interpreter.evaluate("2 + 3 * 4") as! Double, 14) XCTAssertEqual(interpreter.evaluate("2 * 3 + 4") as! Double, 10) XCTAssertEqual(interpreter.evaluate("2 * (3 + 4)") as! Double, 14) XCTAssertEqual(interpreter.evaluate("(3 + 4) * 2") as! Double, 14) XCTAssertEqual(interpreter.evaluate("'hello'") as! String, "hello") XCTAssertEqual(interpreter.evaluate("false") as! Bool, false) XCTAssertEqual(interpreter.evaluate("!false") as! Bool, true) XCTAssertEqual(interpreter.evaluate("not(false)") as! Bool, true) XCTAssertEqual(interpreter.evaluate("true") as! Bool, true) XCTAssertEqual(interpreter.evaluate("add(1,2)") as! Double, 3) XCTAssertEqual(interpreter.evaluate("[1,2]") as! [Double], [1, 2]) XCTAssertEqual(interpreter.evaluate("['1','2']") as! [String], ["1", "2"]) XCTAssertEqual(interpreter.evaluate("[true, false]") as! [Bool], [true, false]) XCTAssertEqual(interpreter.evaluate("[1,2].count") as! Double, 2) XCTAssertEqual(interpreter.evaluate("'hello'.length") as! Double, 5) XCTAssertEqual(interpreter.evaluate("[0,3,1,2].max") as! Double, 3) XCTAssertEqual(interpreter.evaluate("pi * 2") as! Double, Double.pi * 2) XCTAssertEqual(interpreter.evaluate("1 in [3,2,1,2,3]") as! Bool, true) XCTAssertEqual(interpreter.evaluate("not(1 in [1])") as! Bool, false) XCTAssertEqual(interpreter.evaluate("'b' in ['a','c','d']") as! Bool, false) XCTAssertEqual(interpreter.evaluate("1...5") as! [Double], [1, 2, 3, 4, 5]) XCTAssertEqual(interpreter.evaluate("[1, test]") as! [Double], [1, 2]) XCTAssertEqual(interpreter.evaluate("2 in 1...5") as! Bool, true) XCTAssertEqual(interpreter.evaluate("5 is odd") as! Bool, true) XCTAssertEqual(interpreter.evaluate("2 is odd") as! Bool, false) XCTAssertEqual(interpreter.evaluate("4 is even") as! Bool, true) XCTAssertEqual(interpreter.evaluate("2 < 3") as! Bool, true) XCTAssertEqual(interpreter.evaluate("'Teve' starts with 'T'") as! Bool, true) XCTAssertEqual(interpreter.evaluate("'Hello ' + name") as! String, "Hello Teve") XCTAssertEqual(interpreter.evaluate("12++") as! Double, 13) XCTAssertEqual(interpreter.evaluate("(test + 2)++") as! Double, 5) XCTAssertEqual(interpreter.evaluate("test++") as! Double, 3) XCTAssertEqual(interpreter.evaluate("test") as! Double, 3) XCTAssertEqual(interpreter.evaluate("sqrt(4)") as! Double, 2) XCTAssertNotNil(interpreter.evaluate("now.format('yyyy-MM-dd')")) XCTAssertEqual(interpreter.evaluate("Date(2018, 12, 13).format('yyyy-MM-dd')") as! String, "2018-12-13") XCTAssertEqual(interpreter.evaluate("test(foo=1, bar=2)") as! Double, 3) XCTAssertNil(interpreter.evaluate("add(1,'a')")) XCTAssertNil(interpreter.evaluate("hello")) let context = Context() _ = interpreter.evaluate("Date(1009 * 2, sqrt(144), 10 + 3).format('yyyy-MM-dd')", context: context) context.debugInfo.forEach { print("DEBUG STEP: '\($0.value.pattern)', where \($0.value.variables), rendered to \($0.value.output) from input \($0.key)") } let ifStatement = Pattern>([Keyword("{%"), Keyword("if"), Variable("condition"), Keyword("%}"), TemplateVariable("body"), Keyword("{% endif %}")]) { guard let condition = $0.variables["condition"] as? Bool, let body = $0.variables["body"] as? String else { return nil } if condition { return body } return nil } let printStatement = Pattern>([Keyword("{{"), Variable("body"), Keyword("}}")]) { guard let body = $0.variables["body"] else { return nil } return $0.interpreter.typedInterpreter.print(body) } let template = StringTemplateInterpreter(statements: [ifStatement, printStatement], interpreter: interpreter, context: Context()) XCTAssertEqual(template.evaluate("{{ 1 + 2 }}"), "3.0") XCTAssertEqual(template.evaluate("{{ 'Hello' + ' ' + 'World' + '!' }}"), "Hello World!") XCTAssertEqual(template.evaluate("asd {% if 10 < 21 %}Hello{% endif %} asd"), "asd Hello asd") XCTAssertEqual(template.evaluate("asd {% if 10 < 21 %}{{ 'Hello ' + name }}{% endif %} asd"), "asd Hello Teve asd") } func test_whenEmbeddingTags_thenInterpretationWorksCorrectly() { let parenthesis = Function([OpenKeyword("("), Variable("body"), CloseKeyword(")")]) { $0.variables["body"] } let addition = infixOperator("+") { (lhs: Double, rhs: Double) in lhs + rhs } let interpreter = TypedInterpreter(dataTypes: [numberDataType(), stringDataType()], functions: [parenthesis, addition]) XCTAssertEqual(interpreter.evaluate("(1)") as! Double, 1) XCTAssertEqual(interpreter.evaluate("(1) + (2)") as! Double, 3) XCTAssertEqual(interpreter.evaluate("(1 + (2))") as! Double, 3) XCTAssertEqual(interpreter.evaluate("(1 + 2)") as! Double, 3) XCTAssertEqual(interpreter.evaluate("((1) + 2)") as! Double, 3) XCTAssertEqual(interpreter.evaluate("((1) + (2))") as! Double, 3) } func test_whenEmbeddingTagsWithNonCummitativeOperation_thenInterpretationWorksCorrectly() { let parenthesis = Function([OpenKeyword("("), Variable("body"), CloseKeyword(")")]) { $0.variables["body"] } let addition = infixOperator("+") { (lhs: Double, rhs: Double) in lhs + rhs } let subtraction = infixOperator("-") { (lhs: Double, rhs: Double) in lhs - rhs } let interpreter = TypedInterpreter(dataTypes: [numberDataType(), stringDataType()], functions: [parenthesis, subtraction, addition]) XCTAssertEqual(interpreter.evaluate("6 - 4 - 2") as! Double, 0) XCTAssertEqual(interpreter.evaluate("6 - (4 + 2)") as! Double, 0) XCTAssertEqual(interpreter.evaluate("6 - (4 - 2)") as! Double, 4) XCTAssertEqual(interpreter.evaluate("12 - (6 - (4 - 2))") as! Double, 8) } func test_whenStartsWithParentheses_thenInterpretationWorksCorrectly() { let parenthesis = Function([OpenKeyword("("), Variable("body"), CloseKeyword(")")]) { $0.variables["body"] } let addition = infixOperator("+") { (lhs: Double, rhs: Double) in lhs + rhs } let multiplication = infixOperator("*") { (lhs: Double, rhs: Double) in lhs * rhs } let interpreter = TypedInterpreter(dataTypes: [numberDataType(), stringDataType()], functions: [parenthesis, multiplication, addition]) XCTAssertEqual(interpreter.evaluate("(2 + 3)") as! Double, 5) XCTAssertEqual(interpreter.evaluate("(2 + 3) * 4") as! Double, 20) } // MARK: Helpers - operators func infixOperator(_ symbol: String, body: @escaping (A, B) -> T) -> Function { return Function([Variable("lhs"), Keyword(symbol), Variable("rhs")], options: .backwardMatch) { guard let lhs = $0.variables["lhs"] as? A, let rhs = $0.variables["rhs"] as? B else { return nil } return body(lhs, rhs) } } func prefixOperator(_ symbol: String, body: @escaping (A) -> T) -> Function { return Function([Keyword(symbol), Variable("value")]) { guard let value = $0.variables["value"] as? A else { return nil } return body(value) } } func suffixOperator(_ symbol: String, body: @escaping (A) -> T) -> Function { return Function([Variable("value"), Keyword(symbol)]) { guard let value = $0.variables["value"] as? A else { return nil } return body(value) } } func function(_ name: String, body: @escaping ([Any]) -> T?) -> Function { return Function([Keyword(name), OpenKeyword("("), Variable("arguments", options: .notInterpreted), CloseKeyword(")")]) { match in guard let arguments = match.variables["arguments"] as? String else { return nil } let interpretedArguments: [Any] = arguments.split(separator: ",").compactMap { match.interpreter.evaluate(String($0).trim(), context: match.context) } return body(interpretedArguments) } } func functionWithNamedParameters(_ name: String, body: @escaping ([String: Any]) -> T?) -> Function { return Function([Keyword(name), Keyword("("), Variable("arguments", options: .notInterpreted), Keyword(")")]) { guard let arguments = $0.variables["arguments"] as? String else { return nil } var interpretedArguments: [String: Any] = [:] for argument in arguments.split(separator: ",") { let parts = String(argument).trim().split(separator: "=") if let key = parts.first, let value = parts.last { interpretedArguments[String(key)] = $0.interpreter.evaluate(String(value)) } } return body(interpretedArguments) } } func objectFunction(_ name: String, body: @escaping (O) -> T?) -> Function { return Function([Variable("lhs"), Keyword("."), Variable("rhs", options: .notInterpreted) { guard let value = $0.value as? String, value == name else { return nil } return value }]) { guard let object = $0.variables["lhs"] as? O, $0.variables["rhs"] != nil else { return nil } return body(object) } } func objectFunctionWithParameters(_ name: String, body: @escaping (O, [Any]) -> T?) -> Function { return Function([Variable("lhs"), Keyword("."), Variable("rhs", options: .notInterpreted) { guard let value = $0.value as? String, value == name else { return nil } return value }, Keyword("("), Variable("arguments", options: .notInterpreted), Keyword(")")]) { match in guard let object = match.variables["lhs"] as? O, match.variables["rhs"] != nil, let arguments = match.variables["arguments"] as? String else { return nil } let interpretedArguments = arguments.split(separator: ",").compactMap { match.interpreter.evaluate(String($0).trim()) } return body(object, interpretedArguments) } } // MARK: Helpers - data types func numberDataType() -> DataType { return DataType(type: Double.self, literals: [Literal { Double($0.value) }, Literal("pi", convertsTo: Double.pi)]) { String(describing: $0.value) } } func stringDataType() -> DataType { let singleQuotesLiteral = Literal { literal -> String? in guard let first = literal.value.first, let last = literal.value.last, first == last, first == "'" else { return nil } let trimmed = literal.value.trimmingCharacters(in: CharacterSet(charactersIn: "'")) return trimmed.contains("'") ? nil : trimmed } return DataType(type: String.self, literals: [singleQuotesLiteral]) { $0.value } } func dateDataType() -> DataType { let dateFormatter = DateFormatter() dateFormatter.calendar = Calendar(identifier: .gregorian) dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" return DataType(type: Date.self, literals: [Literal("now", convertsTo: Date())]) { dateFormatter.string(from: $0.value) } } func arrayDataType() -> DataType<[CustomStringConvertible]> { let arrayLiteral = Literal { literal -> [CustomStringConvertible]? in guard let first = literal.value.first, let last = literal.value.last, first == "[", last == "]" else { return nil } return literal.value .trimmingCharacters(in: CharacterSet(charactersIn: "[]")) .split(separator: ",") .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .map { literal.interpreter.evaluate(String($0)) as? CustomStringConvertible ?? String($0) } } return DataType(type: [CustomStringConvertible].self, literals: [arrayLiteral]) { $0.value.map { $0.description }.joined(separator: ",") } } func booleanDataType() -> DataType { return DataType(type: Bool.self, literals: [Literal("false", convertsTo: false), Literal("true", convertsTo: true)]) { $0.value ? "true" : "false" } } // MARK: Helpers - functions func methodCallFunction() -> Function { return Function(patterns: [ Pattern(Variable("lhs") + Keyword(".") + Variable("rhs", options: .notInterpreted)) { if let lhs = $0.variables["lhs"] as? NSObjectProtocol, !(lhs is NSNull), let rhs = $0.variables["rhs"] as? String, let result = lhs.perform(Selector(rhs)) { return Double(Int(bitPattern: result.toOpaque())) } return nil } ]) } func dateFomatFunction() -> Function { return objectFunctionWithParameters("format") { (object: Date, arguments: [Any]) -> String? in guard let format = arguments.first as? String else { return nil } let dateFormatter = DateFormatter() dateFormatter.calendar = Calendar(identifier: .gregorian) dateFormatter.dateFormat = format return dateFormatter.string(from: object) } } func dateFactoryFunction() -> Function { return function("Date") { (arguments: [Any]) -> Date? in guard let arguments = arguments as? [Double], arguments.count >= 3 else { return nil } var components = DateComponents() components.calendar = Calendar(identifier: .gregorian) components.year = Int(arguments[0]) components.month = Int(arguments[1]) components.day = Int(arguments[2]) components.hour = arguments.count > 3 ? Int(arguments[3]) : 0 components.minute = arguments.count > 4 ? Int(arguments[4]) : 0 components.second = arguments.count > 5 ? Int(arguments[5]) : 0 return components.date } } func incrementFunction() -> Function { return Function([Variable("value", options: .notInterpreted), Keyword("++")]) { if let argument = $0.variables["value"] as? String { if let variable = $0.interpreter.context.variables.first(where: { argument == $0.key }), let value = variable.value as? Double { let incremented = value + 1 $0.interpreter.context.variables[variable.key] = incremented return incremented } else if let argument = $0.interpreter.evaluate(argument) as? Double { return argument + 1 } } return nil } } } ================================================ FILE: Tests/EvalTests/IntegrationTests/PerformanceTest.swift ================================================ // // PerformanceTest.swift // EvalTests // // Created by László Teveli on 2019. 09. 14.. // import XCTest @testable import Eval class PerformanceTest: XCTestCase { var interpreter: TypedInterpreter? override func setUp() { super.setUp() let not = prefixOperator("!") { (value: Bool) in !value } let not2 = prefixOperator("x") { (value: Bool) in !value } let equality = infixOperator("==") { (lhs: Bool, rhs: Bool) in lhs == rhs } interpreter = TypedInterpreter(dataTypes: [stringDataType(), booleanDataType(), numberDataType()], functions: [not, not2, equality], context: Context(variables: ["nothing": true])) } func test_suffix1() { self.measure { for _ in 1...1000 { _ = self.interpreter?.evaluate("!nothing") } } } func test_suffix2() { self.measure { for _ in 1...1000 { _ = self.interpreter?.evaluate("x nothing") } } } func test_suffix3() { self.measure { for _ in 1...1000 { _ = self.interpreter?.evaluate("nothing == true") } } } func numberDataType() -> DataType { return DataType(type: Double.self, literals: [Literal { Double($0.value) }, Literal("pi", convertsTo: Double.pi)]) { String(describing: $0.value) } } func stringDataType() -> DataType { let singleQuotesLiteral = Literal { literal -> String? in guard let first = literal.value.first, let last = literal.value.last, first == last, first == "'" else { return nil } let trimmed = literal.value.trimmingCharacters(in: CharacterSet(charactersIn: "'")) return trimmed.contains("'") ? nil : trimmed } return DataType(type: String.self, literals: [singleQuotesLiteral]) { $0.value } } func booleanDataType() -> DataType { return DataType(type: Bool.self, literals: [Literal("false", convertsTo: false), Literal("true", convertsTo: true)]) { $0.value ? "true" : "false" } } func prefixOperator(_ symbol: String, body: @escaping (A) -> T) -> Function { return Function([Keyword(symbol), Variable("value")]) { guard let value = $0.variables["value"] as? A else { return nil } return body(value) } } func infixOperator(_ symbol: String, body: @escaping (A, B) -> T) -> Function { return Function([Variable("lhs"), Keyword(symbol), Variable("rhs")], options: .backwardMatch) { guard let lhs = $0.variables["lhs"] as? A, let rhs = $0.variables["rhs"] as? B else { return nil } return body(lhs, rhs) } } } ================================================ FILE: Tests/EvalTests/IntegrationTests/Suffix.swift ================================================ // // Suffix.swift // Eval // // Created by László Teveli on 2019. 09. 14.. // import Foundation import XCTest @testable import Eval class MiniExpressionStandardLibraryTest: XCTestCase { private func evaluate(_ expression: String, inputs: [String: Any] = [:]) -> R? { let context = Context(variables: inputs) let interpreter = TypedInterpreter(dataTypes: MiniExpressionStandardLibrary.dataTypes, functions: MiniExpressionStandardLibrary.functions, context: context) let result = interpreter.evaluate(expression, context: context) print(context.debugInfo) return result as? R } func testComposition() { // based on feedback I realized this doesn't work... // TODO: figure out the suffix error in this case XCTAssertEqual(evaluate("(toggle == true) and (url exists)", inputs: ["toggle": true, "url": 1]), true) // XCTAssertEqual(evaluate("(toggle == true) and (url exists)", inputs: ["toggle": true]), false) XCTAssertEqual(evaluate("(toggle == true) and (not(url exists))", inputs: ["toggle": true]), true) } func testComposition2() { // this (prefix function) does work... XCTAssertEqual(evaluate("(toggle == true) and (didset url)", inputs: ["toggle": true, "url": 1]), true) // XCTAssertEqual(evaluate("(toggle == true) and (didset url)", inputs: ["toggle": true]), false) XCTAssertEqual(evaluate("(toggle == true) and (not(didset url))", inputs: ["toggle": true]), true) } } class MiniExpressionStandardLibrary { static var dataTypes: [DataTypeProtocol] { return [ booleanType, ] } static var functions: [FunctionProtocol] { return [ andOperator, boolParentheses, existsOperator, boolEqualsOperator, didsetOperator, ] } // MARK: - Types static var booleanType: DataType { let trueLiteral = Literal("true", convertsTo: true) let falseLiteral = Literal("false", convertsTo: false) return DataType(type: Bool.self, literals: [trueLiteral, falseLiteral]) { $0.value ? "true" : "false" } } // MARK: - Functions static var boolEqualsOperator: Function { return infixOperator("==") { (lhs: Bool, rhs: Bool) in lhs == rhs } } static var boolParentheses: Function { return Function([OpenKeyword("("), Variable("body"), CloseKeyword(")")]) { $0.variables["body"] as? Bool } } static var andOperator: Function { return infixOperator("and") { (lhs: Bool, rhs: Bool) in lhs && rhs } } static var existsOperator: Function { return suffixOperator("exists") { (expression: Any?) in expression != nil } } static var didsetOperator: Function { return prefixOperator("didset") { (expression: Any?) in expression != nil } } // MARK: - Operator helpers static func infixOperator(_ symbol: String, body: @escaping (A, B) -> T) -> Function { return Function([Variable("lhs"), Keyword(symbol), Variable("rhs")], options: .backwardMatch) { guard let lhs = $0.variables["lhs"] as? A, let rhs = $0.variables["rhs"] as? B else { return nil } return body(lhs, rhs) } } static func prefixOperator(_ symbol: String, body: @escaping (A) -> T) -> Function { return Function([Keyword(symbol), Variable("value")]) { guard let value = $0.variables["value"] as? A else { return nil } return body(value) } } static func suffixOperator(_ symbol: String, body: @escaping (A) -> T) -> Function { return Function([Variable("value"), Keyword(symbol)]) { guard let value = $0.variables["value"] as? A else { return nil } return body(value) } } } ================================================ FILE: Tests/EvalTests/IntegrationTests/TemplateTests.swift ================================================ @testable import Eval import class Eval.Pattern import XCTest class TemplateTests: XCTestCase { func test_flow() { let parenthesis = Function([Keyword("("), Variable("body"), Keyword(")")]) { $0.variables["body"] } let subtractOperator = infixOperator("-") { (lhs: Double, rhs: Double) in lhs - rhs } let interpreter = TypedInterpreter(dataTypes: [numberDataType()], functions: [parenthesis, subtractOperator]) XCTAssertEqual(interpreter.evaluate("6 - (4 - 2)") as! Double, 4) } func test_whenAddingALotOfFunctions_thenInterpretationWorksCorrectly() { let parenthesis = Function([Keyword("("), Variable("body"), Keyword(")")]) { $0.variables["body"] } let plusOperator = infixOperator("+") { (lhs: Double, rhs: Double) in lhs + rhs } let concat = infixOperator("+") { (lhs: String, rhs: String) in lhs + rhs } let lessThan = infixOperator("<") { (lhs: Double, rhs: Double) in lhs < rhs } let interpreter = TypedInterpreter(dataTypes: [numberDataType(), stringDataType()], functions: [concat, parenthesis, plusOperator, lessThan], context: Context(variables: ["name": "Laszlo Teveli"])) let ifStatement = Pattern>([Keyword("{%"), Keyword("if"), Variable("condition"), Keyword("%}"), TemplateVariable("body"), Keyword("{% endif %}")]) { guard let condition = $0.variables["condition"] as? Bool, let body = $0.variables["body"] as? String else { return nil } return condition ? body : nil } let printStatement = Pattern>([Keyword("{{"), Variable("body"), Keyword("}}")]) { guard let body = $0.variables["body"] else { return nil } return $0.interpreter.typedInterpreter.print(body) } let template = StringTemplateInterpreter(statements: [ifStatement, printStatement], interpreter: interpreter, context: Context()) XCTAssertEqual(template.evaluate("{{ 1 + 2 }}"), "3.0") XCTAssertEqual(template.evaluate("{{ 'Hello' + ' ' + 'World' + '!' }}"), "Hello World!") XCTAssertEqual(template.evaluate("asd {% if 10 < 21 %}Hello{% endif %} asd"), "asd Hello asd") XCTAssertEqual(template.evaluate("ehm, {% if 10 < 21 %}{{ 'Hello ' + name }}{% endif %}!"), "ehm, Hello Laszlo Teveli!") } func test_whenEmbeddingTags_thenInterpretationWorksCorrectly() { let parenthesis = Function([OpenKeyword("("), Variable("body"), CloseKeyword(")")]) { $0.variables["body"] } let lessThan = infixOperator("<") { (lhs: Double, rhs: Double) in lhs < rhs } let interpreter = TypedInterpreter(dataTypes: [numberDataType(), stringDataType(), booleanDataType()], functions: [parenthesis, lessThan], context: Context()) let braces = Pattern>([OpenKeyword("("), TemplateVariable("body"), CloseKeyword(")")]) { $0.variables["body"] as? String } let ifStatement = Pattern>([OpenKeyword("{% if"), Variable("condition"), Keyword("%}"), TemplateVariable("body"), CloseKeyword("{% endif %}")]) { guard let condition = $0.variables["condition"] as? Bool, let body = $0.variables["body"] as? String else { return nil } return condition ? body : nil } let template = StringTemplateInterpreter(statements: [braces, ifStatement], interpreter: interpreter, context: Context()) XCTAssertEqual(template.evaluate("(a)"), "a") XCTAssertEqual(template.evaluate("(a(b))"), "ab") XCTAssertEqual(template.evaluate("((a)b)"), "ab") XCTAssertEqual(template.evaluate("(a(b)c)"), "abc") XCTAssertEqual(template.evaluate("{% if 10 < 21 %}Hello {% if true %}you{% endif %}!{% endif %}"), "Hello you!") } // MARK: Helpers - data types func numberDataType() -> DataType { return DataType(type: Double.self, literals: [Literal { Double($0.value) }, Literal("pi", convertsTo: Double.pi) ]) { String(describing: $0.value) } } func stringDataType() -> DataType { let singleQuotesLiteral = Literal { literal -> String? in guard let first = literal.value.first, let last = literal.value.last, first == last, first == "'" else { return nil } let trimmed = literal.value.trimmingCharacters(in: CharacterSet(charactersIn: "'")) return trimmed.contains("'") ? nil : trimmed } return DataType(type: String.self, literals: [singleQuotesLiteral]) { $0.value } } func booleanDataType() -> DataType { return DataType(type: Bool.self, literals: [Literal("false", convertsTo: false), Literal("true", convertsTo: true)]) { $0.value ? "true" : "false" } } // MARK: Helpers - operators func infixOperator(_ symbol: String, body: @escaping (A, B) -> T) -> Function { return Function([Variable("lhs"), Keyword(symbol), Variable("rhs")]) { guard let lhs = $0.variables["lhs"] as? A, let rhs = $0.variables["rhs"] as? B else { return nil } return body(lhs, rhs) } } } ================================================ FILE: Tests/EvalTests/UnitTests/DataTypeTests.swift ================================================ @testable import Eval import XCTest class DataTypeTests: XCTestCase { // MARK: init func test_whenInitialised_then() { let type = Double.self let literal = Literal { Double($0.value) } let print: (DataTypeBody) -> String = { String($0.value) } let dataType = DataType(type: type, literals: [literal], print: print) XCTAssertTrue(type == dataType.type.self) XCTAssertTrue(literal === dataType.literals[0]) XCTAssertNotNil(dataType.print) } // MARK: convert func test_whenConverting_thenGeneratesStringValue() { let dataType = DataType(type: Double.self, literals: [Literal { Double($0.value) }]) { String($0.value) } let result = dataType.convert(input: "1", interpreter: TypedInterpreter()) XCTAssertEqual(result as! Double, 1) } func test_whenConvertingInvalidValue_thenGeneratesNilValue() { let dataType = DataType(type: Double.self, literals: [Literal { Double($0.value) }]) { String($0.value) } let result = dataType.convert(input: "a", interpreter: TypedInterpreter()) XCTAssertNil(result) } // MARK: print func test_whenPrinting_thenGeneratesStringValue() { let dataType = DataType(type: Double.self, literals: [Literal { Double($0.value) }]) { _ in "printed value" } let result = dataType.print(value: 1.0, printer: TypedInterpreter()) XCTAssertEqual(result, "printed value") } } ================================================ FILE: Tests/EvalTests/UnitTests/FunctionTests.swift ================================================ @testable import Eval import XCTest import class Eval.Pattern class FunctionTests: XCTestCase { // MARK: init func test_whenInitialised_thenPatternsAreSaved() { let pattern = Pattern([Keyword("in")]) { _ in 1 } let function = Function(patterns: [pattern]) XCTAssertEqual(function.patterns.count, 1) XCTAssertTrue(pattern === function.patterns[0]) } func test_whenInitialisedWithOnePatters_thenPatternIsSaved() { let pattern = [Keyword("in")] let function = Function(pattern) { _ in 1 } XCTAssertEqual(function.patterns.count, 1) XCTAssertEqual(function.patterns[0].elements.count, 1) XCTAssertTrue(pattern[0] === function.patterns[0].elements[0] as! Keyword) } // MARK: convert func test_whenConverting_thenResultIsValid() { let function = Function([Keyword("in")]) { _ in 1 } let result = function.convert(input: "input", interpreter: TypedInterpreter(), context: Context()) XCTAssertEqual(result as! Int, 1) } func test_whenConvertingInvalidValue_thenConversionReturnsNil() { let function = Function([Keyword("in")]) { _ in 1 } let result = function.convert(input: "example", interpreter: TypedInterpreter(), context: Context()) XCTAssertNil(result) } } ================================================ FILE: Tests/EvalTests/UnitTests/InterpreterContextTests.swift ================================================ @testable import Eval import XCTest class InterpreterContextTests: XCTestCase { // MARK: init func test_whenCreated_thenVariablesAreSet() { let variables = ["test": 2] let context = Context(variables: variables) XCTAssertEqual(variables, context.variables as! [String: Int]) } // MARK: push/pop func test_whenPushing_thenRemainsTheSame() { let variables = ["test": 2] let context = Context(variables: variables) context.push() XCTAssertEqual(variables, context.variables as! [String: Int]) } func test_whenPushingAndModifying_thenContextChanges() { let variables = ["test": 2] let context = Context(variables: variables) context.push() context.variables["a"] = 3 XCTAssertNotEqual(variables, context.variables as! [String: Int]) } func test_whenPushingModifyingAndPopping_thenRestores() { let variables = ["test": 2] let context = Context(variables: variables) context.push() context.variables["a"] = 3 context.pop() XCTAssertEqual(variables, context.variables as! [String: Int]) } func test_whenJustPopping_thenNothingHappens() { let variables = ["test": 2] let context = Context(variables: variables) context.pop() XCTAssertEqual(variables, context.variables as! [String: Int]) } // MARK: merging func test_whenMergingTwo_thenCreatesANewContext() { let one = Context(variables: ["a": 1]) let two = Context(variables: ["b": 2]) let result = one.merging(with: two) XCTAssertEqual(result.variables as! [String: Int], ["a": 1, "b": 2]) XCTAssertFalse(one === result) XCTAssertFalse(two === result) } func test_whenMergingTwo_thenParameterOverridesVariablesInSelf() { let one = Context(variables: ["a": 1]) let two = Context(variables: ["a": 2, "x": 3]) let result = one.merging(with: two) XCTAssertEqual(result.variables as! [String: Int], ["a": 2, "x": 3]) } func test_whenMergingWithNil_thenReturnsSelf() { let context = Context(variables: ["a": 1]) let result = context.merging(with: nil) XCTAssertTrue(result === context) } // MARK: merge func test_whenMergingTwoInAMutableWay_thenMergesVariables() { let one = Context(variables: ["a": 1]) let two = Context(variables: ["b": 2]) one.merge(with: two) { existing, _ in existing } XCTAssertEqual(one.variables as! [String: Int], ["a": 1, "b": 2]) } func test_whenMergingTwoInAMutableWay_thenParameterOverridesVariablesInSelf() { let one = Context(variables: ["a": 1]) let two = Context(variables: ["a": 2, "x": 3]) one.merge(with: two) { existing, _ in existing } XCTAssertEqual(one.variables as! [String: Int], ["a": 1, "x": 3]) } func test_whenMergingTwoInAMutableWayReversed_thenParameterOverridesVariablesInSelf() { let one = Context(variables: ["a": 1]) let two = Context(variables: ["a": 2, "x": 3]) two.merge(with: one) { _, new in new } XCTAssertEqual(two.variables as! [String: Int], ["a": 1, "x": 3]) } func test_whenMergingWithNilInAMutableWay_thenReturnsSelf() { let context = Context(variables: ["a": 1]) context.merge(with: nil) { existing, _ in existing } XCTAssertTrue(context.variables as! [String: Int] == ["a": 1]) } } ================================================ FILE: Tests/EvalTests/UnitTests/KeywordTests.swift ================================================ @testable import Eval import XCTest class KeywordTests: XCTestCase { // MARK: Initialisation func test_whenKeywordIsCreated_thenNameAndTypeIsSet() { let dummyName = "test name" let dummyType = Keyword.KeywordType.generic let keyword = Keyword(dummyName, type: dummyType) XCTAssertEqual(keyword.name, dummyName) XCTAssertEqual(keyword.type, dummyType) } func test_whenKeywordIsCreated_thenTypeIsGenericByDefault() { let keyword = Keyword("test name") XCTAssertEqual(keyword.type, .generic) } // MARK: Equality func test_whenIdenticalKeywordsAreCreated_thenTheyAreEqual() { let keyword1 = Keyword("test name", type: .openingStatement) let keyword2 = Keyword("test name", type: .openingStatement) XCTAssertEqual(keyword1, keyword2) } func test_whenKeywordsWithDifferentNamesAreCreated_thenTheyAreNotEqual() { let keyword1 = Keyword("test name", type: .openingStatement) let keyword2 = Keyword("different", type: .openingStatement) XCTAssertNotEqual(keyword1, keyword2) } func test_whenKeywordsWithDifferentTypesAreCreated_thenTheyAreNotEqual() { let keyword1 = Keyword("test name", type: .openingStatement) let keyword2 = Keyword("test name", type: .closingStatement) XCTAssertNotEqual(keyword1, keyword2) } // MARK: Match func test_whenPartlyMatches_thenReturnsPossibleMatch() { let keyword = Keyword("checking prefix") let result = keyword.matches(prefix: "check") XCTAssertTrue(result.isPossibleMatch()) } func test_whenNotMatches_thenReturnsNoMatch() { let keyword = Keyword("checking prefix") let result = keyword.matches(prefix: "example") XCTAssertTrue(result.isNoMatch()) } func test_whenMatches_thenReturnsExactMatch() { let keyword = Keyword("checking prefix") let result = keyword.matches(prefix: "checking prefix") verifyMatch(expectation: "checking prefix", result: result) } func test_whenMatchesAndContinues_thenReturnsExactMatch() { let keyword = Keyword("checking prefix") let result = keyword.matches(prefix: "checking prefix with extra content") verifyMatch(expectation: "checking prefix", result: result) } // MARK: OpenKeyword func test_whenCreatingOpenKeyword_thenTheTypeIsSetCorrectly() { let keyword = OpenKeyword("checking prefix") XCTAssertEqual(keyword.type, .openingStatement) XCTAssertEqual(keyword.name, "checking prefix") } // MARK: CloseKeyword func test_whenCreatingCloseKeyword_thenTheTypeIsSetCorrectly() { let keyword = CloseKeyword("checking prefix") XCTAssertEqual(keyword.type, .closingStatement) XCTAssertEqual(keyword.name, "checking prefix") } // MARK: Match performance func test_whenShortKeywordMatchesShortInput_thenPerformsWell() { let keyword = Keyword("=") self.measure { _ = keyword.matches(prefix: "= asd") } } func test_whenShortKeywordNotMatchesInput_thenPerformsWell() { let keyword = Keyword("=") self.measure { _ = keyword.matches(prefix: "checking prefix") } } func test_whenShortKeywordHureInput_thenPerformsWell() { let keyword = Keyword("=") self.measure { _ = keyword.matches(prefix: """ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. """) } } func test_whenLargeKeywordMatchesShortInput_thenPerformsWell() { let keyword = Keyword("this is an example to match") self.measure { _ = keyword.matches(prefix: "this is an example to match and some other things") } } func test_whenLargeKeywordNotMatchesInput_thenPerformsWell() { let keyword = Keyword("this is an example to match") self.measure { _ = keyword.matches(prefix: "x and y") } } func test_whenLargeKeywordHureInput_thenPerformsWell() { let keyword = Keyword("=") self.measure { _ = keyword.matches(prefix: """ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. """) } } // MARK: Helpers func verifyMatch(expectation: String, result: MatchResult) { if case .exactMatch(let length, let output, let variables) = result { XCTAssertEqual(length, expectation.count) XCTAssertEqual(output as! String, expectation) XCTAssertTrue(variables.isEmpty) } else { fatalError("No match") } } } ================================================ FILE: Tests/EvalTests/UnitTests/LiteralTests.swift ================================================ @testable import Eval import XCTest class LiteralTests: XCTestCase { // MARK: init func test_whenInitialisedWithBlock_thenParameterIsSaved() { let block: (LiteralBody) -> Double? = { Double($0.value) } let literal = Literal(convert: block) XCTAssertNotNil(literal.convert) } func test_whenInitialisedWithValue_thenConvertBlockIsSaved() { let literal = Literal("true", convertsTo: false) XCTAssertNotNil(literal.convert) } // MARK: convert func test_whenConverting_thenCallsBlock() { let block: (LiteralBody) -> Int? = { _ in 123 } let literal = Literal(convert: block) let result = literal.convert(input: "asd", interpreter: TypedInterpreter()) XCTAssertEqual(result!, 123) } } ================================================ FILE: Tests/EvalTests/UnitTests/MatchResultTests.swift ================================================ @testable import Eval import XCTest class MatchResultTests: XCTestCase { // MARK: isMatch func test_whenMatchIsExactMatch_thenIsMatchReturnsTrue() { let matchResult = MatchResult.exactMatch(length: 1, output: "a", variables: [:]) XCTAssertTrue(matchResult.isMatch()) } func test_whenMatchIsNotExactMatch_thenIsMatchReturnsFalse() { let matchResult = MatchResult.anyMatch(exhaustive: true) XCTAssertFalse(matchResult.isMatch()) } // MARK: isPossibleMatch func test_whenMatchIsPossibleyMatch_thenIsPossibleMatchReturnsTrue() { let matchResult = MatchResult.possibleMatch XCTAssertTrue(matchResult.isPossibleMatch()) } func test_whenMatchIsNotPossibleyMatch_thenIsPossibleMatchReturnsFalse() { let matchResult = MatchResult.noMatch XCTAssertFalse(matchResult.isPossibleMatch()) } // MARK: isNoMatch func test_whenMatchIsNoMatch_thenIsNoMatchReturnsTrue() { let matchResult = MatchResult.noMatch XCTAssertTrue(matchResult.isNoMatch()) } func test_whenMatchIsNotNoMatch_thenIsNoMatchReturnsFalse() { let matchResult = MatchResult.anyMatch(exhaustive: false) XCTAssertFalse(matchResult.isNoMatch()) } // MARK: isAnyMatch func test_whenMatchIsAnyMatch_thenIsAnyMatchReturnsTrue() { let matchResult = MatchResult.anyMatch(exhaustive: true) XCTAssertTrue(matchResult.isAnyMatch(exhaustive: true)) } func test_whenMatchIsAnyMatch_thenIsAnyMatchWithParameterReturnsTrue() { let matchResult = MatchResult.anyMatch(exhaustive: false) XCTAssertTrue(matchResult.isAnyMatch(exhaustive: false)) } func test_whenMatchIsNotAnyMatch_thenIsAnyMatchReturnsFalse() { let matchResult = MatchResult.possibleMatch XCTAssertFalse(matchResult.isAnyMatch()) } // MARK: Equality func test_whenTwoAnyMatchesAreCompared_thenResultIsEqual() { let one = MatchResult.anyMatch(exhaustive: false) let two = MatchResult.anyMatch(exhaustive: false) XCTAssertTrue(one == two) } func test_whenTwoAnyMatchesWithDifferentPropertiesAreCompared_thenResultIsNotEqual() { let one = MatchResult.anyMatch(exhaustive: false) let two = MatchResult.anyMatch(exhaustive: true) XCTAssertFalse(one == two) } func test_whenTwoNoMatchesAreCompared_thenResultIsEqual() { let one = MatchResult.noMatch let two = MatchResult.noMatch XCTAssertTrue(one == two) } func test_whenTwoPossibleMatchesAreCompared_thenResultIsEqual() { let one = MatchResult.possibleMatch let two = MatchResult.possibleMatch XCTAssertTrue(one == two) } func test_whenTwoExactMatchesAreCompared_thenResultIsEqual() { let one = MatchResult.exactMatch(length: 1, output: "a", variables: [:]) let two = MatchResult.exactMatch(length: 1, output: "a", variables: [:]) XCTAssertTrue(one == two) } func test_whenTwoExactMatchesWithDifferentPropertiesAreCompared_thenResultIsNotEqual() { let one = MatchResult.exactMatch(length: 1, output: "a", variables: [:]) let two = MatchResult.exactMatch(length: 2, output: "b", variables: ["1": 2]) XCTAssertFalse(one == two) } func test_whenTwoDifferentMatchesAreCompared_thenResultIsNotEqual() { let one = MatchResult.exactMatch(length: 1, output: "a", variables: [:]) let two = MatchResult.anyMatch(exhaustive: true) XCTAssertFalse(one == two) } } ================================================ FILE: Tests/EvalTests/UnitTests/MatchStatementTests.swift ================================================ @testable import Eval import XCTest import class Eval.Pattern class MatchStatementTests: XCTestCase { func test_whenMatchingOne_returnsMatch() { let input = "input" let matcher = Pattern([Keyword("in")]) { _ in 1 } let result1 = matcher.matches(string: input, interpreter: DummyInterpreter(), context: Context()) let result2 = matchStatement(amongst: [matcher], in: input, interpreter: DummyInterpreter(), context: Context()) XCTAssertTrue(result1 == result2) } func test_whenMatchingTwo_returnsMatch() { let input = "input" let matcher1 = Pattern([Keyword("in")]) { _ in 1 } let matcher2 = Pattern([Keyword("on")]) { _ in 2 } let result = matchStatement(amongst: [matcher1, matcher2], in: input, interpreter: DummyInterpreter(), context: Context()) XCTAssertTrue(result == MatchResult.exactMatch(length: 2, output: 1, variables: [:])) } func test_whenMatchingTwoMatches_returnsTheFirstMatch() { let input = "input" let matcher1 = Pattern([Keyword("in")]) { _ in 1 } let matcher2 = Pattern([Keyword("inp")]) { _ in 2 } let result = matchStatement(amongst: [matcher1, matcher2], in: input, interpreter: DummyInterpreter(), context: Context()) XCTAssertTrue(result == MatchResult.exactMatch(length: 2, output: 1, variables: [:])) } func test_whenMatchingInvalid_returnsNoMatch() { let input = "xxx" let matcher1 = Pattern([Keyword("in")]) { _ in 1 } let matcher2 = Pattern([Keyword("on")]) { _ in 2 } let result = matchStatement(amongst: [matcher1, matcher2], in: input, interpreter: DummyInterpreter(), context: Context()) XCTAssertTrue(result == MatchResult.noMatch) } func test_whenMatchingPrefix_returnsPossibleMatch() { let input = "i" let matcher1 = Pattern([Keyword("in")]) { _ in 1 } let matcher2 = Pattern([Keyword("on")]) { _ in 2 } let result = matchStatement(amongst: [matcher1, matcher2], in: input, interpreter: DummyInterpreter(), context: Context()) XCTAssertTrue(result == MatchResult.possibleMatch) } } ================================================ FILE: Tests/EvalTests/UnitTests/MatcherTests.swift ================================================ @testable import Eval import XCTest class MatcherTests: XCTestCase { // MARK: isEmbedded func test_whenEmbedding_thenIsEmbeddedReturnsTrue() { let opening = Keyword("(", type: .openingStatement) let closing = Keyword(")", type: .closingStatement) let processor = VariableProcessor(interpreter: DummyInterpreter(), context: Context()) let matcher = Matcher(pattern: pattern([opening, closing]), processor: processor) let input = "(input(random))" let result = matcher.isEmbedded(element: closing, in: input, at: input.startIndex) XCTAssertTrue(result) } func test_whenNotEmbedding_thenIsEmbeddedReturnsFalse() { let opening = Keyword("(", type: .openingStatement) let closing = Keyword(")", type: .closingStatement) let processor = VariableProcessor(interpreter: DummyInterpreter(), context: Context()) let matcher = Matcher(pattern: pattern([opening, closing]), processor: processor) let input = "input" let result = matcher.isEmbedded(element: opening, in: input, at: input.startIndex) XCTAssertFalse(result) } func test_whenEmbeddingButLate_thenIsEmbeddedReturnsFalse() { let opening = Keyword("(", type: .openingStatement) let closing = Keyword(")", type: .closingStatement) let processor = VariableProcessor(interpreter: DummyInterpreter(), context: Context()) let matcher = Matcher(pattern: pattern([opening, closing]), processor: processor) let input = "input(random)" let result = matcher.isEmbedded(element: closing, in: input, at: input.index(input.startIndex, offsetBy: 12)) XCTAssertFalse(result) } // MARK: positionOfClosingTag func test_whenEmbedding_thenClosingPositionIsValid() { let opening = Keyword("(", type: .openingStatement) let closing = Keyword(")", type: .closingStatement) let processor = VariableProcessor(interpreter: DummyInterpreter(), context: Context()) let matcher = Matcher(pattern: pattern([opening, closing]), processor: processor) let input = "(input(random))" XCTAssertEqual(matcher.positionOfClosingTag(in: input, from: input.startIndex), input.index(input.startIndex, offsetBy: 14)) XCTAssertEqual(matcher.positionOfClosingTag(in: input, from: input.index(after: input.startIndex)), input.index(input.startIndex, offsetBy: 13)) } func test_whenNotEmbedding_thenClosingPositionIsNil() { let opening = Keyword("(", type: .openingStatement) let closing = Keyword(")", type: .closingStatement) let processor = VariableProcessor(interpreter: DummyInterpreter(), context: Context()) let matcher = Matcher(pattern: pattern([opening, closing]), processor: processor) let input = "input" XCTAssertNil(matcher.positionOfClosingTag(in: input, from: input.startIndex)) } func test_whenEmbeddingButLate_thenClosingPositionIsNil() { let opening = Keyword("(", type: .openingStatement) let closing = Keyword(")", type: .closingStatement) let processor = VariableProcessor(interpreter: DummyInterpreter(), context: Context()) let matcher = Matcher(pattern: pattern([opening, closing]), processor: processor) let input = "(input(random))" XCTAssertNil(matcher.positionOfClosingTag(in: input, from: input.index(input.startIndex, offsetBy: 8))) } private func pattern(_ elements: [PatternElement]) -> Eval.Pattern { return Pattern(elements) { _ in "" } } } ================================================ FILE: Tests/EvalTests/UnitTests/PatternTests.swift ================================================ @testable import Eval import XCTest import class Eval.Pattern class PatternTests: XCTestCase { // MARK: init class DummyElement: PatternElement { func matches(prefix: String, options: PatternOptions) -> MatchResult { return .noMatch } } func test_whenInitialised_thenElementsAndMatcherAreSet() { let element = DummyElement() let matcherBlock: MatcherBlock = { _ in 1 } let matcher = Pattern([element], matcher: matcherBlock) XCTAssertTrue(element === matcher.elements[0] as! DummyElement) //XCTAssertTrue(matcherBlock === matcher.matcher) } func test_whenInitialisedWithVariableOnTheLastPlace_thenLastVariableBecomesNotShortest() { let element = DummyElement() let variable = GenericVariable("name") let matcher = Pattern([element, variable]) { _ in "x" } let result = matcher.elements[1] as! GenericVariable XCTAssertTrue(element === matcher.elements[0] as! DummyElement) XCTAssertFalse(variable === result) XCTAssertTrue(result.options.contains(.exhaustiveMatch)) } // MARK: matches // swiftlint:disable:next function_body_length func test_whenMatching_thenExpectingAppropriateResults() { // swiftlint:disable:next nesting typealias TestCase = (elements: [PatternElement], input: String, expectedResult: MatchResult) let keyword = Keyword("ok") let variable = GenericVariable("name", options: .notInterpreted) let variableLongest = GenericVariable("name", options: [.notInterpreted, .exhaustiveMatch]) let variable2 = GenericVariable("last", options: .notInterpreted) let testCases: [TestCase] = [ ([keyword], "invalid", .noMatch), ([keyword], "o", .possibleMatch), ([keyword], "ok", .exactMatch(length: 2, output: 1, variables: [:])), ([keyword], "okokok", .exactMatch(length: 2, output: 1, variables: [:])), ([variable], "anything", .exactMatch(length: 8, output: 1, variables: ["name": "anything"])), ([variableLongest], "anything", .exactMatch(length: 8, output: 1, variables: ["name": "anything"])), ([variable, keyword], "invalid", .possibleMatch), ([variable, keyword], "invalid o", .possibleMatch), ([variable, keyword], "invalid ok", .exactMatch(length: 10, output: 1, variables: ["name": "invalid"])), ([variable, keyword], "invalidxok", .exactMatch(length: 10, output: 1, variables: ["name": "invalidx"])), ([variable, keyword], "invalidxok extra", .exactMatch(length: 10, output: 1, variables: ["name": "invalidx"])), ([keyword, variable], "xokthen", .noMatch), ([keyword, variable], "o", .possibleMatch), ([keyword, variable], "oi", .noMatch), ([keyword, variable], "ok", .exactMatch(length: 2, output: 1, variables: ["name": ""])), ([keyword, variable], "ok then", .exactMatch(length: 7, output: 1, variables: ["name": "then"])), ([keyword, variable], "ok,then", .exactMatch(length: 7, output: 1, variables: ["name": ",then"])), ([keyword, variable, keyword], "o", .possibleMatch), ([keyword, variable, keyword], "ok", .possibleMatch), ([keyword, variable, keyword], "oko", .possibleMatch), ([keyword, variable, keyword], "okok", .exactMatch(length: 4, output: 1, variables: ["name": ""])), ([keyword, variable, keyword], "okxok", .exactMatch(length: 5, output: 1, variables: ["name": "x"])), ([keyword, variable, keyword], "okokok", .exactMatch(length: 4, output: 1, variables: ["name": ""])), ([keyword, variableLongest, keyword], "ok", .possibleMatch), ([keyword, variableLongest, keyword], "okx", .possibleMatch), ([keyword, variableLongest, keyword], "oko", .possibleMatch), ([keyword, variableLongest, keyword], "okok", .possibleMatch), ([keyword, variableLongest, keyword], "okxok", .possibleMatch), ([variable, keyword, variable2], "xo", .possibleMatch), ([variable, keyword, variable2], "xok", .exactMatch(length: 3, output: 1, variables: ["name": "x", "last": ""])), ([variable, keyword, variable2], "xoky", .exactMatch(length: 4, output: 1, variables: ["name": "x", "last": "y"])), ([variable, keyword, variable2], "xoky and rest", .exactMatch(length: 13, output: 1, variables: ["name": "x", "last": "y and rest"])), ([variable, keyword, variable2], "xokoky", .exactMatch(length: 6, output: 1, variables: ["name": "x", "last": "oky"])) ] for testCase in testCases { let matcher = Pattern(testCase.elements) { _ in 1 } let result = matcher.matches(string: testCase.input, interpreter: DummyInterpreter(), context: Context()) XCTAssertTrue(result == testCase.expectedResult, "\(testCase.input) should have resulted in \(testCase.expectedResult) but got \(result) instead") } } func test_whenMatchingForwards_thenExpectingAppropriateResults() { let matcher = Pattern([Variable("lhs"), Keyword("-"), Variable("rhs")]) { guard let lhs = $0.variables["lhs"] as? Int, let rhs = $0.variables["rhs"] as? Int else { return nil } return lhs - rhs } let function = Function(patterns: [matcher]) let interpreter = TypedInterpreter(dataTypes: [DataType(type: Int.self, literals: [Literal { Int($0.value) }]) { String($0.value) }], functions: [function]) XCTAssertEqual(interpreter.evaluate("3 - 2 - 1") as! Int, 2) } func test_whenMatchingBackwards_thenExpectingAppropriateResults() { let matcher = Pattern([Variable("lhs"), Keyword("-"), Variable("rhs")], options: .backwardMatch) { guard let lhs = $0.variables["lhs"] as? Int, let rhs = $0.variables["rhs"] as? Int else { return nil } return lhs - rhs } let function = Function(patterns: [matcher]) let interpreter = TypedInterpreter(dataTypes: [DataType(type: Int.self, literals: [Literal { Int($0.value) }]) { String($0.value) }], functions: [function]) XCTAssertEqual(interpreter.evaluate("3 - 2 - 1") as! Int, 0) } } ================================================ FILE: Tests/EvalTests/UnitTests/TemplateInterpreterTests.swift ================================================ @testable import Eval import XCTest import class Eval.Pattern class StringTemplateInterpreterTests: XCTestCase { // MARK: init func test_whenInitialised_thenPropertiesAreSaved() { let matcher = Pattern>([Keyword("in")]) { _ in "a" } let statements = [matcher] let interpreter = TypedInterpreter() let context = Context() let stringTemplateInterpreter = StringTemplateInterpreter(statements: statements, interpreter: interpreter, context: context) XCTAssertEqual(stringTemplateInterpreter.statements.count, 1) XCTAssertTrue(statements[0] === stringTemplateInterpreter.statements[0]) XCTAssertTrue(interpreter === stringTemplateInterpreter.typedInterpreter) XCTAssertTrue(context === stringTemplateInterpreter.context) XCTAssertFalse(stringTemplateInterpreter.typedInterpreter.context === stringTemplateInterpreter.context) } func test_whenInitialised_thenTypedAndTemplateInterpreterDoNotShareTheSameContext() { let stringTemplateInterpreter = StringTemplateInterpreter(statements: [], interpreter: TypedInterpreter(), context: Context()) XCTAssertFalse(stringTemplateInterpreter.typedInterpreter.context === stringTemplateInterpreter.context) } // MARK: evaluate func test_whenEvaluates_thenTransformationHappens() { let matcher = Pattern>([Keyword("in")]) { _ in "contains" } let interpreter = StringTemplateInterpreter(statements: [matcher], interpreter: TypedInterpreter(), context: Context()) let result = interpreter.evaluate("a in b") XCTAssertEqual(result, "a contains b") } func test_whenEvaluates_thenUsesGlobalContext() { let matcher = Pattern>([Keyword("{somebody}")]) { $0.context.variables["person"] as? String } let interpreter = StringTemplateInterpreter(statements: [matcher], interpreter: TypedInterpreter(), context: Context(variables: ["person": "you"])) let result = interpreter.evaluate("{somebody} + me") XCTAssertEqual(result, "you + me") } // MARK: evaluate with context func test_whenEvaluatesWithContext_thenUsesLocalContext() { let matcher = Pattern>([Keyword("{somebody}")]) { $0.context.variables["person"] as? String } let interpreter = StringTemplateInterpreter(statements: [matcher], interpreter: TypedInterpreter(), context: Context()) let result = interpreter.evaluate("{somebody} + me", context: Context(variables: ["person": "you"])) XCTAssertEqual(result, "you + me") } func test_whenEvaluatesWithContext_thenLocalOverridesGlobalContext() { let matcher = Pattern>([Keyword("{somebody}")]) { $0.context.variables["person"] as? String } let interpreter = StringTemplateInterpreter(statements: [matcher], interpreter: TypedInterpreter(), context: Context(variables: ["person": "nobody"])) let result = interpreter.evaluate("{somebody} + me", context: Context(variables: ["person": "you"])) XCTAssertEqual(result, "you + me") } // MARK: TemplateVariable func test_whenUsingTemplateVariable_thenTransformationHappens() { let matcher = Pattern>([Keyword("{"), TemplateVariable("person"), Keyword("}")]) { _ in "you" } let interpreter = StringTemplateInterpreter(statements: [matcher], interpreter: TypedInterpreter(), context: Context()) let result = interpreter.evaluate("{somebody} + me") XCTAssertEqual(result, "you + me") } func test_whenUsingTemplateVariableWithNilResult_thenTransformationNotHappens() { let matcher = Pattern>([Keyword("{"), TemplateVariable("person"), Keyword("}")]) { _ in nil } let interpreter = StringTemplateInterpreter(statements: [matcher], interpreter: TypedInterpreter(), context: Context()) let result = interpreter.evaluate("{somebody} + me") XCTAssertEqual(result, "{somebody} + me") } } ================================================ FILE: Tests/EvalTests/UnitTests/TypedInterpreterTests.swift ================================================ @testable import Eval import XCTest class TypedInterpreterTests: XCTestCase { // MARK: init func test_whenInitialised_thenPropertiesAreSaved() { let dataTypes = [DataType(type: String.self, literals: []) { $0.value }] let functions = [Function([Keyword("a")]) { _ in "a" } ] let context = Context() let interpreter = TypedInterpreter(dataTypes: dataTypes, functions: functions, context: context) XCTAssertEqual(interpreter.dataTypes.count, 1) XCTAssertTrue(dataTypes[0] === interpreter.dataTypes[0] as! DataType) XCTAssertEqual(interpreter.functions.count, 1) XCTAssertTrue(functions[0] === interpreter.functions[0] as! Function) XCTAssertTrue(context === interpreter.context) } // MARK: evaluate func test_whenEvaluates_thenTransformationHappens() { let interpreter = TypedInterpreter(dataTypes: [DataType(type: Int.self, literals: [Literal { Int($0.value) }]) { String($0.value) }], functions: [Function([Variable("lhs"), Keyword("plus"), Variable("rhs")]) { ($0.variables["lhs"] as! Int) + ($0.variables["rhs"] as! Int) } ], context: Context()) let result = interpreter.evaluate("1 plus 2") XCTAssertEqual(result as! Int, 3) } func test_whenEvaluates_thenUsesGlobalContext() { let interpreter = TypedInterpreter(dataTypes: [DataType(type: Int.self, literals: [Literal { Int($0.value) }]) { String($0.value) }], functions: [Function([Variable("lhs"), Keyword("plus"), Variable("rhs")]) { ($0.variables["lhs"] as! Int) + ($0.variables["rhs"] as! Int) } ], context: Context(variables: ["a": 2])) let result = interpreter.evaluate("1 plus a") XCTAssertEqual(result as! Int, 3) } // MARK: evaluate with context func test_whenEvaluatesWithContext_thenUsesLocalContext() { let interpreter = TypedInterpreter(dataTypes: [DataType(type: Int.self, literals: [Literal { Int($0.value) }]) { String($0.value) }], functions: [Function([Variable("lhs"), Keyword("plus"), Variable("rhs")]) { ($0.variables["lhs"] as! Int) + ($0.variables["rhs"] as! Int) } ], context: Context()) let result = interpreter.evaluate("1 plus a", context: Context(variables: ["a": 2])) XCTAssertEqual(result as! Int, 3) } func test_whenEvaluatesWithContext_thenLocalOverridesGlobalContext() { let interpreter = TypedInterpreter(dataTypes: [DataType(type: Int.self, literals: [Literal { Int($0.value) }]) { String($0.value) }], functions: [Function([Variable("lhs"), Keyword("plus"), Variable("rhs")]) { ($0.variables["lhs"] as! Int) + ($0.variables["rhs"] as! Int) } ], context: Context(variables: ["a": 1])) let result = interpreter.evaluate("1 plus a", context: Context(variables: ["a": 2])) XCTAssertEqual(result as! Int, 3) } // MARK: print func test_whenPrintingDataType_thenReturnsItsBlock() { let interpreter = TypedInterpreter(dataTypes: [DataType(type: Int.self, literals: [Literal { Int($0.value) }]) { String($0.value) }], functions: [], context: Context()) let result = interpreter.print(1) XCTAssertEqual(result, "1") } func test_whenPrintingUnknownDataType_thenReturnsDescription() { let interpreter = TypedInterpreter(dataTypes: [DataType(type: Int.self, literals: [Literal { Int($0.value) }]) { String($0.value) }], functions: [], context: Context()) let result = interpreter.print(true) XCTAssertEqual(result, true.description) } } ================================================ FILE: Tests/EvalTests/UnitTests/UtilTests.swift ================================================ @testable import Eval import XCTest class UtilTests: XCTestCase { // MARK: plus operator on Elements class DummyX: PatternElement, Equatable { func matches(prefix: String, options: PatternOptions) -> MatchResult { return .noMatch } static func == (lhs: DummyX, rhs: DummyX) -> Bool { return true } } class DummyY: PatternElement, Equatable { func matches(prefix: String, options: PatternOptions) -> MatchResult { return .possibleMatch } static func == (lhs: DummyY, rhs: DummyY) -> Bool { return true } } func test_whenApplyingPlusOperatorOnElements_thenItCreatedAnArray() { let element1 = DummyX() let element2 = DummyY() let result = element1 + element2 XCTAssertEqual(result.count, 2) XCTAssertEqual(result[0] as! DummyX, element1) XCTAssertEqual(result[1] as! DummyY, element2) } // MARK: plus operator on Array func test_whenApplyingPlusOperatorOnArrayWithItem_thenItCreatesAMergedArray() { let result = [1, 2, 3] + 4 XCTAssertEqual(result, [1, 2, 3, 4]) } func test_whenApplyingPlusEqualsOperatorOnArrayWithItem_thenItCreatesAMergedArray() { var array = [1, 2, 3] array += 4 XCTAssertEqual(array, [1, 2, 3, 4]) } // MARK: String trim func test_whenTrimminString_thenRemovesWhitespaces() { XCTAssertEqual(" asd ".trim(), "asd") XCTAssertEqual(" \t asd \n ".trim(), "asd") } } ================================================ FILE: Tests/EvalTests/UnitTests/VariableProcessor.swift ================================================ @testable import Eval import XCTest class VariableProcessorTests: XCTestCase { // init func test_whenInitialising_thenSetsParametersCorrectly() { let interpreter = DummyInterpreter() let context = Context() let processor = VariableProcessor(interpreter: interpreter, context: context) XCTAssertTrue(interpreter === processor.interpreter) XCTAssertTrue(context === processor.context) } // process func test_whenProcessing_thenUsesMap() { let variable: VariableValue = (metadata: GenericVariable("name") { _ in "xyz" }, value: "asd") let processor = VariableProcessor(interpreter: DummyInterpreter(), context: Context()) let result = processor.process(variable) XCTAssertEqual(result as! String, "xyz") } func test_whenProcessingAndInterpreted_thenUsesInterpreter() { let variable: VariableValue = (metadata: GenericVariable("name", options: .notTrimmed), value: "asd") let processor = VariableProcessor(interpreter: DummyInterpreter(), context: Context()) let result = processor.process(variable) XCTAssertEqual(result as! String, "a") } func test_whenProcessingAndNotTrimmed_thenDoesNotTrim() { let variable: VariableValue = (metadata: GenericVariable("name", options: [.notTrimmed, .notInterpreted]), value: " asd ") let processor = VariableProcessor(interpreter: DummyInterpreter(), context: Context()) let result = processor.process(variable) XCTAssertEqual(result as! String, " asd ") } } ================================================ FILE: Tests/EvalTests/UnitTests/VariableTests.swift ================================================ @testable import Eval import XCTest class VariableTests: XCTestCase { // MARK: init func test_whenInitialised_thenPropertiesAreSet() { let variable = GenericVariable("name", options: .acceptsNilValue) { _ in nil } XCTAssertEqual(variable.name, "name") XCTAssertEqual(variable.options, .acceptsNilValue) XCTAssertNotNil(variable.map) } func test_whenInitialised_thenDefaultAreSetCorrectly() { let variable = GenericVariable("name") XCTAssertEqual(variable.name, "name") XCTAssertEqual(variable.options, []) XCTAssertNotNil(variable.map) } // MARK: matches func test_whenCallingMatches_thenReturnAny() { let variable = GenericVariable("name") XCTAssertTrue(variable.matches(prefix: "asd").isAnyMatch()) } // MARK: mapped func test_whenCallingMatched_thenCreatesANewVariable() { let variable = GenericVariable("name") let result = variable.mapped { Double($0) } XCTAssertNotNil(result) XCTAssertEqual(variable.name, result.name) XCTAssertEqual(variable.options, result.options) XCTAssertEqual(result.performMap(input: "1", interpreter: TypedInterpreter()) as! Double, 1) } // MARK: performMap func test_whenCallingPerformMap_thenUsesMapClosure() { let variable = GenericVariable("name") { _ in 123 } let result = variable.performMap(input: 1, interpreter: DummyInterpreter()) XCTAssertEqual(result as! Int, 123) } // MARK: matches performance func test_whenCallingMatchesWithShortInput_thenPerformsEffectively() { let variable = GenericVariable("name") XCTAssertTrue(variable.matches(prefix: "asd").isAnyMatch()) } func test_whenCallingMatchesWithLargeInput_thenPerformsEffectively() { let variable = GenericVariable("name") XCTAssertTrue(variable.matches(prefix: """ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. """).isAnyMatch()) } } ================================================ FILE: Tests/EvalTests/Utils.swift ================================================ import Eval import Foundation class DummyInterpreter: Interpreter { typealias VariableEvaluator = DummyInterpreter typealias EvaluatedType = String var context: Context var interpreterForEvaluatingVariables: DummyInterpreter { return self } func evaluate(_ expression: String) -> String { return "a" } func evaluate(_ expression: String, context: Context) -> String { return "a" } init() { context = Context() } } ================================================ FILE: Tests/LinuxMain.swift ================================================ @testable import InterpreterTests import XCTest XCTMain([ testCase(InterpreterTests.allTests) ])