├── .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 ├── Gemfile.lock ├── 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 /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | ignore: 3 | - Applications/Xcode.app/.* 4 | - build/.* 5 | - .build/.* 6 | - Documentation/.* 7 | - Example/Pods/.* 8 | - Scripts/.* 9 | - vendor/.* 10 | precision: 2 11 | round: down 12 | range: "80...100" 13 | status: 14 | patch: yes 15 | project: yes 16 | changes: no -------------------------------------------------------------------------------- /.github/.config.yml: -------------------------------------------------------------------------------- 1 | updateDocsComment: > 2 | 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. 3 | 4 | requestInfoReplyComment: > 5 | We would appreciate it if you could provide us with more info about this issue/pr! 6 | 7 | requestInfoLabelToAdd: request-more-info 8 | 9 | newPRWelcomeComment: > 10 | Thanks so much for opening your first PR here! 11 | 12 | firstPRMergeComment: > 13 | Congrats on merging your first pull request here! :tada: How awesome! 14 | 15 | newIssueWelcomeComment: > 16 | Thanks for opening this issue, a maintainer will get back to you shortly! 17 | 18 | sentimentBotToxicityThreshold: .7 19 | 20 | sentimentBotReplyComment: > 21 | Please be sure to review the code of conduct and be respectful of other users 22 | 23 | lockThreads: 24 | toxicityThreshold: .7 25 | numComments: 2 26 | setTimeInHours: 72 27 | replyComment: > 28 | This thread is being locked due to exceeding the toxicity minimums -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | build 4 | /Packages 5 | 6 | xcuserdata 7 | .xcuserstate 8 | 9 | Pods 10 | Package.resolved 11 | 12 | docs 13 | gh-pages 14 | Documentation/Output -------------------------------------------------------------------------------- /.jazzy.yml: -------------------------------------------------------------------------------- 1 | author: Laszlo Teveli 2 | author_url: https://tevelee.github.io 3 | readme: README.md 4 | 5 | documentation: Documentation/*.md 6 | abstract: Documentation/Sections/*.md 7 | 8 | module: Eval 9 | xcodebuild_arguments: [-scheme,Eval-Package] 10 | 11 | output: Documentation/Output 12 | theme: apple 13 | 14 | github_url: https://github.com/tevelee/Eval 15 | github_file_prefix: https://github.com/tevelee/Eval/tree/master 16 | 17 | root_url: https://tevelee.github.io/Eval 18 | 19 | clean: true 20 | min_acl: internal 21 | 22 | framework_root: Sources 23 | source_directory: . -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | reporter: "xcode" 2 | opt_in_rules: 3 | - array_init 4 | - attributes 5 | - block_based_kvo 6 | - class_delegate_protocol 7 | - closing_brace 8 | - closure_end_indentation 9 | - closure_parameter_position 10 | - closure_spacing 11 | - colon 12 | - comma 13 | - compiler_protocol_init 14 | # - conditional_returns_on_newline 15 | - contains_over_first_not_nil 16 | - control_statement 17 | - custom_rules 18 | - cyclomatic_complexity 19 | - discarded_notification_center_observer 20 | - discouraged_direct_init 21 | - discouraged_object_literal 22 | - dynamic_inline 23 | - empty_count 24 | - empty_enum_arguments 25 | - empty_parameters 26 | - empty_parentheses_with_trailing_closure 27 | # - explicit_acl 28 | - explicit_enum_raw_value 29 | - explicit_init 30 | - explicit_top_level_acl 31 | # - explicit_type_interface 32 | - extension_access_modifier 33 | - fallthrough 34 | - fatal_error_message 35 | - file_header 36 | - file_length 37 | - first_where 38 | - for_where 39 | - force_cast 40 | - force_try 41 | - force_unwrapping 42 | - function_body_length 43 | - function_parameter_count 44 | - generic_type_name 45 | - identifier_name 46 | - implicit_getter 47 | - implicit_return 48 | - implicitly_unwrapped_optional 49 | - is_disjoint 50 | - joined_default_parameter 51 | - large_tuple 52 | - leading_whitespace 53 | - legacy_cggeometry_functions 54 | - legacy_constant 55 | - legacy_constructor 56 | - legacy_nsgeometry_functions 57 | - let_var_whitespace 58 | - line_length 59 | - literal_expression_end_indentation 60 | - mark 61 | - multiline_arguments 62 | - multiline_parameters 63 | - multiple_closures_with_trailing_closure 64 | - nesting 65 | - nimble_operator 66 | # - no_extension_access_modifier 67 | # - no_grouping_extension 68 | - notification_center_detachment 69 | - number_separator 70 | - object_literal 71 | - opening_brace 72 | - operator_usage_whitespace 73 | - operator_whitespace 74 | - overridden_super_call 75 | - override_in_extension 76 | - pattern_matching_keywords 77 | - prefixed_toplevel_constant 78 | - private_action 79 | - private_outlet 80 | - private_over_fileprivate 81 | - private_unit_test 82 | - prohibited_super_call 83 | - protocol_property_accessors_order 84 | - quick_discouraged_call 85 | - quick_discouraged_focused_test 86 | - quick_discouraged_pending_test 87 | - redundant_discardable_let 88 | - redundant_nil_coalescing 89 | - redundant_optional_initialization 90 | - redundant_string_enum_value 91 | - redundant_void_return 92 | - required_enum_case 93 | - return_arrow_whitespace 94 | - shorthand_operator 95 | - single_test_class 96 | - sorted_first_last 97 | - sorted_imports 98 | - statement_position 99 | - strict_fileprivate 100 | - superfluous_disable_command 101 | - switch_case_alignment 102 | - switch_case_on_newline 103 | - syntactic_sugar 104 | - todo 105 | - trailing_closure 106 | - trailing_comma 107 | - trailing_newline 108 | - trailing_semicolon 109 | - trailing_whitespace 110 | - type_body_length 111 | - type_name 112 | - unneeded_break_in_switch 113 | - unneeded_parentheses_in_closure_argument 114 | - unused_closure_parameter 115 | - unused_enumerated 116 | - unused_optional_binding 117 | - valid_ibinspectable 118 | - vertical_parameter_alignment 119 | - vertical_parameter_alignment_on_call 120 | - vertical_whitespace 121 | - void_return 122 | - weak_delegate 123 | - xctfail_message 124 | - yoda_condition 125 | included: 126 | - Sources 127 | - Tests 128 | - Examples/AttributedStringExample/Sources 129 | - Examples/AttributedStringExample/Tests 130 | - Examples/ColorParserExample/Sources 131 | - Examples/ColorParserExample/Tests 132 | - Examples/TemplateExample/Sources 133 | - Examples/TemplateExample/Tests 134 | - Scripts/Sources 135 | force_cast: error 136 | force_try: error 137 | line_length: 320 138 | type_body_length: 139 | - 300 140 | - 400 141 | file_length: 142 | warning: 500 143 | error: 1000 144 | type_name: 145 | min_length: 4 146 | max_length: 25 147 | identifier_name: 148 | min_length: 3 149 | max_length: 35 150 | file_header: 151 | severity: error 152 | required_string: | 153 | /* 154 | * Copyright (c) 2018 Laszlo Teveli. 155 | * 156 | * Licensed to the Apache Software Foundation (ASF) under one 157 | * or more contributor license agreements. See the NOTICE file 158 | * distributed with this work for additional information 159 | * regarding copyright ownership. The ASF licenses this file 160 | * to you under the Apache License, Version 2.0 (the 161 | * "License"); you may not use this file except in compliance 162 | * with the License. You may obtain a copy of the License at 163 | * 164 | * http://www.apache.org/licenses/LICENSE-2.0 165 | * 166 | * Unless required by applicable law or agreed to in writing, 167 | * software distributed under the License is distributed on an 168 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 169 | * KIND, either express or implied. See the License for the 170 | * specific language governing permissions and limitations 171 | * under the License. 172 | */ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | osx_image: xcode11.3 2 | language: swift 3 | sudo: true 4 | env: 5 | global: 6 | - EXPANDED_CODE_SIGN_IDENTITY="-" 7 | - EXPANDED_CODE_SIGN_IDENTITY_NAME="-" 8 | - EXPANDED_PROVISIONING_PROFILE="-" 9 | before_script: 10 | - sh Scripts/git_auth.sh 11 | script: 12 | - travis_retry Scripts/ci.sh 13 | - sleep 3 14 | -------------------------------------------------------------------------------- /.version: -------------------------------------------------------------------------------- 1 | 1.5.0 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Eval 2 | 3 | ## 👍🎉 First off, thanks for taking the time to contribute! 🎉👍 4 | 5 | **You are more than welcomed to do so!** 6 | 7 | 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. 8 | 9 | ## Code of Conduct 10 | 11 | Nothing serious, all I ask is to be respectful and as helpful as you would expect others commnicating with you. 12 | 13 | ## I don't want to read this whole thing, I just have a question! 14 | 15 | 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`. 16 | 17 | If you have bigger concerns, feel free to write a GitHub issue. 18 | 19 | ## What should I know before I get started? 20 | 21 | First of all, please read the [documentation pages](Documentation), and feel free to check out the examples. 22 | 23 | There are a few things you need to be aware of when contributing: 24 | 25 | * 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. 26 | * There is a CI up and running 27 | * I use SwiftLint with build-in rules to keep the code as nice as possible 28 | * There is Danger configured with some rules, auto-checking contributions before I do 29 | * I intend to keep the code test coverage as high as possible. Please be mindful about this when contributing 30 | 31 | # How Can I Contribute? 32 | 33 | ## Reporting Bugs or Suggesting Enhancements 34 | 35 | Feel free to use the same channels as I described in the README: 36 | 37 | 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`. 38 | 39 | If you have bigger concerns, feel free to write a GitHub issue. 40 | 41 | ## Pull Requests 42 | 43 | ### Git Commit Messages 44 | 45 | Please use git rebase keep your number commits as low as possible. 46 | 47 | ### Documentation Styleguide 48 | 49 | I use a standard style of markdown pages in the [README.md](README.md) and in the [Documentation folder](Documentation). 50 | 51 | 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%. 52 | 53 | ### Issue and Pull Request Labels 54 | 55 | No rules you need to be aware of 56 | -------------------------------------------------------------------------------- /Dangerfile: -------------------------------------------------------------------------------- 1 | # Sometimes it's a README fix, or something like that - which isn't relevant for 2 | # including in a project's CHANGELOG for example 3 | not_declared_trivial = !(github.pr_title.include? "#trivial") 4 | has_app_changes = !git.modified_files.grep(/Sources/).empty? 5 | 6 | # ENSURE THAT LABELS HAVE BEEN USED ON THE PR 7 | fail "Please add labels to this PR" if github.pr_labels.empty? 8 | 9 | # Mainly to encourage writing up some reasoning about the PR, rather than just leaving a title 10 | if github.pr_body.length < 5 11 | fail "Please provide a summary in the Pull Request description" 12 | end 13 | 14 | # Pay extra attention if external contributors modify certain files 15 | if git.modified_files.include?("LICENSE.txt") 16 | fail "External contributor has edited the LICENSE.txt" 17 | end 18 | if git.modified_files.include?("Gemfile") or git.modified_files.include?("Gemfile.lock") 19 | warn "External contributor has edited the Gemfile and/or Gemfile.lock" 20 | end 21 | if git.modified_files.include?("Eval.podspec") or git.modified_files.include?("Package.swift") 22 | warn "External contributor has edited the Eval.podspec and/or Package.swift" 23 | end 24 | 25 | # Make it more obvious that a PR is a work in progress and shouldn't be merged yet 26 | warn("PR is classed as Work in Progress") if github.pr_title.include? "WIP" 27 | 28 | # Warn when there is a big PR 29 | warn("Big PR, try to keep changes smaller if you can") if git.lines_of_code > 500 30 | 31 | # Changelog entries are required for changes to library files. 32 | no_changelog_entry = !git.modified_files.include?("Changelog.md") 33 | if has_app_changes && no_changelog_entry && not_declared_trivial 34 | #warn("Any changes to library code should be reflected in the Changelog. Please consider adding a note there") 35 | end 36 | 37 | # Added (or removed) library files need to be added (or removed) from the Carthage Xcode project to avoid breaking things for our Carthage users. 38 | added_swift_library_files = !(git.added_files.grep(/Sources.*\.swift/).empty?) 39 | deleted_swift_library_files = !(git.deleted_files.grep(/Sources.*\.swift/).empty?) 40 | modified_carthage_xcode_project = !(git.modified_files.grep(/Eval\.xcodeproj/).empty?) 41 | if (added_swift_library_files || deleted_swift_library_files) && !modified_carthage_xcode_project 42 | warn("Added or removed library files require the Carthage Xcode project to be updated") 43 | end 44 | 45 | missing_doc_changes = git.modified_files.grep(/Documentation/).empty? 46 | doc_changes_recommended = git.insertions > 15 47 | if has_app_changes && missing_doc_changes && doc_changes_recommended && not_declared_trivial 48 | warn("Consider adding supporting documentation to this change. Documentation can be found in the `Documentation` directory.") 49 | end 50 | 51 | # Warn when library files has been updated but not tests. 52 | tests_updated = !git.modified_files.grep(/Tests/).empty? 53 | if has_app_changes && !tests_updated 54 | warn("The library files were changed, but the tests remained unmodified. Consider updating or adding to the tests to match the library changes.") 55 | end 56 | 57 | # Give inline build results (compile and link time warnings and errors) 58 | xcode_summary.report 'build/tests/summary.json' if File.file?('build/tests/summary.json') 59 | xcode_summary.report 'build/example/summary.json' if File.file?('build/example/summary.json') 60 | 61 | # Run SwiftLint 62 | swiftlint.lint_files 63 | #swiftlint.lint_files inline_mode: true 64 | -------------------------------------------------------------------------------- /Documentation/Example projects.md: -------------------------------------------------------------------------------- 1 | # Example projects 2 | 3 | I included a few use-cases, which bring significant improvements on how things are processed before - at least in my previous projects. 4 | ​ 5 | ### [Template language](https://github.com/tevelee/Eval/blob/master/Examples/TemplateExample/Tests/TemplateExampleTests/TemplateExampleTests.swift) 6 | 7 | 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! 8 | 9 | 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) 10 | 11 | Together, it makes an excellent addition to my model-object generation project, and **REALLY useful for server-side Swift development as well**! 12 | 13 | ### [Attributed string parser](https://github.com/tevelee/Eval/blob/master/Examples/AttributedStringExample/Tests/AttributedStringExampleTests/AttributedStringExampleTests.swift) 14 | 15 | I created another small example, parsing attribtuted strings from simple expressions using XML style tags, such as bold, italic, underlined, colored, etc. 16 | 17 | With just a few operators, this solution can deliver attributed strings from basic APIs, which otherwise would be hard to manage. 18 | 19 | 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. 20 | 21 | ### [Color parser](https://github.com/tevelee/Eval/blob/master/Examples/ColorParserExample/Tests/ColorParserExampleTests/ColorParserExampleTests.swift) 22 | 23 | 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. 24 | 25 | -------------------------------------------------------------------------------- /Documentation/Interpreter engine details.md: -------------------------------------------------------------------------------- 1 | # Technical details 2 | 3 | ## Interpreter engine 4 | 5 | TBD 6 | 7 | ## Template engine 8 | 9 | TBD -------------------------------------------------------------------------------- /Documentation/Strongly-typed evaluator.md: -------------------------------------------------------------------------------- 1 | # Strongly typed evaluator 2 | 3 | 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. 4 | 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. 5 | 6 | ## Creation 7 | 8 | The way to create typed interpreters is the following: 9 | 10 | ```swift 11 | let interpreter = TypedInterpreter(dataTypes: [number, string, boolean, array, date], 12 | functions: [concat, add, multiply, substract, divide], 13 | context: Context(variables: ["example": 1])) 14 | ``` 15 | 16 | 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) 17 | 18 | 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. 19 | Typically, in case of numbers, these are numeric operators. In case of string, these can be concatenation, getters, slicing, etc. 20 | Or, these can be complex things, such as parentheses, data factories, or high level functional operations, such as filter, map or reduce. 21 | 22 | And lastly, an optional context object, if you have any global variables. 23 | 24 | Variables in the context can be expression specific, or global that apply to every evaluation session. 25 | 26 | ## Data types 27 | 28 | Data types map the outside world to the inside of the expression. They map existing types to inner data types. 29 | 30 | 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. 31 | 32 | Let's see it in action: 33 | 34 | ```swift 35 | let number = DataType(type: Double.self, literals: [numberLiteral, piConstant]) { String(describing: $0) } 36 | ``` 37 | 38 | 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. 39 | 40 | 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. 41 | 42 | 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. 43 | 44 | In summary: literals provide the input, print provides the output of a mapped type. 45 | 46 | ### Literals 47 | 48 | Let's check out some literals a bit more deeply. 49 | 50 | The block used for literals have two parameters: the input string, and an interpreter. 51 | Most of the times, only the input is enough to recognise things, like numbers: 52 | 53 | ```swift 54 | Literal { value, _ in Double(value) } 55 | ``` 56 | Arrays, on the other hand, should process their content (Comma `,` separated values between brackets `[` `]`). For this, the second parameter, the interpreter can be used. 57 | 58 | #### Constants 59 | 60 | Literals are the perfect place to recognise constants, such as: 61 | 62 | ```swift 63 | Literal("pi", convertsTo: Double.pi) 64 | ``` 65 | or 66 | 67 | ```swift 68 | [Literal("false", convertsTo: false) 69 | ``` 70 | 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. 71 | 72 | The `convertsTo` parameter of `Literal`s are `autoclosure` parameters, which means, that they are going to be processed lazily. 73 | 74 | ```swift 75 | Literal("now", convertsTo: Date()) 76 | ``` 77 | 78 | The `now` string is going to be expressed as the current timestamp at the time of the evaluation, not the time of the initialisation. 79 | 80 | ## Functions 81 | 82 | Similarly to templates, typed interpreters use the same building blocks to build up their patterns: `Keyword`s and `Variable`s. 83 | 84 | ### Keywords 85 | 86 | `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. 87 | 88 | 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. 89 | 90 | ### Variables 91 | 92 | Let's check out the following, really straightforward pattern: 93 | 94 | ```swift 95 | Function(Keyword("(") + Variable("body") + Keyword(")")) { variables, _, _ in 96 | return variables["body"] 97 | } 98 | ``` 99 | 100 | 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`. 101 | 102 | #### Generics 103 | 104 | 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. 105 | 106 | Let check out a `+` operator. This could equally mean addition for numeric types 107 | 108 | ```swift 109 | Function(Variable("lhs") + Keyword("+") + Variable("rhs")) { arguments,_,_ in 110 | guard let lhs = arguments["lhs"] as? Double, let rhs = arguments["rhs"] as? Double else { return nil } 111 | return lhs + rhs 112 | } 113 | ``` 114 | or concatenation for strings 115 | 116 | ```swift 117 | Function(Variable("lhs") + Keyword("+") + Variable("rhs")) { arguments,_,_ in 118 | guard let lhs = arguments["lhs"] as? String, let rhs = arguments["rhs"] as? String else { return nil } 119 | return lhs + rhs 120 | } 121 | ``` 122 | Since the interpreter is strongly typed, always the appropriate one is going to be selected by the framework. 123 | 124 | #### Evaluation 125 | 126 | Variables also have optional properties, such as `interpreted`, `shortest`, or `acceptsNilValue`. They might also have a `map` block, which by default is `nil`. 127 | 128 | * `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`. 129 | * `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! 130 | 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. 131 | But don't worry, the framework already knows about this, so it sets the right value for your variables, even in the last place! 132 | * `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. 133 | * 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. 134 | 135 | ### Specialised elements 136 | 137 | #### Open & Close Keyword 138 | 139 | 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. 140 | 141 | 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. 142 | 143 | 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. 144 | 145 | The previous parentheses statement should - correctly - look like this: 146 | 147 | ```swift 148 | Function(OpenVariable("lhs") + Keyword("+") + CloseVariable("rhs")) { arguments,_,_ in 149 | guard let lhs = arguments["lhs"] as? String, let rhs = arguments["rhs"] as? String else { return nil } 150 | return lhs + rhs 151 | } 152 | ``` 153 | 154 | By using the `OpenKeyword` and `CloseKeyword` types, these become connected, so embedding parentheses in an expression shouldn't be a problem. 155 | After this match is defined, they can be embedded in each other as deeply as needed. 156 | 157 | #### Multiple Patterns in one Function 158 | 159 | This is a rarely used pattern, but `Function`s consists of an array of `Pattern` elements. 160 | Usually, one `Function` does only one operation. Unless this is true, grouping multiple `Pattern`s into one `Function` allows semantical grouping of opeartors. 161 | 162 | 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. 163 | 164 | ## Context 165 | 166 | You can also pass contextual values, which - for now - equal to variables. 167 | 168 | ```swift 169 | expression.evaluate("1 + var", context: Context(variables: ["var": 2])) 170 | ``` 171 | 172 | 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. 173 | 174 | 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. 175 | 176 | 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). 177 | 178 | ### Order of statements define precedence 179 | 180 | The earlier a pattern is represent in the array of `functions`, the higher precedence it gets. 181 | 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: 182 | `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. 183 | 184 | Typically, parentheses and higher precedence operators should go earlier in the array. -------------------------------------------------------------------------------- /Documentation/Template evaluator.md: -------------------------------------------------------------------------------- 1 | # Template evaluator 2 | 3 | The logic of the interpreter is fairly easy: it goes over the input character by character and replaces any patterns it can find. 4 | 5 | ## Creation 6 | 7 | The way to create template interpreters is the following: 8 | 9 | ```swift 10 | let template = StringTemplateInterpreter(statements: [ifStatement, printStatement], 11 | interpreter: interpreter, 12 | context: Context(variables: ["example": 1])) 13 | ``` 14 | 15 | First, you'll need the statements that you aim to recognise. 16 | Then, you'll need a typed interpreter, so that you can evaluate strongly typed expressions. 17 | And lastly, an optional context object, if you have any global variables. 18 | 19 | Variables in the context can be expression specific, or global that apply to every evaluation session. 20 | 21 | 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. 22 | 23 | ## Statement examples 24 | 25 | ### Keywords 26 | 27 | `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. 28 | 29 | 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. 30 | 31 | ### Variables 32 | 33 | Let's check out the following, really straightforward pattern: 34 | 35 | ```swift 36 | Pattern(Keyword("{{") + Variable("body") + Keyword("}}")) { variables, interpreter, _ in 37 | guard let body = variables["body"] else { return nil } 38 | return interpreter.typedInterpreter.print(body) 39 | } 40 | ``` 41 | 42 | 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`. 43 | 44 | #### Generics 45 | 46 | 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. 47 | 48 | #### Evaluation 49 | 50 | Variables also have optional properties, such as `interpreted`, `shortest`, or `acceptsNilValue`. They might also have a `map` block, which by default is `nil`. 51 | 52 | * `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`. 53 | * `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! 54 | 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. 55 | But don't worry, the framework already knows about this, so it sets the right value for your variables, even in the last place! 56 | * `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. 57 | * 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. 58 | 59 | ### Specialised elements 60 | 61 | #### Template Variable 62 | 63 | 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: 64 | 65 | 66 | ```swift 67 | Pattern(Keyword("{%") + Keyword("if") + Variable("condition") + Keyword("%}") + TemplateVariable("body") + Keyword("{% endif %}")) { variables, interpreter, _ in 68 | guard let condition = variables["condition"] as? Bool, let body = variables["body"] as? String else { return nil } 69 | if condition { 70 | return body 71 | } 72 | return nil 73 | } 74 | ``` 75 | 76 | 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. 77 | 78 | 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 79 | 80 | 1. Should not be interpreted 81 | 2. Should be evaluated using the template interpreter, not the typed interpreter 82 | 83 | 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. 84 | 85 | A quick example: `Header ... {% if x > 0 %}Number of results: {{ x }} {% endif %} ... Footer` 86 | 87 | Here, `x > 0` is a `Boolean` expression, but the body between the `if`, and `endif` tags is a template, such as the whole expression. 88 | 89 | #### Open & Close Keyword 90 | 91 | `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. 92 | 93 | 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. 94 | 95 | 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. 96 | 97 | The previous `if` statement, now with an `else` block should - correctly - look like this: 98 | 99 | ```swift 100 | Pattern(OpenKeyword("{% "if") + Variable("condition") + Keyword("%}") + TemplateVariable("body") + Keyword("{% else %}") + TemplateVariable("else") + CloseKeyword("{% endif %}")) { variables, interpreter, _ in 101 | guard let condition = variables["condition"] as? Bool, let body = variables["body"] as? String else { return nil } 102 | if condition { 103 | return body 104 | } else { 105 | return variables["else"] as? String 106 | } 107 | } 108 | ``` 109 | 110 | By using the `OpenKeyword` and `CloseKeyword` types, these become connected, so embedding `if` statements in a template shouldn't be a problem. 111 | 112 | Similarly, this works for the `print` statement from an earlier example: 113 | 114 | ```swift 115 | Pattern(OpenKeyword("{{") + Variable("body") + CloseKeyword("}}")) { variables, interpreter, _ in 116 | guard let body = variables["body"] else { return nil } 117 | return interpreter.typedInterpreter.print(body) 118 | } 119 | ``` 120 | 121 | ## Evaluation 122 | 123 | The evaluation of the templates happens with the `evaluate` function on the interpreter: 124 | 125 | ```swift 126 | template.evaluate("{{ 1 + 2 }}") 127 | ``` 128 | 129 | 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. 130 | 131 | ### Context 132 | 133 | You can also pass contextual values, which - for now - equal to variables. 134 | 135 | ```swift 136 | template.evaluate("{{ 1 + var }}", context: Context(variables: ["var": 2])) 137 | ``` 138 | 139 | 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. 140 | 141 | 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. 142 | 143 | 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). 144 | 145 | ### Order of statements define precedence 146 | 147 | The earlier a pattern is represent in the array of `statements`, the higher precedence it gets. 148 | 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: 149 | `{% 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`. 150 | 151 | Typically, parentheses and richer type of expressions should go earlier in the array. 152 | -------------------------------------------------------------------------------- /Documentation/Tips & Tricks.md: -------------------------------------------------------------------------------- 1 | # Tips & Tricks 2 | 3 | The following sections provide handy Tips and Tricks to help you effectively build up your own interpreter using custom operators and data types. 4 | 5 | ## Get inspired by checking out the examples 6 | 7 | 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 8 | 9 | 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. 10 | 11 | ## Use helper functions to define operators 12 | 13 | It's a lot readable to define operators in a one-liner expression, rather than using long patterns: 14 | 15 | ```swift 16 | infixOperator("+") { (lhs: String, rhs: String) in lhs + rhs } 17 | ``` 18 | ```swift 19 | suffixOperator("is odd") { (value: Double) in Int(value) % 2 == 1 } 20 | ``` 21 | ```swift 22 | prefixOperator("!") { (value: Bool) in !value } 23 | ``` 24 | 25 | 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! 26 | 27 | ## Be mindful about precedence 28 | 29 | #### Template expressions 30 | 31 | The earlier a pattern is represent in the array of `statements`, the higher precedence it gets. 32 | 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: 33 | `{% 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`. 34 | 35 | Typically, parentheses and richer type of expressions should go earlier in the array. 36 | 37 | #### Typed expressions 38 | 39 | The earlier a pattern is represent in the array of `functions`, the higher precedence it gets. 40 | 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: 41 | `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. 42 | 43 | Typically, parentheses and higher precedence operators should go earlier in the array. 44 | 45 | ## Use Any for generics: `Variable` 46 | 47 | 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. 48 | 49 | It makes life a lot easier, than definig functions for each type. 50 | 51 | ## Use map on `Variable`s for pre-filtering 52 | 53 | Before processing Variable values, there is an option to pre-filter or modify them before it hits the match block. 54 | 55 | Examples include data type conversion and other types of validation. 56 | 57 | ## Use `OpenKeyword` and `CloseKeyword` for embedding parentheses 58 | 59 | 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. 60 | 61 | ## Share context between `StringTemplateInterpreter` and `TypedInterpreter` 62 | 63 | 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. 64 | 65 | Since `Context` is a class, its reference can be passed around and used in multiple places. 66 | 67 | 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. 68 | 69 | 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. 70 | 71 | ## Define constants in `Literal`s 72 | 73 | The frameworks allows multiple ways to express static strings and convert them. 74 | I believe the best place to put constants are in the `Literal`s of `DataType`s. 75 | 76 | Use the `Literal("YES", convertsTo: true)` `Literal` initialiser for easy definition. 77 | 78 | The `convertsTo` parameter of `Literal`s are `autoclosure` parameters, which means, that they are going to be processed lazily. 79 | 80 | ```swift 81 | Literal("now", convertsTo: Date()) 82 | ``` 83 | 84 | The `now` string is going to be expressed as the current timestamp at the time of the evaluation, not the time of the initialisation. 85 | 86 | ## Map any function signatures from Swift, dynamically 87 | 88 | The framework is really lightweight and not really restrictive in regards of how to parse your expressions. Free your mind, and do stuff dynamically. 89 | 90 | ```swift 91 | Function(Variable("lhs") + Keyword(".") + Variable("rhs", interpreted: false)) { (arguments,_,_) -> Double? in 92 | if let lhs = arguments["lhs"] as? NSObjectProtocol, 93 | let rhs = arguments["rhs"] as? String, 94 | let result = lhs.perform(Selector(rhs)) { 95 | return Double(Int(bitPattern: result.toOpaque())) 96 | } 97 | return nil 98 | } 99 | ]) 100 | ``` 101 | 102 | 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. 103 | 104 | 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. 105 | 106 | ## Experiment with your expressions! 107 | 108 | 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. 109 | 110 | You can always add new types or functions if you need extra functionality. The options are practically endless! 111 | 112 | ## Debugging tips 113 | 114 | #### If an expression haven't been matched 115 | * It's common, that some validation caught the value 116 | * Print your expressions or put breakpoints into the affected match blocks or variable map blocks 117 | 118 | #### If you see weird output 119 | * Play with the order of the newly added opeartions. 120 | * Incorrect precedence can turn expressions upside down 121 | 122 | The framework is still in an early stage, so debugging helpers will follow in upcoming releases. Please stay tuned! 123 | 124 | ## Validate your expressions before putting them out in production code 125 | 126 | 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. 127 | 128 | 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. 129 | -------------------------------------------------------------------------------- /Eval.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | //: Playground - noun: a place where people can play 2 | import Foundation 3 | import Eval 4 | 5 | let context = InterpreterContext() 6 | 7 | let interpreter = TypedInterpreter(dataTypes: [numberDataType, stringDataType, arrayDataType, booleanDataType, dateDataType], 8 | functions: [parentheses, multipication, addition, lessThan], 9 | context: context) 10 | 11 | let template = TemplateInterpreter(statements: [ifStatement, printStatement], 12 | interpreter: interpreter, 13 | context: context) 14 | 15 | interpreter.evaluate("2 + 3 * 4") 16 | 17 | template.evaluate("{% if 10 < 21 %}Hello{% endif %} {{ name }}!", context: InterpreterContext(variables: ["name": "Eval"])) 18 | -------------------------------------------------------------------------------- /Eval.playground/Sources/Helpers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Eval 3 | 4 | public func infixOperator(_ symbol: String, body: @escaping (A, B) -> T) -> Function { 5 | return Function([Variable("lhs", shortest: true), Keyword(symbol), Variable("rhs", shortest: false)]) { arguments,_,_ in 6 | guard let lhs = arguments["lhs"] as? A, let rhs = arguments["rhs"] as? B else { return nil } 7 | return body(lhs, rhs) 8 | } 9 | } 10 | 11 | public func prefixOperator(_ symbol: String, body: @escaping (A) -> T) -> Function { 12 | return Function([Keyword(symbol), Variable("value", shortest: false)]) { arguments,_,_ in 13 | guard let value = arguments["value"] as? A else { return nil } 14 | return body(value) 15 | } 16 | } 17 | 18 | public func suffixOperator(_ symbol: String, body: @escaping (A) -> T) -> Function { 19 | return Function([Variable("value", shortest: true), Keyword(symbol)]) { arguments,_,_ in 20 | guard let value = arguments["value"] as? A else { return nil } 21 | return body(value) 22 | } 23 | } 24 | 25 | public func function(_ name: String, body: @escaping ([Any]) -> T?) -> Function { 26 | return Function([Keyword(name), Keyword("("), Variable("arguments", shortest: true, interpreted: false), Keyword(")")]) { variables, interpreter, _ in 27 | guard let arguments = variables["arguments"] as? String else { return nil } 28 | let interpretedArguments = arguments.split(separator: ",").flatMap { interpreter.evaluate(String($0).trimmingCharacters(in: .whitespacesAndNewlines)) } 29 | return body(interpretedArguments) 30 | } 31 | } 32 | 33 | public func functionWithNamedParameters(_ name: String, body: @escaping ([String: Any]) -> T?) -> Function { 34 | return Function([Keyword(name), Keyword("("), Variable("arguments", shortest: true, interpreted: false), Keyword(")")]) { variables, interpreter, _ in 35 | guard let arguments = variables["arguments"] as? String else { return nil } 36 | var interpretedArguments: [String: Any] = [:] 37 | for argument in arguments.split(separator: ",") { 38 | let parts = String(argument).trimmingCharacters(in: .whitespacesAndNewlines).split(separator: "=") 39 | if let key = parts.first, let value = parts.last { 40 | interpretedArguments[String(key)] = interpreter.evaluate(String(value)) 41 | } 42 | } 43 | return body(interpretedArguments) 44 | } 45 | } 46 | 47 | public func objectFunction(_ name: String, body: @escaping (O) -> T?) -> Function { 48 | return Function([Variable("lhs", shortest: true), Keyword("."), Variable("rhs", shortest: false, interpreted: false) { value,_ in 49 | guard let value = value as? String, value == name else { return nil } 50 | return value 51 | }]) { variables, interpreter, _ in 52 | guard let object = variables["lhs"] as? O, variables["rhs"] != nil else { return nil } 53 | return body(object) 54 | } 55 | } 56 | 57 | public func objectFunctionWithParameters(_ name: String, body: @escaping (O, [Any]) -> T?) -> Function { 58 | return Function([Variable("lhs", shortest: true), Keyword("."), Variable("rhs", interpreted: false) { value,_ in 59 | guard let value = value as? String, value == name else { return nil } 60 | return value 61 | }, Keyword("("), Variable("arguments", interpreted: false), Keyword(")")]) { variables, interpreter, _ in 62 | guard let object = variables["lhs"] as? O, variables["rhs"] != nil, let arguments = variables["arguments"] as? String else { return nil } 63 | let interpretedArguments = arguments.split(separator: ",").flatMap { interpreter.evaluate(String($0).trimmingCharacters(in: .whitespacesAndNewlines)) } 64 | return body(object, interpretedArguments) 65 | } 66 | } 67 | 68 | public func objectFunctionWithNamedParameters(_ name: String, body: @escaping (O, [String: Any]) -> T?) -> Function { 69 | return Function([Variable("lhs", shortest: true), Keyword("."), Variable("rhs", interpreted: false) { value,_ in 70 | guard let value = value as? String, value == name else { return nil } 71 | return value 72 | }, Keyword("("), Variable("arguments", interpreted: false), Keyword(")")]) { variables, interpreter, _ in 73 | guard let object = variables["lhs"] as? O, variables["rhs"] != nil, let arguments = variables["arguments"] as? String else { return nil } 74 | var interpretedArguments: [String: Any] = [:] 75 | for argument in arguments.split(separator: ",") { 76 | let parts = String(argument).trimmingCharacters(in: .whitespacesAndNewlines).split(separator: "=") 77 | if let key = parts.first, let value = parts.last { 78 | interpretedArguments[String(key)] = interpreter.evaluate(String(value)) 79 | } 80 | } 81 | return body(object, interpretedArguments) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Eval.playground/Sources/TypesAndFunctions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Eval 3 | 4 | //MARK: Double 5 | 6 | public let numberDataType = DataType(type: Double.self, literals:[ 7 | Literal { Double($0.value) }, 8 | Literal("pi", convertsTo: Double.pi) 9 | ]) { value, _ in String(describing: value) } 10 | 11 | //MARK: Bool 12 | 13 | public let booleanDataType = DataType(type: Bool.self, literals: [ 14 | Literal("false", convertsTo: false), 15 | Literal("true", convertsTo: true) 16 | ]) { $0.value ? "true" : "false" } 17 | 18 | //MARK: String 19 | 20 | let singleQuotesLiteral = Literal { (input, _) -> String? in 21 | guard let first = input.first, let last = input.last, first == last, first == "'" else { return nil } 22 | let trimmed = input.trimmingCharacters(in: CharacterSet(charactersIn: "'")) 23 | return trimmed.contains("'") ? nil : trimmed 24 | } 25 | public let stringDataType = DataType(type: String.self, literals: [singleQuotesLiteral]) { $0.value } 26 | 27 | //MARK: Date 28 | 29 | public let dateDataType = DataType(type: Date.self, literals: [Literal("now", convertsTo: Date())]) { 30 | let dateFormatter = DateFormatter() 31 | dateFormatter.calendar = Calendar(identifier: .gregorian) 32 | dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" 33 | return dateFormatter.string(from: $0) 34 | } 35 | 36 | //MARK: Array 37 | 38 | let arrayLiteral = Literal { (input, interpreter) -> [CustomStringConvertible]? in 39 | guard let first = input.first, let last = input.last, first == "[", last == "]" else { return nil } 40 | return input 41 | .trimmingCharacters(in: CharacterSet(charactersIn: "[]")) 42 | .split(separator: ",") 43 | .map{ $0.trimmingCharacters(in: .whitespacesAndNewlines) } 44 | .map{ interpreter.evaluate(String($0)) as? CustomStringConvertible ?? String($0) } 45 | } 46 | public let arrayDataType = DataType(type: [CustomStringConvertible].self, literals: [arrayLiteral]) { $0.value.map{ $0.description }.joined(separator: ",") } 47 | 48 | //MARK: Operators 49 | 50 | public let max = objectFunction("max") { (object: [Double]) -> Double? in object.max() } 51 | public let min = objectFunction("min") { (object: [Double]) -> Double? in object.min() } 52 | 53 | public let formatDate = objectFunctionWithParameters("format") { (object: Date, arguments: [Any]) -> String? in 54 | guard let format = arguments.first as? String else { return nil } 55 | let dateFormatter = DateFormatter() 56 | dateFormatter.calendar = Calendar(identifier: .gregorian) 57 | dateFormatter.dateFormat = format 58 | return dateFormatter.string(from: object) 59 | } 60 | 61 | public let not = prefixOperator("!") { (value: Bool) in !value } 62 | 63 | public let dateFactory = function("Date") { (arguments: [Any]) -> Date? in 64 | guard let arguments = arguments as? [Double], arguments.count >= 3 else { return nil } 65 | var components = DateComponents() 66 | components.calendar = Calendar(identifier: .gregorian) 67 | components.year = Int(arguments[0]) 68 | components.month = Int(arguments[1]) 69 | components.day = Int(arguments[2]) 70 | components.hour = arguments.count > 3 ? Int(arguments[3]) : 0 71 | components.minute = arguments.count > 4 ? Int(arguments[4]) : 0 72 | components.second = arguments.count > 5 ? Int(arguments[5]) : 0 73 | return components.date 74 | } 75 | 76 | public let parentheses = Function([Keyword("("), Variable("body"), Keyword(")")]) { $0.variables["body"] } 77 | public let addition = infixOperator("+") { (lhs: Double, rhs: Double) in lhs + rhs } 78 | public let multipication = infixOperator("*") { (lhs: Double, rhs: Double) in lhs * rhs } 79 | public let concat = infixOperator("+") { (lhs: String, rhs: String) in lhs + rhs } 80 | public let inNumberArray = infixOperator("in") { (lhs: Double, rhs: [Double]) in rhs.contains(lhs) } 81 | public let inStringArray = infixOperator("in") { (lhs: String, rhs: [String]) in rhs.contains(lhs) } 82 | public let range = infixOperator("...") { (lhs: Double, rhs: Double) in CountableClosedRange(uncheckedBounds: (lower: Int(lhs), upper: Int(rhs))).map { Double($0) } } 83 | public let prefix = infixOperator("starts with") { (lhs: String, rhs: String) in lhs.hasPrefix(lhs) } 84 | public let isOdd = suffixOperator("is odd") { (value: Double) in Int(value) % 2 == 1 } 85 | public let isEven = suffixOperator("is even") { (value: Double) in Int(value) % 2 == 0 } 86 | public let lessThan = infixOperator("<") { (lhs: Double, rhs: Double) in lhs < rhs } 87 | public let greaterThan = infixOperator(">") { (lhs: Double, rhs: Double) in lhs > rhs } 88 | public let equals = infixOperator("==") { (lhs: Double, rhs: Double) in lhs == rhs } 89 | 90 | //MARK: Template elements 91 | 92 | public let ifStatement = Matcher([Keyword("{%"), Keyword("if"), Variable("condition"), Keyword("%}"), TemplateVariable("body"), Keyword("{%"), Keyword("endif"), Keyword("%}")]) { (variables, interpreter: StringTemplateInterpreter, _) -> String? in 93 | guard let condition = variables["condition"] as? Bool, let body = variables["body"] as? String else { return nil } 94 | if condition { 95 | return body 96 | } 97 | return nil 98 | } 99 | 100 | public let printStatement = Matcher([Keyword("{{"), Variable("body"), Keyword("}}")]) { (variables, interpreter: StringTemplateInterpreter, _) -> String? in 101 | guard let body = variables["body"] else { return nil } 102 | return interpreter.typedInterpreter.print(body) 103 | } 104 | -------------------------------------------------------------------------------- /Eval.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Eval.playground/playground.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Eval.playground/xcshareddata/xcschemes/Playground.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Eval.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "Eval" 3 | s.version = "1.5.0" 4 | s.summary = "Eval is a lightweight interpreter framework written in Swift, evaluating expressions at runtime" 5 | s.description = <<-DESC 6 | Eval is a lightweight interpreter framework written in Swift, for 📱iOS, 🖥 macOS, and 🐧Linux platforms. 7 | 8 | It evaluates expressions at runtime, with operators and data types you define. 9 | DESC 10 | s.homepage = "https://tevelee.github.io/Eval/" 11 | s.license = { :type => "Apache 2.0", :file => "LICENSE.txt" } 12 | s.author = { "Laszlo Teveli" => "tevelee@gmail.com" } 13 | s.social_media_url = "http://twitter.com/tevelee" 14 | s.source = { :git => "https://github.com/tevelee/Eval.git", :tag => "#{s.version}" } 15 | s.source_files = "Sources/**/*.{h,swift}" 16 | 17 | s.ios.deployment_target = "8.0" 18 | s.osx.deployment_target = "10.10" 19 | s.watchos.deployment_target = "2.0" 20 | s.tvos.deployment_target = "9.0" 21 | end 22 | -------------------------------------------------------------------------------- /Eval.xcodeproj/EvalTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Eval.xcodeproj/Eval_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Eval.xcodeproj/xcshareddata/xcschemes/Eval-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 36 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 58 | 59 | 65 | 66 | 67 | 68 | 69 | 70 | 76 | 77 | 79 | 80 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /Eval.xcodeproj/xcshareddata/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SchemeUserState 5 | 6 | Eval-Package.xcscheme 7 | 8 | 9 | SuppressBuildableAutocreation 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Eval.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Eval.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - file_header -------------------------------------------------------------------------------- /Examples/AttributedStringExample/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj -------------------------------------------------------------------------------- /Examples/AttributedStringExample/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.2 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "AttributedStringExample", 7 | products: [ 8 | .library( 9 | name: "AttributedStringExample", 10 | targets: ["AttributedStringExample"]) 11 | ], 12 | dependencies: [ 13 | .package(url: "../../", from: "1.4.0") 14 | ], 15 | targets: [ 16 | .target( 17 | name: "AttributedStringExample", 18 | dependencies: ["Eval"]), 19 | .testTarget( 20 | name: "AttributedStringExampleTests", 21 | dependencies: ["AttributedStringExample"]) 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /Examples/AttributedStringExample/README.md: -------------------------------------------------------------------------------- 1 | # TemplateExample 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Examples/AttributedStringExample/Sources/AttributedStringExample/TemplateExample.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | @_exported import Eval 3 | import Foundation 4 | @_exported import class Eval.Pattern 5 | 6 | // swiftlint:disable:next type_name 7 | public class AttributedStringTemplateInterpreter: TemplateInterpreter { 8 | typealias EvaluatedType = NSAttributedString 9 | 10 | override public func evaluate(_ expression: String, context: Context = Context()) -> NSAttributedString { 11 | return evaluate(expression, context: context, reducer: (initialValue: NSAttributedString(), reduceValue: { existing, next in 12 | existing.appending(next) 13 | }, reduceCharacter: { existing, next in 14 | existing.appending(NSAttributedString(string: String(next))) 15 | })) 16 | } 17 | } 18 | 19 | // swiftlint:disable:next type_name 20 | public class AttributedStringInterpreter: EvaluatorWithLocalContext { 21 | public typealias EvaluatedType = NSAttributedString 22 | 23 | let interpreter: AttributedStringTemplateInterpreter 24 | 25 | init() { 26 | let context = Context() 27 | 28 | let center = NSMutableParagraphStyle() 29 | center.alignment = .center 30 | 31 | interpreter = AttributedStringTemplateInterpreter(statements: [AttributedStringInterpreter.attributeMatcher(name: "bold", attributes: [.font: NSFont.boldSystemFont(ofSize: 12)]), 32 | AttributedStringInterpreter.attributeMatcher(name: "red", attributes: [.foregroundColor: NSColor.red]), 33 | AttributedStringInterpreter.attributeMatcher(name: "center", attributes: [.paragraphStyle: center])], 34 | interpreter: TypedInterpreter(context: context), 35 | context: context) 36 | } 37 | 38 | public func evaluate(_ expression: String) -> AttributedStringInterpreter.EvaluatedType { 39 | return interpreter.evaluate(expression) 40 | } 41 | 42 | public func evaluate(_ expression: String, context: Context) -> AttributedStringInterpreter.EvaluatedType { 43 | return interpreter.evaluate(expression, context: context) 44 | } 45 | 46 | static func attributeMatcher(name: String, attributes: [NSAttributedString.Key: Any]) -> Pattern> { 47 | return Pattern([OpenKeyword("<\(name)>"), GenericVariable("body", options: .notInterpreted), CloseKeyword("")]) { 48 | guard let body = $0.variables["body"] as? String else { return nil } 49 | return NSAttributedString(string: body, attributes: attributes) 50 | } 51 | } 52 | } 53 | 54 | public extension NSAttributedString { 55 | func appending(_ other: NSAttributedString) -> NSAttributedString { 56 | let mutable = NSMutableAttributedString(attributedString: self) 57 | mutable.append(other) 58 | return mutable 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Examples/AttributedStringExample/Tests/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - force_cast 3 | - force_try 4 | - type_name 5 | - file_header 6 | - explicit_top_level_acl -------------------------------------------------------------------------------- /Examples/AttributedStringExample/Tests/AttributedStringExampleTests/AttributedStringExampleTests.swift: -------------------------------------------------------------------------------- 1 | @testable import AttributedStringExample 2 | import Eval 3 | import XCTest 4 | 5 | class AttributedStringExampleTests: XCTestCase { 6 | let interpreter: AttributedStringInterpreter = AttributedStringInterpreter() 7 | 8 | func testExample() { 9 | let interpreter = AttributedStringInterpreter() 10 | 11 | XCTAssertEqual(interpreter.evaluate("Hello"), NSAttributedString(string: "Hello", attributes: [.font: NSFont.boldSystemFont(ofSize: 12)])) 12 | 13 | XCTAssertEqual(interpreter.evaluate("It's red"), NSAttributedString(string: "It's ").appending(NSAttributedString(string: "red", attributes: [.foregroundColor: NSColor.red]))) 14 | 15 | let style = interpreter.evaluate("
Centered text
").attribute(.paragraphStyle, at: 0, effectiveRange: nil) as! NSParagraphStyle 16 | XCTAssertEqual(style.alignment, .center) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Examples/AttributedStringExample/Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | @testable import TemplateExampleTests 2 | import XCTest 3 | 4 | XCTMain([ 5 | testCase(TemplateExampleTests.allTests) 6 | ]) 7 | -------------------------------------------------------------------------------- /Examples/ColorParserExample/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj -------------------------------------------------------------------------------- /Examples/ColorParserExample/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.2 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "ColorParserExample", 7 | products: [ 8 | .library( 9 | name: "ColorParserExample", 10 | targets: ["ColorParserExample"]) 11 | ], 12 | dependencies: [ 13 | .package(url: "../../", from: "1.4.0") 14 | ], 15 | targets: [ 16 | .target( 17 | name: "ColorParserExample", 18 | dependencies: ["Eval"]), 19 | .testTarget( 20 | name: "ColorParserExampleTests", 21 | dependencies: ["ColorParserExample"]) 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /Examples/ColorParserExample/README.md: -------------------------------------------------------------------------------- 1 | # TemplateExample 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Examples/ColorParserExample/Sources/ColorParserExample/ColorParserExample.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | @_exported import Eval 3 | @_exported import class Eval.Pattern 4 | import Foundation 5 | 6 | public class ColorParser: EvaluatorWithLocalContext { 7 | let interpreter: TypedInterpreter 8 | 9 | init() { 10 | interpreter = TypedInterpreter(dataTypes: [ColorParser.colorDataType()], functions: [ColorParser.mixFunction()]) 11 | } 12 | 13 | static func colorDataType() -> DataType { 14 | let hex = Literal { 15 | guard $0.value.first == "#", $0.value.count == 7, 16 | let red = Int($0.value[1...2], radix: 16), 17 | let green = Int($0.value[3...4], radix: 16), 18 | let blue = Int($0.value[5...6], radix: 16) else { return nil } 19 | return NSColor(calibratedRed: CGFloat(red), green: CGFloat(green), blue: CGFloat(blue), alpha: 1) 20 | } 21 | 22 | let red = Literal("red", convertsTo: NSColor.red) 23 | 24 | return DataType(type: NSColor.self, literals: [hex, red]) { $0.value.description } 25 | } 26 | 27 | static func mixFunction() -> Function { 28 | return Function([Variable("lhs"), Keyword("mixed with"), Variable("rhs")]) { 29 | guard let lhs = $0.variables["lhs"] as? NSColor, let rhs = $0.variables["rhs"] as? NSColor else { return nil } 30 | return lhs.blend(with: rhs) 31 | } 32 | } 33 | 34 | public func evaluate(_ expression: String) -> Any? { 35 | return interpreter.evaluate(expression) 36 | } 37 | 38 | public func evaluate(_ expression: String, context: Context) -> Any? { 39 | return interpreter.evaluate(expression, context: context) 40 | } 41 | } 42 | 43 | extension String { 44 | subscript (range: CountableClosedRange) -> Substring { 45 | return self[index(startIndex, offsetBy: range.lowerBound) ..< index(startIndex, offsetBy: range.upperBound)] 46 | } 47 | } 48 | 49 | extension NSColor { 50 | func blend(with other: NSColor, using factor: CGFloat = 0.5) -> NSColor { 51 | let inverseFactor = 1.0 - factor 52 | 53 | var leftRed: CGFloat = 0 54 | var leftGreen: CGFloat = 0 55 | var leftBlue: CGFloat = 0 56 | var leftAlpha: CGFloat = 0 57 | getRed(&leftRed, green: &leftGreen, blue: &leftBlue, alpha: &leftAlpha) 58 | 59 | var rightRed: CGFloat = 0 60 | var rightGreen: CGFloat = 0 61 | var rightBlue: CGFloat = 0 62 | var rightAlpha: CGFloat = 0 63 | other.getRed(&rightRed, green: &rightGreen, blue: &rightBlue, alpha: &rightAlpha) 64 | 65 | return NSColor(calibratedRed: leftRed * factor + rightRed * inverseFactor, 66 | green: leftGreen * factor + rightGreen * inverseFactor, 67 | blue: leftBlue * factor + rightBlue * inverseFactor, 68 | alpha: leftAlpha * factor + rightAlpha * inverseFactor) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Examples/ColorParserExample/Tests/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - force_cast 3 | - force_try 4 | - type_name 5 | - file_header 6 | - explicit_top_level_acl -------------------------------------------------------------------------------- /Examples/ColorParserExample/Tests/ColorParserExampleTests/ColorParserExampleTests.swift: -------------------------------------------------------------------------------- 1 | @testable import ColorParserExample 2 | import Eval 3 | import XCTest 4 | 5 | class ColorParserExampleTests: XCTestCase { 6 | let colorParser: ColorParser = ColorParser() 7 | 8 | func testExample() { 9 | XCTAssertEqual(colorParser.evaluate("#00ff00") as! NSColor, NSColor(calibratedRed: 0, green: 1, blue: 0, alpha: 1)) 10 | XCTAssertEqual(colorParser.evaluate("red") as! NSColor, .red) 11 | XCTAssertEqual(colorParser.evaluate("#ff0000 mixed with #0000ff") as! NSColor, NSColor(calibratedRed: 0.5, green: 0, blue: 0.5, alpha: 1)) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Examples/ColorParserExample/Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | @testable import TemplateExampleTests 2 | import XCTest 3 | 4 | XCTMain([ 5 | testCase(TemplateExampleTests.allTests) 6 | ]) 7 | -------------------------------------------------------------------------------- /Examples/TemplateExample/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /Examples/TemplateExample/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.2 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "TemplateExample", 7 | products: [ 8 | .library( 9 | name: "TemplateExample", 10 | targets: ["TemplateExample"]) 11 | ], 12 | dependencies: [ 13 | .package(url: "../../", from: "1.4.0") 14 | ], 15 | targets: [ 16 | .target( 17 | name: "TemplateExample", 18 | dependencies: ["Eval"]), 19 | .testTarget( 20 | name: "TemplateExampleTests", 21 | dependencies: ["TemplateExample"]) 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /Examples/TemplateExample/README.md: -------------------------------------------------------------------------------- 1 | # TemplateExample 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Examples/TemplateExample/Tests/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - force_cast 3 | - force_try 4 | - force_unwrapping 5 | - type_name 6 | - file_header 7 | - explicit_top_level_acl -------------------------------------------------------------------------------- /Examples/TemplateExample/Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | @testable import TemplateExampleTests 2 | import XCTest 3 | 4 | XCTMain([ 5 | testCase(TemplateExampleTests.allTests) 6 | ]) 7 | -------------------------------------------------------------------------------- /Examples/TemplateExample/Tests/TemplateExampleTests/TemplateExampleComponentTests.swift: -------------------------------------------------------------------------------- 1 | import Eval 2 | @testable import TemplateExample 3 | import XCTest 4 | 5 | class TemplateExampleComponentTests: XCTestCase { 6 | let interpreter: TemplateLanguage = TemplateLanguage() 7 | 8 | func testComplexExample() { 9 | XCTAssertEqual(eval( 10 | """ 11 | {% if greet %}Hello{% else %}Bye{% endif %} {{ name }}! 12 | {% set works = true %} 13 | {% for i in [3,2,1] %}{{ i }}, {% endfor %}go! 14 | 15 | This template engine {% if !works %}does not {% endif %}work{% if works %}s{% endif %}! 16 | """, ["greet": true, "name": "Laszlo"]), 17 | """ 18 | Hello Laszlo! 19 | 20 | 3, 2, 1, go! 21 | 22 | This template engine works! 23 | """) 24 | } 25 | 26 | // MARK: Helpers 27 | 28 | func eval(_ template: String, _ variables: [String: Any] = [:]) -> String { 29 | let context = Context(variables: variables) 30 | let result = interpreter.evaluate(template, context: context) 31 | if !context.debugInfo.isEmpty { 32 | print(context.debugInfo) 33 | } 34 | return result 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Examples/TemplateExample/Tests/TemplateExampleTests/import.txt: -------------------------------------------------------------------------------- 1 | {% import 'template.txt' %} 2 | Bye! -------------------------------------------------------------------------------- /Examples/TemplateExample/Tests/TemplateExampleTests/template.txt: -------------------------------------------------------------------------------- 1 | Hello {{name}}! -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'jazzy', '>=0.9' 4 | 5 | gem 'xcpretty' 6 | gem 'xcpretty-json-formatter' 7 | 8 | gem 'cocoapods' 9 | 10 | gem 'danger' 11 | gem 'danger-auto_label' 12 | gem 'danger-commit_lint' 13 | gem 'danger-mention' 14 | gem 'danger-pronto' 15 | gem 'danger-prose' 16 | gem 'danger-shellcheck' 17 | # gem 'danger-slather' 18 | gem 'danger-swiftlint' 19 | gem 'danger-tailor' 20 | gem 'danger-welcome_message' 21 | gem 'danger-xcode_summary' 22 | gem 'danger-xcodebuild' 23 | gem 'danger-xcov' -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.2) 5 | activesupport (4.2.11.1) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | addressable (2.7.0) 11 | public_suffix (>= 2.0.2, < 5.0) 12 | algoliasearch (1.27.1) 13 | httpclient (~> 2.8, >= 2.8.3) 14 | json (>= 1.5.1) 15 | atomos (0.1.3) 16 | babosa (1.0.3) 17 | claide (1.0.3) 18 | claide-plugins (0.9.2) 19 | cork 20 | nap 21 | open4 (~> 1.3) 22 | cocoapods (1.8.4) 23 | activesupport (>= 4.0.2, < 5) 24 | claide (>= 1.0.2, < 2.0) 25 | cocoapods-core (= 1.8.4) 26 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 27 | cocoapods-downloader (>= 1.2.2, < 2.0) 28 | cocoapods-plugins (>= 1.0.0, < 2.0) 29 | cocoapods-search (>= 1.0.0, < 2.0) 30 | cocoapods-stats (>= 1.0.0, < 2.0) 31 | cocoapods-trunk (>= 1.4.0, < 2.0) 32 | cocoapods-try (>= 1.1.0, < 2.0) 33 | colored2 (~> 3.1) 34 | escape (~> 0.0.4) 35 | fourflusher (>= 2.3.0, < 3.0) 36 | gh_inspector (~> 1.0) 37 | molinillo (~> 0.6.6) 38 | nap (~> 1.0) 39 | ruby-macho (~> 1.4) 40 | xcodeproj (>= 1.11.1, < 2.0) 41 | cocoapods-core (1.8.4) 42 | activesupport (>= 4.0.2, < 6) 43 | algoliasearch (~> 1.0) 44 | concurrent-ruby (~> 1.1) 45 | fuzzy_match (~> 2.0.4) 46 | nap (~> 1.0) 47 | cocoapods-deintegrate (1.0.4) 48 | cocoapods-downloader (1.3.0) 49 | cocoapods-plugins (1.0.0) 50 | nap 51 | cocoapods-search (1.0.0) 52 | cocoapods-stats (1.1.0) 53 | cocoapods-trunk (1.4.1) 54 | nap (>= 0.8, < 2.0) 55 | netrc (~> 0.11) 56 | cocoapods-try (1.1.0) 57 | colored (1.2) 58 | colored2 (3.1.2) 59 | commander-fastlane (4.4.6) 60 | highline (~> 1.7.2) 61 | concurrent-ruby (1.1.5) 62 | cork (0.3.0) 63 | colored2 (~> 3.1) 64 | danger (6.1.0) 65 | claide (~> 1.0) 66 | claide-plugins (>= 0.9.2) 67 | colored2 (~> 3.1) 68 | cork (~> 0.1) 69 | faraday (~> 0.9) 70 | faraday-http-cache (~> 2.0) 71 | git (~> 1.5) 72 | kramdown (~> 2.0) 73 | kramdown-parser-gfm (~> 1.0) 74 | no_proxy_fix 75 | octokit (~> 4.7) 76 | terminal-table (~> 1) 77 | danger-auto_label (1.3.1) 78 | danger-plugin-api (~> 1.0) 79 | danger-commit_lint (0.0.7) 80 | danger-plugin-api (~> 1.0) 81 | danger-mention (0.6.1) 82 | danger (>= 2.0.0) 83 | danger-plugin-api (1.0.0) 84 | danger (> 2.0) 85 | danger-pronto (0.3.2) 86 | danger-plugin-api (~> 1.0) 87 | danger-prose (2.0.7) 88 | danger 89 | danger-shellcheck (1.0.0) 90 | danger-plugin-api (~> 1.0) 91 | danger-swiftlint (0.24.0) 92 | danger 93 | rake (> 10) 94 | thor (~> 0.19) 95 | danger-tailor (1.0.0) 96 | danger-plugin-api (~> 1.0) 97 | danger-welcome_message (0.0.1) 98 | danger-plugin-api (~> 1.0) 99 | danger-xcode_summary (0.5.1) 100 | danger-plugin-api (~> 1.0) 101 | danger-xcodebuild (0.0.6) 102 | danger-plugin-api (~> 1.0) 103 | danger-xcov (0.4.1) 104 | danger (>= 2.1) 105 | xcov (>= 1.1.2) 106 | declarative (0.0.10) 107 | declarative-option (0.1.0) 108 | digest-crc (0.4.1) 109 | domain_name (0.5.20190701) 110 | unf (>= 0.0.5, < 1.0.0) 111 | dotenv (2.7.5) 112 | emoji_regex (1.0.1) 113 | escape (0.0.4) 114 | excon (0.71.1) 115 | faraday (0.17.3) 116 | multipart-post (>= 1.2, < 3) 117 | faraday-cookie_jar (0.0.6) 118 | faraday (>= 0.7.4) 119 | http-cookie (~> 1.0.0) 120 | faraday-http-cache (2.0.0) 121 | faraday (~> 0.8) 122 | faraday_middleware (0.13.1) 123 | faraday (>= 0.7.4, < 1.0) 124 | fastimage (2.1.7) 125 | fastlane (2.140.0) 126 | CFPropertyList (>= 2.3, < 4.0.0) 127 | addressable (>= 2.3, < 3.0.0) 128 | babosa (>= 1.0.2, < 2.0.0) 129 | bundler (>= 1.12.0, < 3.0.0) 130 | colored 131 | commander-fastlane (>= 4.4.6, < 5.0.0) 132 | dotenv (>= 2.1.1, < 3.0.0) 133 | emoji_regex (>= 0.1, < 2.0) 134 | excon (>= 0.71.0, < 1.0.0) 135 | faraday (~> 0.17) 136 | faraday-cookie_jar (~> 0.0.6) 137 | faraday_middleware (~> 0.13.1) 138 | fastimage (>= 2.1.0, < 3.0.0) 139 | gh_inspector (>= 1.1.2, < 2.0.0) 140 | google-api-client (>= 0.29.2, < 0.37.0) 141 | google-cloud-storage (>= 1.15.0, < 2.0.0) 142 | highline (>= 1.7.2, < 2.0.0) 143 | json (< 3.0.0) 144 | jwt (~> 2.1.0) 145 | mini_magick (>= 4.9.4, < 5.0.0) 146 | multi_xml (~> 0.5) 147 | multipart-post (~> 2.0.0) 148 | plist (>= 3.1.0, < 4.0.0) 149 | public_suffix (~> 2.0.0) 150 | rubyzip (>= 1.3.0, < 2.0.0) 151 | security (= 0.1.3) 152 | simctl (~> 1.6.3) 153 | slack-notifier (>= 2.0.0, < 3.0.0) 154 | terminal-notifier (>= 2.0.0, < 3.0.0) 155 | terminal-table (>= 1.4.5, < 2.0.0) 156 | tty-screen (>= 0.6.3, < 1.0.0) 157 | tty-spinner (>= 0.8.0, < 1.0.0) 158 | word_wrap (~> 1.0.0) 159 | xcodeproj (>= 1.13.0, < 2.0.0) 160 | xcpretty (~> 0.3.0) 161 | xcpretty-travis-formatter (>= 0.0.3) 162 | ffi (1.12.1) 163 | fourflusher (2.3.1) 164 | fuzzy_match (2.0.4) 165 | gh_inspector (1.1.3) 166 | git (1.5.0) 167 | google-api-client (0.36.4) 168 | addressable (~> 2.5, >= 2.5.1) 169 | googleauth (~> 0.9) 170 | httpclient (>= 2.8.1, < 3.0) 171 | mini_mime (~> 1.0) 172 | representable (~> 3.0) 173 | retriable (>= 2.0, < 4.0) 174 | signet (~> 0.12) 175 | google-cloud-core (1.5.0) 176 | google-cloud-env (~> 1.0) 177 | google-cloud-errors (~> 1.0) 178 | google-cloud-env (1.3.0) 179 | faraday (~> 0.11) 180 | google-cloud-errors (1.0.0) 181 | google-cloud-storage (1.25.1) 182 | addressable (~> 2.5) 183 | digest-crc (~> 0.4) 184 | google-api-client (~> 0.33) 185 | google-cloud-core (~> 1.2) 186 | googleauth (~> 0.9) 187 | mini_mime (~> 1.0) 188 | googleauth (0.10.0) 189 | faraday (~> 0.12) 190 | jwt (>= 1.4, < 3.0) 191 | memoist (~> 0.16) 192 | multi_json (~> 1.11) 193 | os (>= 0.9, < 2.0) 194 | signet (~> 0.12) 195 | highline (1.7.10) 196 | http-cookie (1.0.3) 197 | domain_name (~> 0.5) 198 | httpclient (2.8.3) 199 | i18n (0.9.5) 200 | concurrent-ruby (~> 1.0) 201 | jazzy (0.13.1) 202 | cocoapods (~> 1.5) 203 | mustache (~> 1.1) 204 | open4 205 | redcarpet (~> 3.4) 206 | rouge (>= 2.0.6, < 4.0) 207 | sassc (~> 2.1) 208 | sqlite3 (~> 1.3) 209 | xcinvoke (~> 0.3.0) 210 | json (2.3.0) 211 | jwt (2.1.0) 212 | kramdown (2.1.0) 213 | kramdown-parser-gfm (1.1.0) 214 | kramdown (~> 2.0) 215 | liferaft (0.0.6) 216 | memoist (0.16.2) 217 | mini_magick (4.10.1) 218 | mini_mime (1.0.2) 219 | minitest (5.14.0) 220 | molinillo (0.6.6) 221 | multi_json (1.14.1) 222 | multi_xml (0.6.0) 223 | multipart-post (2.0.0) 224 | mustache (1.1.1) 225 | nanaimo (0.2.6) 226 | nap (1.1.0) 227 | naturally (2.2.0) 228 | netrc (0.11.0) 229 | no_proxy_fix (0.1.2) 230 | octokit (4.15.0) 231 | faraday (>= 0.9) 232 | sawyer (~> 0.8.0, >= 0.5.3) 233 | open4 (1.3.4) 234 | os (1.0.1) 235 | plist (3.5.0) 236 | public_suffix (2.0.5) 237 | rake (13.0.1) 238 | redcarpet (3.5.0) 239 | representable (3.0.4) 240 | declarative (< 0.1.0) 241 | declarative-option (< 0.2.0) 242 | uber (< 0.2.0) 243 | retriable (3.1.2) 244 | rouge (2.0.7) 245 | ruby-macho (1.4.0) 246 | rubyzip (1.3.0) 247 | sassc (2.2.1) 248 | ffi (~> 1.9) 249 | sawyer (0.8.2) 250 | addressable (>= 2.3.5) 251 | faraday (> 0.8, < 2.0) 252 | security (0.1.3) 253 | signet (0.12.0) 254 | addressable (~> 2.3) 255 | faraday (~> 0.9) 256 | jwt (>= 1.5, < 3.0) 257 | multi_json (~> 1.10) 258 | simctl (1.6.7) 259 | CFPropertyList 260 | naturally 261 | slack-notifier (2.3.2) 262 | sqlite3 (1.4.2) 263 | terminal-notifier (2.0.0) 264 | terminal-table (1.8.0) 265 | unicode-display_width (~> 1.1, >= 1.1.1) 266 | thor (0.20.3) 267 | thread_safe (0.3.6) 268 | tty-cursor (0.7.0) 269 | tty-screen (0.7.0) 270 | tty-spinner (0.9.2) 271 | tty-cursor (~> 0.7) 272 | tzinfo (1.2.6) 273 | thread_safe (~> 0.1) 274 | uber (0.1.0) 275 | unf (0.1.4) 276 | unf_ext 277 | unf_ext (0.0.7.6) 278 | unicode-display_width (1.6.1) 279 | word_wrap (1.0.0) 280 | xcinvoke (0.3.0) 281 | liferaft (~> 0.0.6) 282 | xcodeproj (1.14.0) 283 | CFPropertyList (>= 2.3.3, < 4.0) 284 | atomos (~> 0.1.3) 285 | claide (>= 1.0.2, < 2.0) 286 | colored2 (~> 3.1) 287 | nanaimo (~> 0.2.6) 288 | xcov (1.7.0) 289 | fastlane (>= 2.82.0, < 3.0.0) 290 | multipart-post 291 | slack-notifier 292 | terminal-table 293 | xcodeproj 294 | xcresult (~> 0.2.0) 295 | xcpretty (0.3.0) 296 | rouge (~> 2.0.7) 297 | xcpretty-json-formatter (0.1.1) 298 | xcpretty (~> 0.2, >= 0.0.7) 299 | xcpretty-travis-formatter (1.0.0) 300 | xcpretty (~> 0.2, >= 0.0.7) 301 | xcresult (0.2.0) 302 | 303 | PLATFORMS 304 | ruby 305 | 306 | DEPENDENCIES 307 | cocoapods 308 | danger 309 | danger-auto_label 310 | danger-commit_lint 311 | danger-mention 312 | danger-pronto 313 | danger-prose 314 | danger-shellcheck 315 | danger-swiftlint 316 | danger-tailor 317 | danger-welcome_message 318 | danger-xcode_summary 319 | danger-xcodebuild 320 | danger-xcov 321 | jazzy (>= 0.9) 322 | xcpretty 323 | xcpretty-json-formatter 324 | 325 | BUNDLED WITH 326 | 1.17.2 327 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Eval", 7 | products: [ 8 | .library( 9 | name: "Eval", 10 | targets: ["Eval"]), 11 | ], 12 | dependencies: [ 13 | ], 14 | targets: [ 15 | .target( 16 | name: "Eval", 17 | dependencies: []), 18 | .testTarget( 19 | name: "EvalTests", 20 | dependencies: ["Eval"]), 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /Scripts/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /Scripts/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - force_try 3 | - file_header 4 | - file_length 5 | - explicit_top_level_acl 6 | - function_body_length 7 | -------------------------------------------------------------------------------- /Scripts/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.2 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Automation", 7 | dependencies: [ 8 | .package(url: "https://github.com/xcodeswift/xcproj.git", from: "4.0.0"), 9 | ], 10 | targets: [ 11 | .target( 12 | name: "Automation", 13 | dependencies: ["xcproj"]), 14 | ] 15 | ) 16 | -------------------------------------------------------------------------------- /Scripts/Sources/Automation/Error.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum CIError: Error { 4 | case invalidExitCode(statusCode: Int32, errorOutput: String?) 5 | case timeout 6 | case logicalError(message: String) 7 | } 8 | -------------------------------------------------------------------------------- /Scripts/Sources/Automation/Shell.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class Shell { 4 | static func executeAndPrint(_ command: String, timeout: Double = 10, allowFailure: Bool = false) throws { 5 | print("$ \(command)") 6 | let output = try executeShell(commandPath: "/bin/bash", arguments: ["-c", command], timeout: timeout, allowFailure: allowFailure) { 7 | print($0, separator: "", terminator: "") 8 | } 9 | if let error = output?.error { 10 | print(error) 11 | } 12 | } 13 | 14 | static func execute(_ command: String, timeout: Double = 10, allowFailure: Bool = false) throws -> (output: String?, error: String?)? { 15 | return try executeShell(commandPath: "/bin/bash", arguments: ["-c", command], timeout: timeout, allowFailure: allowFailure) 16 | } 17 | 18 | static func bash(commandName: String, 19 | arguments: [String] = [], 20 | timeout: Double = 10, 21 | allowFailure: Bool = false) throws -> (output: String?, error: String?)? { 22 | guard let execution = try? executeShell(commandPath: "/bin/bash" , 23 | arguments: [ "-l", "-c", "/usr/bin/which \(commandName)" ], 24 | timeout: 1), 25 | var whichPathForCommand = execution?.output else { return nil } 26 | 27 | whichPathForCommand = whichPathForCommand.trimmingCharacters(in: NSCharacterSet.whitespacesAndNewlines) 28 | return try executeShell(commandPath: whichPathForCommand, arguments: arguments, timeout: timeout, allowFailure: allowFailure) 29 | } 30 | 31 | static func executeShell(commandPath: String, 32 | arguments: [String] = [], 33 | timeout: Double = 10, 34 | allowFailure: Bool = false, 35 | stream: @escaping (String) -> Void = { _ in }) throws -> (output: String?, error: String?)? { 36 | let task = Process() 37 | task.launchPath = commandPath 38 | task.arguments = arguments 39 | 40 | let pipeForOutput = Pipe() 41 | task.standardOutput = pipeForOutput 42 | 43 | let pipeForError = Pipe() 44 | task.standardError = pipeForError 45 | task.launch() 46 | 47 | let fileHandle = pipeForOutput.fileHandleForReading 48 | fileHandle.waitForDataInBackgroundAndNotify() 49 | 50 | var outputData = Data() 51 | 52 | func process(data: Data) { 53 | outputData.append(data) 54 | if let output = String(data: data, encoding: .utf8) { 55 | stream(output) 56 | } 57 | } 58 | 59 | let observer = NotificationCenter.default.addObserver(forName: Notification.Name.NSFileHandleDataAvailable, object: fileHandle, queue: nil) { notification in 60 | if let noitificationFileHandle = notification.object as? FileHandle { 61 | process(data: noitificationFileHandle.availableData) 62 | noitificationFileHandle.waitForDataInBackgroundAndNotify() 63 | } 64 | } 65 | 66 | defer { 67 | NotificationCenter.default.removeObserver(observer) 68 | } 69 | 70 | var shouldTimeout = false 71 | DispatchQueue.main.asyncAfter(deadline: .now() + timeout) { 72 | if task.isRunning { 73 | shouldTimeout = true 74 | task.terminate() 75 | } 76 | } 77 | 78 | task.waitUntilExit() 79 | 80 | process(data: fileHandle.readDataToEndOfFile()) 81 | 82 | if shouldTimeout { 83 | throw CIError.timeout 84 | } 85 | 86 | let output = String(data: outputData, encoding: .utf8) 87 | 88 | let errorData = pipeForError.fileHandleForReading.readDataToEndOfFile() 89 | let error = String(data: errorData, encoding: .utf8) 90 | 91 | let exitCode = task.terminationStatus 92 | if exitCode > 0 && !allowFailure { 93 | throw CIError.invalidExitCode(statusCode: exitCode, errorOutput: error) 94 | } 95 | 96 | return (output, error) 97 | } 98 | 99 | static func env(name: String) -> String? { 100 | return ProcessInfo.processInfo.environment[name] 101 | } 102 | 103 | static func args() -> [String] { 104 | return ProcessInfo.processInfo.arguments 105 | } 106 | 107 | static func nextArg(_ arg: String) -> String? { 108 | if let index = Shell.args().index(of: arg), Shell.args().count > index + 1 { 109 | return Shell.args()[index.advanced(by: 1)] 110 | } 111 | return nil 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Scripts/Sources/Automation/Travis.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class TravisCI { 4 | enum JobType: CustomStringConvertible { 5 | case local 6 | case travisAPI 7 | case travisCron 8 | case travisPushOnBranch(branch: String) 9 | case travisPushOnTag(name: String) 10 | case travisPullRequest(branch: String, sha: String, slug: String) 11 | 12 | var description: String { 13 | switch self { 14 | case .local: 15 | return "Local" 16 | case .travisAPI: 17 | return "Travis (API)" 18 | case .travisCron: 19 | return "Travis (Cron job)" 20 | case .travisPushOnBranch(let branch): 21 | return "Travis (Push on branch '\(branch)')" 22 | case .travisPushOnTag(let name): 23 | return "Travis (Push of tag '\(name)')" 24 | case .travisPullRequest(let branch): 25 | return "Travis (Pull Request on branch '\(branch)')" 26 | } 27 | } 28 | } 29 | 30 | static func isPullRquestJob() -> Bool { 31 | return Shell.env(name: "TRAVIS_EVENT_TYPE") == "pull_request" 32 | } 33 | 34 | static func isRunningLocally() -> Bool { 35 | return Shell.env(name: "TRAVIS") != "true" 36 | } 37 | 38 | static func isCIJob() -> Bool { 39 | return !isRunningLocally() && !isPullRquestJob() 40 | } 41 | 42 | static func jobType() -> JobType { 43 | if isRunningLocally() { 44 | return .local 45 | } else if isPullRquestJob() { 46 | return .travisPullRequest(branch: Shell.env(name: "TRAVIS_PULL_REQUEST_BRANCH") ?? "", 47 | sha: Shell.env(name: "TRAVIS_PULL_REQUEST_SHA") ?? "", 48 | slug: Shell.env(name: "TRAVIS_PULL_REQUEST_SLUG") ?? "") 49 | } else if Shell.env(name: "TRAVIS_EVENT_TYPE") == "cron" { 50 | return .travisCron 51 | } else if Shell.env(name: "TRAVIS_EVENT_TYPE") == "api" { 52 | return .travisAPI 53 | } else if let tag = Shell.env(name: "TRAVIS_TAG"), !tag.isEmpty { 54 | return .travisPushOnTag(name: tag) 55 | } else if let branch = Shell.env(name: "TRAVIS_BRANCH"), !branch.isEmpty { 56 | return .travisPushOnBranch(branch: branch) 57 | } else { 58 | fatalError("Cannot identify job type") 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Scripts/Sources/Automation/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | Eval.main() 4 | -------------------------------------------------------------------------------- /Scripts/ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "🤖 Assembling automation process" 4 | 5 | root=`git rev-parse --show-toplevel` 6 | cd "$root/Scripts" 7 | swift build 8 | 9 | echo "🏃 Running automation process" 10 | 11 | output=`swift build --show-bin-path` 12 | cd "$root" 13 | "$output/automation" 14 | -------------------------------------------------------------------------------- /Scripts/git_auth.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | openssl aes-256-cbc -K $encrypted_f50468713ad3_key -iv $encrypted_f50468713ad3_iv -in github_rsa.enc -out github_rsa -d 4 | chmod 600 github_rsa 5 | ssh-add github_rsa 6 | ssh -o StrictHostKeyChecking=no git@github.com || true 7 | git config --global user.email tevelee@gmail.com 8 | git config --global user.name 'Travis CI' 9 | -------------------------------------------------------------------------------- /Sources/Eval/Common.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Laszlo Teveli. 3 | * 4 | * Licensed to the Apache Software Foundation (ASF) under one 5 | * or more contributor license agreements. See the NOTICE file 6 | * distributed with this work for additional information 7 | * regarding copyright ownership. The ASF licenses this file 8 | * to you under the Apache License, Version 2.0 (the 9 | * "License"); you may not use this file except in compliance 10 | * with the License. You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, 15 | * software distributed under the License is distributed on an 16 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 | * KIND, either express or implied. See the License for the 18 | * specific language governing permissions and limitations 19 | * under the License. 20 | */ 21 | 22 | import Foundation 23 | 24 | /// A protocol which is capable of evaluating string expressions to a strongly typed object 25 | public protocol Evaluator { 26 | /// The type of the evaluation result 27 | associatedtype EvaluatedType 28 | 29 | /// The only method in `Evaluator` protocol which does the evaluation of a string expression, and returns a strongly typed object 30 | /// - parameter expression: The input 31 | /// - returns: The evaluated value 32 | func evaluate(_ expression: String) -> EvaluatedType 33 | } 34 | 35 | /// A special kind of evaluator which uses an `InterpreterContext` instance to evaluate expressions 36 | /// The context contains variables which can be used during the evaluation 37 | public protocol EvaluatorWithLocalContext: Evaluator { 38 | /// Evaluates the provided string expression with the help of the context parameter, and returns a strongly typed object 39 | /// - parameter expression: The input 40 | /// - parameter context: The local context if there is something expression specific needs to be provided 41 | /// - returns: The evaluated value 42 | func evaluate(_ expression: String, context: Context) -> EvaluatedType 43 | } 44 | 45 | /// The base protocol of interpreters, that are context-aware, and capable of recursively evaluating variables. They use the evaluate method as their main input 46 | public protocol Interpreter: EvaluatorWithLocalContext { 47 | /// The evaluator type to use when interpreting variables 48 | associatedtype VariableEvaluator: EvaluatorWithLocalContext 49 | 50 | /// The stored context object for helping evaluation and providing persistency 51 | var context: Context { get } 52 | 53 | /// 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. 54 | var interpreterForEvaluatingVariables: VariableEvaluator { get } 55 | } 56 | 57 | /// A protocol which is able to express custom values as Strings 58 | public protocol Printer { 59 | /// Converts its input parameter to a String value 60 | /// - parameter input: The value to print 61 | /// - returns: The converted String instance 62 | func print(_ input: Any) -> String 63 | } 64 | 65 | /// Detailed information about recognised expressions 66 | public struct ExpressionInfo { 67 | /// The raw String input of the expression 68 | var input: String 69 | /// The generated output of the expression 70 | var output: Any 71 | /// A stringified version of the elements of the `Matcher` object 72 | var pattern: String 73 | /// The name of the pattern 74 | var patternName: String 75 | /// All the variables computed during the evaluation 76 | var variables: [String: Any] 77 | } 78 | 79 | /// 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. 80 | public class Context { 81 | /// The stored variables 82 | public var variables: [String: Any] 83 | 84 | /// Debug information for recognised patterns 85 | public var debugInfo: [String: ExpressionInfo] = [:] 86 | 87 | /// 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. 88 | /// When `pop` is called, it restores the last snapshot, destorying all the changes that happened after the last snapshot. 89 | /// Useful for temporal variables! 90 | var stack : [(variables: [String: Any], debugInfo: [String: ExpressionInfo])] = [] 91 | 92 | /// Users of the context may optionally provide an initial set of variables 93 | /// - parameter variables: Variable names and values 94 | public init(variables: [String: Any] = [:]) { 95 | self.variables = variables 96 | } 97 | 98 | /// 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. 99 | /// When `pop` is called, it restores the last snapshot, destorying all the changes that happened after the last snapshot. 100 | /// Useful for temporal variables! It should be called before setting the temporal variables 101 | public func push() { 102 | stack.append((variables: variables, debugInfo: debugInfo)) 103 | } 104 | 105 | /// 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. 106 | /// When `pop` is called, it restores the last snapshot, destorying all the changes that happened after the last snapshot. 107 | /// Useful for temporal variables! It should be called when the temporal variables are not needed anymore 108 | public func pop() { 109 | if let last = stack.popLast() { 110 | variables = last.variables 111 | debugInfo = last.debugInfo 112 | } 113 | } 114 | 115 | /// Creates a new context instance by merging their variable dictionaries. The one in the parameter overrides the duplicated items of the existing one 116 | /// - parameter with: The other context to merge with 117 | /// - returns: A new `InterpreterContext` instance with the current and the parameter variables merged inside 118 | public func merging(with other: Context?) -> Context { 119 | if let other = other { 120 | return Context(variables: other.variables.merging(self.variables) { eixstingValue, _ in eixstingValue }) 121 | } else { 122 | return self 123 | } 124 | } 125 | 126 | /// 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 127 | /// - parameter with: The other context to merge with 128 | /// - parameter existing: During the merge the parameter on the existing dictionary (same terminolody with Dictionary.merge) 129 | /// - parameter new: During the merge the parameter on the merged dictionary (same terminolody with Dictionary.merge) 130 | /// - returns: The same `InterpreterContext` instance after merging the variables dictionary with the variables in the context given as parameter 131 | public func merge(with other: Context?, merge: (_ existing: Any, _ new: Any) throws -> Any) { 132 | if let other = other { 133 | try? variables.merge(other.variables, uniquingKeysWith: merge) 134 | } 135 | } 136 | } 137 | 138 | /// This is where the `Matcher` is able to determine the `MatchResult` for a given input inside the provided substring range 139 | /// - parameter amongst: All the `Matcher` instances to evaluate, in priority order 140 | /// - parameter in: The input 141 | /// - parameter from: The start of the checked range 142 | /// - parameter interpreter: An interpreter instance - if variables need any further evaluation 143 | /// - parameter context: The context - if variables need any contextual information 144 | /// - parameter connectedRanges: Ranges of string indices that are connected with opening-closing tag pairs, respectively 145 | /// - returns: The result of the match operation 146 | internal func matchStatement(amongst statements: [Pattern], in input: String, from start: String.Index? = nil, interpreter: E, context: Context, connectedRanges: [ClosedRange] = []) -> MatchResult { 147 | let results = statements.lazy.map { statement -> (element: Pattern, result: MatchResult) in 148 | let result = statement.matches(string: input, from: start, interpreter: interpreter, context: context, connectedRanges: connectedRanges) 149 | return (element: statement, result: result) 150 | } 151 | if let matchingElement = results.first(where: { $0.result.isMatch() }) { 152 | return matchingElement.result 153 | } else if results.contains(where: { $0.result.isPossibleMatch() }) { 154 | return .possibleMatch 155 | } 156 | return .noMatch 157 | } 158 | 159 | /// Independent helper function that determines the pairs of opening and closing keywords 160 | /// - parameter input: The input string to search ranges in 161 | /// - parameter statements: Patterns that contain the opening and closing keyword types that should be matched 162 | /// - returns: The ranges of opening-closing pairs, keeping logical hierarchy 163 | internal func collectConnectedRanges(input: String, statements: [Pattern]) -> [ClosedRange] { 164 | return statements.compactMap { pattern -> [ClosedRange] in 165 | let keywords = pattern.elements.compactMap { $0 as? Keyword } 166 | let openingKeywords = keywords.filter { $0.type == .openingStatement } 167 | let closingKeywords = keywords.filter { $0.type == .closingStatement } 168 | 169 | guard !openingKeywords.isEmpty && !closingKeywords.isEmpty else { return [] } 170 | 171 | var ranges: [ClosedRange] = [] 172 | var rangeStart: [String.Index] = [] 173 | var position = input.startIndex 174 | repeat { 175 | let relevantInput = input[position...] 176 | let start = openingKeywords 177 | .first { relevantInput.contains($0.name) } 178 | .flatMap { relevantInput.range(of: $0.name)?.lowerBound } 179 | let end = closingKeywords 180 | .first { relevantInput.contains($0.name) } 181 | .flatMap { relevantInput.range(of: $0.name)?.lowerBound } 182 | if let start = start, let end = end { 183 | if start < end { 184 | rangeStart.append(start) 185 | } else { 186 | let lastOpening = rangeStart.removeLast() 187 | ranges.append(lastOpening...end) 188 | } 189 | position = input.index(after: min(start, end)) 190 | } else if let start = start { 191 | rangeStart.append(start) 192 | position = input.index(after: start) 193 | } else if let end = end { 194 | if rangeStart.isEmpty { 195 | return [] 196 | } 197 | let lastOpening = rangeStart.removeLast() 198 | ranges.append(lastOpening...end) 199 | position = input.index(after: end) 200 | } else { 201 | break 202 | } 203 | } while position < input.endIndex 204 | 205 | return ranges 206 | }.reduce([], +) 207 | } 208 | -------------------------------------------------------------------------------- /Sources/Eval/TemplateInterpreter.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Laszlo Teveli. 3 | * 4 | * Licensed to the Apache Software Foundation (ASF) under one 5 | * or more contributor license agreements. See the NOTICE file 6 | * distributed with this work for additional information 7 | * regarding copyright ownership. The ASF licenses this file 8 | * to you under the Apache License, Version 2.0 (the 9 | * "License"); you may not use this file except in compliance 10 | * with the License. You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, 15 | * software distributed under the License is distributed on an 16 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 | * KIND, either express or implied. See the License for the 18 | * specific language governing permissions and limitations 19 | * under the License. 20 | */ 21 | 22 | import Foundation 23 | 24 | /// This interpreter is used to evaluate string expressions and return a transformed string, replacing the content where it matches certain patterns. 25 | /// 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. 26 | open class TemplateInterpreter: Interpreter { 27 | /// The statements (patterns) registered to the interpreter. If found, these are going to be processed and replaced with the evaluated value 28 | public let statements: [Pattern>] 29 | 30 | /// The context used when evaluating the expressions. These context variables are global, used in every evaluation processed with this instance. 31 | public let context: Context 32 | 33 | /// The `StringTemplateInterpreter` contains a `TypedInterpreter`, as it is quite common practice to evaluate strongly typed expression as s support for the template language. 34 | /// Common examples are: condition part of an if statement, or body of a print statement 35 | public let typedInterpreter: TypedInterpreter 36 | 37 | /// The evaluator type that is being used to process variables. By default, the TypedInterpreter is being used 38 | public typealias VariableEvaluator = TypedInterpreter 39 | 40 | /// The result type of a template evaluation 41 | public typealias EvaluatedType = T 42 | 43 | /// The evaluator, that is being used to process variables 44 | public lazy var interpreterForEvaluatingVariables: TypedInterpreter = { typedInterpreter }() 45 | 46 | /// The statements, and context parameters are optional, but highly recommended to use with actual values. 47 | /// In order to properly initialise a `StringTemplateInterpreter`, you'll need a `TypedInterpreter` instance as well. 48 | /// - parameter statements: The patterns that the interpreter should recognise 49 | /// - parameter interpreter: A `TypedInterpreter` instance to evaluate typed expressions appearing in the template 50 | /// - parameter context: Global context that is going to be used with every expression evaluated with the current instance. Defaults to empty context 51 | public init(statements: [Pattern>] = [], 52 | interpreter: TypedInterpreter = TypedInterpreter(), 53 | context: Context = Context()) { 54 | self.statements = statements 55 | self.typedInterpreter = interpreter 56 | self.context = context 57 | } 58 | 59 | /// The main part of the evaluation happens here. In this case, only the global context variables are going to be used 60 | /// - parameter expression: The input 61 | /// - returns: The output of the evaluation 62 | public func evaluate(_ expression: String) -> T { 63 | return evaluate(expression, context: Context()) 64 | } 65 | 66 | /// 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. 67 | /// - parameter expression: The input 68 | /// - parameter context: Local context that is going to be used with this expression only 69 | /// - returns: The output of the evaluation 70 | open func evaluate(_ expression: String, context: Context) -> T { 71 | fatalError("Shouldn't instantiate `TemplateInterpreter` directly. Please subclass with a dedicated type instead") 72 | } 73 | 74 | /// 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 75 | /// - parameter existing: The previously computed value. In case the current iteration is the first, it's the inital value. 76 | /// - parameter next: The value of the current element in the iteration 77 | /// - returns: The a combined value based on the previous and the new value 78 | public typealias Reducer = (_ existing: T, _ next: K) -> T 79 | 80 | /// In order to support generic types, not just plain String objects, a reducer helps to convert the output to the dedicated output type 81 | /// - parameter initialValue: based on the type, an initial value must to be provided which can serve as a base of the output 82 | /// - 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 83 | /// - 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 84 | public typealias TemplateReducer = (initialValue: T, reduceValue: Reducer, reduceCharacter: Reducer) 85 | 86 | /// 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. 87 | /// - parameter expression: The input 88 | /// - parameter context: Local context that is going to be used with this expression only 89 | /// - 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 90 | /// - returns: The output of the evaluation 91 | public func evaluate(_ expression: String, context: Context = Context(), reducer: TemplateReducer) -> T { 92 | context.merge(with: self.context) { existing, _ in existing } 93 | var output = reducer.initialValue 94 | 95 | var position = expression.startIndex 96 | repeat { 97 | let result = matchStatement(amongst: statements, in: expression, from: position, interpreter: self, context: context) 98 | switch result { 99 | case .noMatch, .possibleMatch: 100 | output = reducer.reduceCharacter(output, expression[position]) 101 | position = expression.index(after: position) 102 | case let .exactMatch(length, matchOutput, _): 103 | output = reducer.reduceValue(output, matchOutput) 104 | position = expression.index(position, offsetBy: length) 105 | default: 106 | assertionFailure("Invalid result") 107 | } 108 | } while position < expression.endIndex 109 | 110 | return output 111 | } 112 | } 113 | 114 | /// This interpreter is used to evaluate string expressions and return a transformed string, replacing the content where it matches certain patterns. 115 | /// 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. 116 | public class StringTemplateInterpreter: TemplateInterpreter { 117 | /// The result of a template evaluation is a String 118 | public typealias EvaluatedType = String 119 | 120 | /// 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. 121 | /// - parameter expression: The input 122 | /// - parameter context: Local context that is going to be used with this expression only 123 | /// - returns: The output of the evaluation 124 | public override func evaluate(_ expression: String, context: Context) -> String { 125 | guard !expression.isEmpty else { return "" } 126 | return evaluate(expression, context: context, reducer: (initialValue: "", 127 | reduceValue: { existing, next in existing + next }, 128 | reduceCharacter: { existing, next in existing + String(next) })) 129 | } 130 | } 131 | 132 | /// 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. 133 | /// It's perfect for expressions, that have a body, that needs to be further interpreted, such as an if or while statement. 134 | public class TemplateVariable: GenericVariable { 135 | /// No changes compared to the initialiser of the superclass `Variable`, uses the same parameters 136 | /// - parameter name: `GenericVariable`s have a name (unique identifier), that is used when matching and returning them in the matcher. 137 | /// - parameter options: Options that modify the behaviour of the variable matching, and the output that the framework provides 138 | /// - parameter map: If provided, then the result of the evaluated variable will be running through this map function 139 | /// Whether the processed variable sould be trimmed (removing whitespaces from both sides). Defaults to `true` 140 | public override init(_ name: String, options: VariableOptions = [], map: @escaping VariableMapper = { $0.value as? String }) { 141 | super.init(name, options: options.union(.notInterpreted)) { 142 | guard let stringValue = $0.value as? String else { return "" } 143 | let result = options.interpreted ? $0.interpreter.evaluate(stringValue) : stringValue 144 | return map(VariableBody(value: result, interpreter: $0.interpreter)) 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Sources/Eval/Utilities/MatchResult.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Laszlo Teveli. 3 | * 4 | * Licensed to the Apache Software Foundation (ASF) under one 5 | * or more contributor license agreements. See the NOTICE file 6 | * distributed with this work for additional information 7 | * regarding copyright ownership. The ASF licenses this file 8 | * to you under the Apache License, Version 2.0 (the 9 | * "License"); you may not use this file except in compliance 10 | * with the License. You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, 15 | * software distributed under the License is distributed on an 16 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 | * KIND, either express or implied. See the License for the 18 | * specific language governing permissions and limitations 19 | * under the License. 20 | */ 21 | 22 | import Foundation 23 | 24 | /// Whenever a match operation is performed, the result is going to be a `MatchResult` instance. 25 | public enum MatchResult { 26 | /// The input could not be matched 27 | case noMatch 28 | /// The input can match, if it were continued. (It's the prefix of the matching expression) 29 | case possibleMatch 30 | /// 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. 31 | /// - parameter length: The length of the match in the input string 32 | /// - parameter output: The interpreted content 33 | /// - parameter variables: The key-value pairs of the found `Variable` instances along the way 34 | case exactMatch(length: Int, output: T, variables: [String: Any]) 35 | /// In case the matching sequence only consists of one variable, the result is going to be anyMatch 36 | /// - parameter exhaustive: Whether the matching should be exaustive or just return the shortest matching result 37 | case anyMatch(exhaustive: Bool) 38 | 39 | /// Shorter syntax for pattern matching `MatchResult.exactMatch` 40 | /// - returns: Whether the case of the current instance is `exactMatch` 41 | func isMatch() -> Bool { 42 | if case .exactMatch(_, _, _) = self { 43 | return true 44 | } 45 | return false 46 | } 47 | 48 | /// Shorter syntax for pattern matching `MatchResult.anyMatch` 49 | /// - parameter exhaustive: If the result is `anyMatch`, this one filter the content by its exhaustive parameter - if provided. Uses `false` otherwise 50 | /// - returns: Whether the case of the current instance is `anyMatch` 51 | func isAnyMatch(exhaustive: Bool = false) -> Bool { 52 | if case .anyMatch(let parameter) = self { 53 | return exhaustive == parameter 54 | } 55 | return false 56 | } 57 | 58 | /// Shorter syntax for pattern matching `MatchResult.noMatch` 59 | /// - returns: Whether the case of the current instance is `noMatch` 60 | func isNoMatch() -> Bool { 61 | if case .noMatch = self { 62 | return true 63 | } 64 | return false 65 | } 66 | 67 | /// Shorter syntax for pattern matching `MatchResult.anypossibleMatch` 68 | /// - returns: Whether the case of the current instance is `possibleMatch` 69 | func isPossibleMatch() -> Bool { 70 | if case .possibleMatch = self { 71 | return true 72 | } 73 | return false 74 | } 75 | } 76 | 77 | /// `MatchResult` with Equatable objects are also Equatable 78 | public extension MatchResult where T: Equatable { 79 | /// `MatchResult` with Equatable objects are also Equatable 80 | /// - parameter lhs: Left hand side 81 | /// - parameter rhs: Right hand side 82 | /// - returns: Whether the `MatchResult` have the same values, including the contents of their associated objects 83 | static func == (lhs: MatchResult, rhs: MatchResult) -> Bool { 84 | switch (lhs, rhs) { 85 | case (.noMatch, .noMatch), (.possibleMatch, .possibleMatch): 86 | return true 87 | case let (.anyMatch(lhsShortest), .anyMatch(rhsShortest)): 88 | return lhsShortest == rhsShortest 89 | case let (.exactMatch(lhsLength, lhsOutput, lhsVariables), .exactMatch(rhsLength, rhsOutput, rhsVariables)): 90 | return lhsLength == rhsLength && lhsOutput == rhsOutput && (lhsVariables as NSDictionary).isEqual(to: rhsVariables) 91 | default: 92 | return false 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/Eval/Utilities/Pattern.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Laszlo Teveli. 3 | * 4 | * Licensed to the Apache Software Foundation (ASF) under one 5 | * or more contributor license agreements. See the NOTICE file 6 | * distributed with this work for additional information 7 | * regarding copyright ownership. The ASF licenses this file 8 | * to you under the Apache License, Version 2.0 (the 9 | * "License"); you may not use this file except in compliance 10 | * with the License. You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, 15 | * software distributed under the License is distributed on an 16 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 | * KIND, either express or implied. See the License for the 18 | * specific language governing permissions and limitations 19 | * under the License. 20 | */ 21 | 22 | import Foundation 23 | 24 | /// It's a data transfer object passed in the `Pattern` matcher block 25 | public struct PatternBody { 26 | /// The key-value pairs of the `Variable` instances found along the way 27 | public var variables: [String: Any] 28 | /// The evaluator instance to help parsing the content 29 | public var interpreter: E 30 | /// The context if the matcher block needs any contextual information 31 | public var context: Context 32 | } 33 | 34 | /// `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. 35 | /// The first parameter contains the values of every matched `Variable` instance. 36 | /// 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 37 | /// In its last parameter if provides information about the context, and therefore allows access to read or modify the context variables. 38 | /// - parameter body: Struct containing the matched variables, an interpreter, and a context 39 | /// - returns: The converted value 40 | public typealias MatcherBlock = (_ body: PatternBody) -> T? 41 | 42 | /// Options that modify the pattern matching algorithm 43 | public struct PatternOptions: OptionSet { 44 | /// Integer representation of the option 45 | public let rawValue: Int 46 | /// Basic initialiser with the integer representation 47 | public init(rawValue: Int) { 48 | self.rawValue = rawValue 49 | } 50 | 51 | /// Searches of the elements of the pattern backward from the end of the output. Othwerise, if not present, it matches from the beginning. 52 | public static let backwardMatch: PatternOptions = PatternOptions(rawValue: 1 << 0) 53 | } 54 | 55 | /// Pattern consists of array of elements 56 | public protocol PatternProtocol { 57 | /// `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. 58 | /// This collection should be provided during the initialisation, and cannot be modified once the `Matcher` instance has been created. 59 | var elements: [PatternElement] { get } 60 | 61 | /// Options that modify the pattern matching algorithm 62 | var options: PatternOptions { get } 63 | 64 | /// Optional name to identify the pattern. If not provided during initialisation, it will fall back to the textual representation of the elements array 65 | var name: String { get } 66 | } 67 | 68 | /// Pattern consists of array of elements 69 | public class Pattern: PatternProtocol { 70 | /// `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. 71 | /// This collection should be provided during the initialisation, and cannot be modified once the `Matcher` instance has been created. 72 | public let elements: [PatternElement] 73 | 74 | /// The block to process the elements with 75 | let matcher: MatcherBlock 76 | 77 | /// Options that modify the pattern matching algorithm 78 | public let options: PatternOptions 79 | 80 | /// Optional name to identify the pattern. If not provided during initialisation, it will fall back to the textual representation of the elements array 81 | public let name: String 82 | 83 | /// 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. 84 | /// - parameter elemenets: The pattern to recognise 85 | /// - parameter name: Optional identifier for the pattern. Defaults to the string representation of the elements 86 | /// - parameter options: Options that modify the pattern matching algorithm 87 | /// - parameter matcher: The block to process the input with 88 | public init(_ elements: [PatternElement], 89 | name: String? = nil, 90 | options: PatternOptions = [], 91 | matcher: @escaping MatcherBlock) { 92 | self.name = name ?? Pattern.stringify(elements: elements) 93 | self.matcher = matcher 94 | self.options = options 95 | self.elements = Pattern.elementsByReplacingTheLastVariableNotToBeShortestMatch(in: elements, options: options) 96 | } 97 | 98 | /// 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. 99 | /// - parameter in: The elements array where the last element should be replaced 100 | /// - parameter options: Options that modify the pattern matching algorithm 101 | /// - returns: A new collection of elements, where the last element is replaced, whether it's a variable with shortest flag on 102 | static func elementsByReplacingTheLastVariableNotToBeShortestMatch(in elements: [PatternElement], options: PatternOptions) -> [PatternElement] { 103 | var elements = elements 104 | let index = options.contains(.backwardMatch) ? elements.startIndex : elements.index(before: elements.endIndex) 105 | 106 | /// Replaces the last element in the elements collection with the new one in the parmeter 107 | /// - parameter element: The element to be replaced 108 | /// - parameter new: The replacement 109 | /// - parameter previousOptions: The element to be replaced 110 | func replaceLast(_ element: VariableProtocol, with new: (_ previousOptions: VariableOptions) -> PatternElement) { 111 | elements.remove(at: index) 112 | elements.insert(new(element.options.union(.exhaustiveMatch)), at: index) 113 | } 114 | 115 | if let last = elements[index] as? GenericVariable, !last.options.contains(.exhaustiveMatch) { 116 | replaceLast(last) { GenericVariable(last.name, options: $0, map: last.map) } 117 | } 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 118 | replaceLast(last) { GenericVariable(last.name, options: $0) { last.performMap(input: $0.value, interpreter: $0.interpreter) } } 119 | } 120 | return elements 121 | } 122 | 123 | /// 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. 124 | /// Uses the `Matcher` class for the evaluation 125 | /// - parameter string: The input 126 | /// - parameter from: The start of the range to analyse the result in 127 | /// - parameter interpreter: An interpreter instance - if the variables need any further evaluation 128 | /// - parameter context: The context - if the block uses any contextual data 129 | /// - parameter connectedRanges: Ranges of string indices that are connected with opening-closing tag pairs, respectively 130 | /// - returns: The result of the matching operation 131 | func matches(string: String, from start: String.Index? = nil, interpreter: I, context: Context, connectedRanges: [ClosedRange] = []) -> MatchResult { 132 | let start = start ?? string.startIndex 133 | let processor = VariableProcessor(interpreter: interpreter, context: context) 134 | let matcher = Matcher(pattern: self, processor: processor) 135 | let result = matcher.match(string: string, from: start, connectedRanges: connectedRanges) { variables in 136 | self.matcher(PatternBody(variables: variables, interpreter: interpreter, context: context)) 137 | } 138 | 139 | if case let .exactMatch(_, output, variables) = result { 140 | let input = String(string[start...]) 141 | context.debugInfo[input] = ExpressionInfo(input: input, output: output, pattern: elementsAsString(), patternName: name, variables: variables) 142 | } 143 | 144 | return result 145 | } 146 | 147 | /// A textual representation of the Pattern's elements array 148 | /// - returns: A stringified version of the input elements 149 | func elementsAsString() -> String { 150 | return Pattern.stringify(elements: elements) 151 | } 152 | 153 | /// A textual representation of the elements array 154 | /// - returns: A stringified version of the input elements 155 | static func stringify(elements: [PatternElement]) -> String { 156 | return elements.map { 157 | if let keyword = $0 as? Keyword { 158 | return keyword.name 159 | } else if let variable = $0 as? VariableProtocol { 160 | return "{\(variable.name)}" 161 | } 162 | return "" 163 | }.joined(separator: " ") 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /Sources/Eval/Utilities/Utils.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 Laszlo Teveli. 3 | * 4 | * Licensed to the Apache Software Foundation (ASF) under one 5 | * or more contributor license agreements. See the NOTICE file 6 | * distributed with this work for additional information 7 | * regarding copyright ownership. The ASF licenses this file 8 | * to you under the Apache License, Version 2.0 (the 9 | * "License"); you may not use this file except in compliance 10 | * with the License. You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, 15 | * software distributed under the License is distributed on an 16 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 | * KIND, either express or implied. See the License for the 18 | * specific language governing permissions and limitations 19 | * under the License. 20 | */ 21 | 22 | import Foundation 23 | 24 | /// Syntactic sugar for `MatchElement` instances to feel like concatenation, whenever the input requires an array of elements. 25 | /// - parameter left: Left hand side 26 | /// - parameter right: Right hand side 27 | /// - returns: An array with two elements (left and right in this order) 28 | public func + (left: PatternElement, right: PatternElement) -> [PatternElement] { 29 | return [left, right] 30 | } 31 | 32 | /// Syntactic sugar for appended arrays 33 | /// - parameter array: The array to append 34 | /// - parameter element: The appended element 35 | /// - returns: A new array by appending `array` with `element` 36 | internal func +
(array: [A], element: A) -> [A] { 37 | return array + [element] 38 | } 39 | 40 | /// Syntactic sugar for appending mutable arrays 41 | /// - parameter array: The array to append 42 | /// - parameter element: The appended element 43 | internal func += (array: inout [A], element: A) { 44 | array = array + element //swiftlint:disable:this shorthand_operator 45 | } 46 | 47 | /// Helpers on `String` to provide `Int` based subscription features and easier usage 48 | extension String { 49 | /// Shorter syntax for trimming 50 | /// - returns: The `String` without the prefix and postfix whitespace characters 51 | func trim() -> String { 52 | return trimmingCharacters(in: .whitespacesAndNewlines) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Tests/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - force_cast 3 | - force_unwrapping 4 | - file_header 5 | - type_name 6 | - explicit_top_level_acl -------------------------------------------------------------------------------- /Tests/EvalTests/IntegrationTests/PerformanceTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PerformanceTest.swift 3 | // EvalTests 4 | // 5 | // Created by László Teveli on 2019. 09. 14.. 6 | // 7 | 8 | import XCTest 9 | @testable import Eval 10 | 11 | class PerformanceTest: XCTestCase { 12 | var interpreter: TypedInterpreter? 13 | 14 | override func setUp() { 15 | super.setUp() 16 | let not = prefixOperator("!") { (value: Bool) in !value } 17 | let not2 = prefixOperator("x") { (value: Bool) in !value } 18 | let equality = infixOperator("==") { (lhs: Bool, rhs: Bool) in lhs == rhs } 19 | 20 | interpreter = TypedInterpreter(dataTypes: [stringDataType(), booleanDataType(), numberDataType()], 21 | functions: [not, not2, equality], 22 | context: Context(variables: ["nothing": true])) 23 | } 24 | 25 | func test_suffix1() { 26 | self.measure { 27 | for _ in 1...1000 { 28 | _ = self.interpreter?.evaluate("!nothing") 29 | } 30 | } 31 | } 32 | 33 | func test_suffix2() { 34 | self.measure { 35 | for _ in 1...1000 { 36 | _ = self.interpreter?.evaluate("x nothing") 37 | } 38 | } 39 | } 40 | 41 | func test_suffix3() { 42 | self.measure { 43 | for _ in 1...1000 { 44 | _ = self.interpreter?.evaluate("nothing == true") 45 | } 46 | } 47 | } 48 | 49 | func numberDataType() -> DataType { 50 | return DataType(type: Double.self, 51 | literals: [Literal { Double($0.value) }, 52 | Literal("pi", convertsTo: Double.pi)]) { String(describing: $0.value) } 53 | } 54 | 55 | func stringDataType() -> DataType { 56 | let singleQuotesLiteral = Literal { literal -> String? in 57 | guard let first = literal.value.first, let last = literal.value.last, first == last, first == "'" else { return nil } 58 | let trimmed = literal.value.trimmingCharacters(in: CharacterSet(charactersIn: "'")) 59 | return trimmed.contains("'") ? nil : trimmed 60 | } 61 | return DataType(type: String.self, literals: [singleQuotesLiteral]) { $0.value } 62 | } 63 | 64 | func booleanDataType() -> DataType { 65 | return DataType(type: Bool.self, literals: [Literal("false", convertsTo: false), Literal("true", convertsTo: true)]) { $0.value ? "true" : "false" } 66 | } 67 | 68 | func prefixOperator(_ symbol: String, body: @escaping (A) -> T) -> Function { 69 | return Function([Keyword(symbol), Variable("value")]) { 70 | guard let value = $0.variables["value"] as? A else { return nil } 71 | return body(value) 72 | } 73 | } 74 | 75 | func infixOperator(_ symbol: String, body: @escaping (A, B) -> T) -> Function { 76 | return Function([Variable("lhs"), Keyword(symbol), Variable("rhs")], options: .backwardMatch) { 77 | guard let lhs = $0.variables["lhs"] as? A, let rhs = $0.variables["rhs"] as? B else { return nil } 78 | return body(lhs, rhs) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Tests/EvalTests/IntegrationTests/Suffix.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Suffix.swift 3 | // Eval 4 | // 5 | // Created by László Teveli on 2019. 09. 14.. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | @testable import Eval 11 | 12 | class MiniExpressionStandardLibraryTest: XCTestCase { 13 | private func evaluate(_ expression: String, inputs: [String: Any] = [:]) -> R? { 14 | let context = Context(variables: inputs) 15 | let interpreter = TypedInterpreter(dataTypes: MiniExpressionStandardLibrary.dataTypes, functions: MiniExpressionStandardLibrary.functions, context: context) 16 | let result = interpreter.evaluate(expression, context: context) 17 | print(context.debugInfo) 18 | return result as? R 19 | } 20 | func testComposition() { 21 | // based on feedback I realized this doesn't work... 22 | // TODO: figure out the suffix error in this case 23 | XCTAssertEqual(evaluate("(toggle == true) and (url exists)", inputs: ["toggle": true, "url": 1]), true) 24 | // XCTAssertEqual(evaluate("(toggle == true) and (url exists)", inputs: ["toggle": true]), false) 25 | XCTAssertEqual(evaluate("(toggle == true) and (not(url exists))", inputs: ["toggle": true]), true) 26 | } 27 | func testComposition2() { 28 | // this (prefix function) does work... 29 | XCTAssertEqual(evaluate("(toggle == true) and (didset url)", inputs: ["toggle": true, "url": 1]), true) 30 | // XCTAssertEqual(evaluate("(toggle == true) and (didset url)", inputs: ["toggle": true]), false) 31 | XCTAssertEqual(evaluate("(toggle == true) and (not(didset url))", inputs: ["toggle": true]), true) 32 | } 33 | } 34 | 35 | class MiniExpressionStandardLibrary { 36 | static var dataTypes: [DataTypeProtocol] { 37 | return [ 38 | booleanType, 39 | ] 40 | } 41 | static var functions: [FunctionProtocol] { 42 | return [ 43 | andOperator, 44 | boolParentheses, 45 | existsOperator, 46 | boolEqualsOperator, 47 | didsetOperator, 48 | ] 49 | } 50 | // MARK: - Types 51 | 52 | static var booleanType: DataType { 53 | let trueLiteral = Literal("true", convertsTo: true) 54 | let falseLiteral = Literal("false", convertsTo: false) 55 | return DataType(type: Bool.self, literals: [trueLiteral, falseLiteral]) { $0.value ? "true" : "false" } 56 | } 57 | // MARK: - Functions 58 | 59 | static var boolEqualsOperator: Function { 60 | return infixOperator("==") { (lhs: Bool, rhs: Bool) in lhs == rhs } 61 | } 62 | static var boolParentheses: Function { 63 | return Function([OpenKeyword("("), Variable("body"), CloseKeyword(")")]) { $0.variables["body"] as? Bool } 64 | } 65 | static var andOperator: Function { 66 | return infixOperator("and") { (lhs: Bool, rhs: Bool) in lhs && rhs } 67 | } 68 | static var existsOperator: Function { 69 | return suffixOperator("exists") { (expression: Any?) in expression != nil } 70 | } 71 | static var didsetOperator: Function { 72 | return prefixOperator("didset") { (expression: Any?) in expression != nil } 73 | } 74 | // MARK: - Operator helpers 75 | 76 | static func infixOperator(_ symbol: String, body: @escaping (A, B) -> T) -> Function { 77 | return Function([Variable("lhs"), Keyword(symbol), Variable("rhs")], options: .backwardMatch) { 78 | guard let lhs = $0.variables["lhs"] as? A, let rhs = $0.variables["rhs"] as? B else { return nil } 79 | return body(lhs, rhs) 80 | } 81 | } 82 | static func prefixOperator(_ symbol: String, body: @escaping (A) -> T) -> Function { 83 | return Function([Keyword(symbol), Variable("value")]) { 84 | guard let value = $0.variables["value"] as? A else { return nil } 85 | return body(value) 86 | } 87 | } 88 | static func suffixOperator(_ symbol: String, body: @escaping (A) -> T) -> Function { 89 | return Function([Variable("value"), Keyword(symbol)]) { 90 | guard let value = $0.variables["value"] as? A else { return nil } 91 | return body(value) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Tests/EvalTests/IntegrationTests/TemplateTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Eval 2 | import class Eval.Pattern 3 | import XCTest 4 | 5 | class TemplateTests: XCTestCase { 6 | func test_flow() { 7 | let parenthesis = Function([Keyword("("), Variable("body"), Keyword(")")]) { $0.variables["body"] } 8 | let subtractOperator = infixOperator("-") { (lhs: Double, rhs: Double) in lhs - rhs } 9 | 10 | let interpreter = TypedInterpreter(dataTypes: [numberDataType()], functions: [parenthesis, subtractOperator]) 11 | 12 | XCTAssertEqual(interpreter.evaluate("6 - (4 - 2)") as! Double, 4) 13 | } 14 | 15 | func test_whenAddingALotOfFunctions_thenInterpretationWorksCorrectly() { 16 | let parenthesis = Function([Keyword("("), Variable("body"), Keyword(")")]) { $0.variables["body"] } 17 | let plusOperator = infixOperator("+") { (lhs: Double, rhs: Double) in lhs + rhs } 18 | let concat = infixOperator("+") { (lhs: String, rhs: String) in lhs + rhs } 19 | let lessThan = infixOperator("<") { (lhs: Double, rhs: Double) in lhs < rhs } 20 | 21 | let interpreter = TypedInterpreter(dataTypes: [numberDataType(), stringDataType()], 22 | functions: [concat, parenthesis, plusOperator, lessThan], 23 | context: Context(variables: ["name": "Laszlo Teveli"])) 24 | 25 | let ifStatement = Pattern>([Keyword("{%"), Keyword("if"), Variable("condition"), Keyword("%}"), TemplateVariable("body"), Keyword("{% endif %}")]) { 26 | guard let condition = $0.variables["condition"] as? Bool, let body = $0.variables["body"] as? String else { return nil } 27 | return condition ? body : nil 28 | } 29 | 30 | let printStatement = Pattern>([Keyword("{{"), Variable("body"), Keyword("}}")]) { 31 | guard let body = $0.variables["body"] else { return nil } 32 | return $0.interpreter.typedInterpreter.print(body) 33 | } 34 | 35 | let template = StringTemplateInterpreter(statements: [ifStatement, printStatement], interpreter: interpreter, context: Context()) 36 | XCTAssertEqual(template.evaluate("{{ 1 + 2 }}"), "3.0") 37 | XCTAssertEqual(template.evaluate("{{ 'Hello' + ' ' + 'World' + '!' }}"), "Hello World!") 38 | XCTAssertEqual(template.evaluate("asd {% if 10 < 21 %}Hello{% endif %} asd"), "asd Hello asd") 39 | XCTAssertEqual(template.evaluate("ehm, {% if 10 < 21 %}{{ 'Hello ' + name }}{% endif %}!"), "ehm, Hello Laszlo Teveli!") 40 | } 41 | 42 | func test_whenEmbeddingTags_thenInterpretationWorksCorrectly() { 43 | let parenthesis = Function([OpenKeyword("("), Variable("body"), CloseKeyword(")")]) { $0.variables["body"] } 44 | let lessThan = infixOperator("<") { (lhs: Double, rhs: Double) in lhs < rhs } 45 | let interpreter = TypedInterpreter(dataTypes: [numberDataType(), stringDataType(), booleanDataType()], functions: [parenthesis, lessThan], context: Context()) 46 | 47 | let braces = Pattern>([OpenKeyword("("), TemplateVariable("body"), CloseKeyword(")")]) { $0.variables["body"] as? String } 48 | let ifStatement = Pattern>([OpenKeyword("{% if"), Variable("condition"), Keyword("%}"), TemplateVariable("body"), CloseKeyword("{% endif %}")]) { 49 | guard let condition = $0.variables["condition"] as? Bool, let body = $0.variables["body"] as? String else { return nil } 50 | return condition ? body : nil 51 | } 52 | 53 | let template = StringTemplateInterpreter(statements: [braces, ifStatement], interpreter: interpreter, context: Context()) 54 | XCTAssertEqual(template.evaluate("(a)"), "a") 55 | XCTAssertEqual(template.evaluate("(a(b))"), "ab") 56 | XCTAssertEqual(template.evaluate("((a)b)"), "ab") 57 | XCTAssertEqual(template.evaluate("(a(b)c)"), "abc") 58 | XCTAssertEqual(template.evaluate("{% if 10 < 21 %}Hello {% if true %}you{% endif %}!{% endif %}"), "Hello you!") 59 | } 60 | 61 | // MARK: Helpers - data types 62 | 63 | func numberDataType() -> DataType { 64 | return DataType(type: Double.self, 65 | literals: [Literal { Double($0.value) }, 66 | Literal("pi", convertsTo: Double.pi) ]) { String(describing: $0.value) } 67 | } 68 | 69 | func stringDataType() -> DataType { 70 | let singleQuotesLiteral = Literal { literal -> String? in 71 | guard let first = literal.value.first, let last = literal.value.last, first == last, first == "'" else { return nil } 72 | let trimmed = literal.value.trimmingCharacters(in: CharacterSet(charactersIn: "'")) 73 | return trimmed.contains("'") ? nil : trimmed 74 | } 75 | return DataType(type: String.self, literals: [singleQuotesLiteral]) { $0.value } 76 | } 77 | 78 | func booleanDataType() -> DataType { 79 | return DataType(type: Bool.self, literals: [Literal("false", convertsTo: false), Literal("true", convertsTo: true)]) { $0.value ? "true" : "false" } 80 | } 81 | 82 | // MARK: Helpers - operators 83 | 84 | func infixOperator(_ symbol: String, body: @escaping (A, B) -> T) -> Function { 85 | return Function([Variable("lhs"), Keyword(symbol), Variable("rhs")]) { 86 | guard let lhs = $0.variables["lhs"] as? A, let rhs = $0.variables["rhs"] as? B else { return nil } 87 | return body(lhs, rhs) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Tests/EvalTests/UnitTests/DataTypeTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Eval 2 | import XCTest 3 | 4 | class DataTypeTests: XCTestCase { 5 | 6 | // MARK: init 7 | 8 | func test_whenInitialised_then() { 9 | let type = Double.self 10 | let literal = Literal { Double($0.value) } 11 | let print: (DataTypeBody) -> String = { String($0.value) } 12 | 13 | let dataType = DataType(type: type, literals: [literal], print: print) 14 | 15 | XCTAssertTrue(type == dataType.type.self) 16 | XCTAssertTrue(literal === dataType.literals[0]) 17 | XCTAssertNotNil(dataType.print) 18 | } 19 | 20 | // MARK: convert 21 | 22 | func test_whenConverting_thenGeneratesStringValue() { 23 | let dataType = DataType(type: Double.self, literals: [Literal { Double($0.value) }]) { String($0.value) } 24 | 25 | let result = dataType.convert(input: "1", interpreter: TypedInterpreter()) 26 | 27 | XCTAssertEqual(result as! Double, 1) 28 | } 29 | 30 | func test_whenConvertingInvalidValue_thenGeneratesNilValue() { 31 | let dataType = DataType(type: Double.self, literals: [Literal { Double($0.value) }]) { String($0.value) } 32 | 33 | let result = dataType.convert(input: "a", interpreter: TypedInterpreter()) 34 | 35 | XCTAssertNil(result) 36 | } 37 | 38 | // MARK: print 39 | 40 | func test_whenPrinting_thenGeneratesStringValue() { 41 | let dataType = DataType(type: Double.self, literals: [Literal { Double($0.value) }]) { _ in "printed value" } 42 | 43 | let result = dataType.print(value: 1.0, printer: TypedInterpreter()) 44 | 45 | XCTAssertEqual(result, "printed value") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/EvalTests/UnitTests/FunctionTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Eval 2 | import XCTest 3 | import class Eval.Pattern 4 | 5 | class FunctionTests: XCTestCase { 6 | 7 | // MARK: init 8 | 9 | func test_whenInitialised_thenPatternsAreSaved() { 10 | let pattern = Pattern([Keyword("in")]) { _ in 1 } 11 | 12 | let function = Function(patterns: [pattern]) 13 | 14 | XCTAssertEqual(function.patterns.count, 1) 15 | XCTAssertTrue(pattern === function.patterns[0]) 16 | } 17 | 18 | func test_whenInitialisedWithOnePatters_thenPatternIsSaved() { 19 | let pattern = [Keyword("in")] 20 | 21 | let function = Function(pattern) { _ in 1 } 22 | 23 | XCTAssertEqual(function.patterns.count, 1) 24 | XCTAssertEqual(function.patterns[0].elements.count, 1) 25 | XCTAssertTrue(pattern[0] === function.patterns[0].elements[0] as! Keyword) 26 | } 27 | 28 | // MARK: convert 29 | 30 | func test_whenConverting_thenResultIsValid() { 31 | let function = Function([Keyword("in")]) { _ in 1 } 32 | 33 | let result = function.convert(input: "input", interpreter: TypedInterpreter(), context: Context()) 34 | 35 | XCTAssertEqual(result as! Int, 1) 36 | } 37 | 38 | func test_whenConvertingInvalidValue_thenConversionReturnsNil() { 39 | let function = Function([Keyword("in")]) { _ in 1 } 40 | 41 | let result = function.convert(input: "example", interpreter: TypedInterpreter(), context: Context()) 42 | 43 | XCTAssertNil(result) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Tests/EvalTests/UnitTests/InterpreterContextTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Eval 2 | import XCTest 3 | 4 | class InterpreterContextTests: XCTestCase { 5 | 6 | // MARK: init 7 | 8 | func test_whenCreated_thenVariablesAreSet() { 9 | let variables = ["test": 2] 10 | 11 | let context = Context(variables: variables) 12 | 13 | XCTAssertEqual(variables, context.variables as! [String: Int]) 14 | } 15 | 16 | // MARK: push/pop 17 | 18 | func test_whenPushing_thenRemainsTheSame() { 19 | let variables = ["test": 2] 20 | let context = Context(variables: variables) 21 | 22 | context.push() 23 | 24 | XCTAssertEqual(variables, context.variables as! [String: Int]) 25 | } 26 | 27 | func test_whenPushingAndModifying_thenContextChanges() { 28 | let variables = ["test": 2] 29 | let context = Context(variables: variables) 30 | 31 | context.push() 32 | context.variables["a"] = 3 33 | 34 | XCTAssertNotEqual(variables, context.variables as! [String: Int]) 35 | } 36 | 37 | func test_whenPushingModifyingAndPopping_thenRestores() { 38 | let variables = ["test": 2] 39 | let context = Context(variables: variables) 40 | 41 | context.push() 42 | context.variables["a"] = 3 43 | context.pop() 44 | 45 | XCTAssertEqual(variables, context.variables as! [String: Int]) 46 | } 47 | 48 | func test_whenJustPopping_thenNothingHappens() { 49 | let variables = ["test": 2] 50 | let context = Context(variables: variables) 51 | 52 | context.pop() 53 | 54 | XCTAssertEqual(variables, context.variables as! [String: Int]) 55 | } 56 | 57 | // MARK: merging 58 | 59 | func test_whenMergingTwo_thenCreatesANewContext() { 60 | let one = Context(variables: ["a": 1]) 61 | let two = Context(variables: ["b": 2]) 62 | 63 | let result = one.merging(with: two) 64 | 65 | XCTAssertEqual(result.variables as! [String: Int], ["a": 1, "b": 2]) 66 | XCTAssertFalse(one === result) 67 | XCTAssertFalse(two === result) 68 | } 69 | 70 | func test_whenMergingTwo_thenParameterOverridesVariablesInSelf() { 71 | let one = Context(variables: ["a": 1]) 72 | let two = Context(variables: ["a": 2, "x": 3]) 73 | 74 | let result = one.merging(with: two) 75 | 76 | XCTAssertEqual(result.variables as! [String: Int], ["a": 2, "x": 3]) 77 | } 78 | 79 | func test_whenMergingWithNil_thenReturnsSelf() { 80 | let context = Context(variables: ["a": 1]) 81 | 82 | let result = context.merging(with: nil) 83 | 84 | XCTAssertTrue(result === context) 85 | } 86 | 87 | // MARK: merge 88 | 89 | func test_whenMergingTwoInAMutableWay_thenMergesVariables() { 90 | let one = Context(variables: ["a": 1]) 91 | let two = Context(variables: ["b": 2]) 92 | 93 | one.merge(with: two) { existing, _ in existing } 94 | 95 | XCTAssertEqual(one.variables as! [String: Int], ["a": 1, "b": 2]) 96 | } 97 | 98 | func test_whenMergingTwoInAMutableWay_thenParameterOverridesVariablesInSelf() { 99 | let one = Context(variables: ["a": 1]) 100 | let two = Context(variables: ["a": 2, "x": 3]) 101 | 102 | one.merge(with: two) { existing, _ in existing } 103 | 104 | XCTAssertEqual(one.variables as! [String: Int], ["a": 1, "x": 3]) 105 | } 106 | 107 | func test_whenMergingTwoInAMutableWayReversed_thenParameterOverridesVariablesInSelf() { 108 | let one = Context(variables: ["a": 1]) 109 | let two = Context(variables: ["a": 2, "x": 3]) 110 | 111 | two.merge(with: one) { _, new in new } 112 | 113 | XCTAssertEqual(two.variables as! [String: Int], ["a": 1, "x": 3]) 114 | } 115 | 116 | func test_whenMergingWithNilInAMutableWay_thenReturnsSelf() { 117 | let context = Context(variables: ["a": 1]) 118 | 119 | context.merge(with: nil) { existing, _ in existing } 120 | 121 | XCTAssertTrue(context.variables as! [String: Int] == ["a": 1]) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Tests/EvalTests/UnitTests/KeywordTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Eval 2 | import XCTest 3 | 4 | class KeywordTests: XCTestCase { 5 | 6 | // MARK: Initialisation 7 | 8 | func test_whenKeywordIsCreated_thenNameAndTypeIsSet() { 9 | let dummyName = "test name" 10 | let dummyType = Keyword.KeywordType.generic 11 | 12 | let keyword = Keyword(dummyName, type: dummyType) 13 | 14 | XCTAssertEqual(keyword.name, dummyName) 15 | XCTAssertEqual(keyword.type, dummyType) 16 | } 17 | 18 | func test_whenKeywordIsCreated_thenTypeIsGenericByDefault() { 19 | let keyword = Keyword("test name") 20 | 21 | XCTAssertEqual(keyword.type, .generic) 22 | } 23 | 24 | // MARK: Equality 25 | 26 | func test_whenIdenticalKeywordsAreCreated_thenTheyAreEqual() { 27 | let keyword1 = Keyword("test name", type: .openingStatement) 28 | let keyword2 = Keyword("test name", type: .openingStatement) 29 | 30 | XCTAssertEqual(keyword1, keyword2) 31 | } 32 | 33 | func test_whenKeywordsWithDifferentNamesAreCreated_thenTheyAreNotEqual() { 34 | let keyword1 = Keyword("test name", type: .openingStatement) 35 | let keyword2 = Keyword("different", type: .openingStatement) 36 | 37 | XCTAssertNotEqual(keyword1, keyword2) 38 | } 39 | 40 | func test_whenKeywordsWithDifferentTypesAreCreated_thenTheyAreNotEqual() { 41 | let keyword1 = Keyword("test name", type: .openingStatement) 42 | let keyword2 = Keyword("test name", type: .closingStatement) 43 | 44 | XCTAssertNotEqual(keyword1, keyword2) 45 | } 46 | 47 | // MARK: Match 48 | 49 | func test_whenPartlyMatches_thenReturnsPossibleMatch() { 50 | let keyword = Keyword("checking prefix") 51 | 52 | let result = keyword.matches(prefix: "check") 53 | 54 | XCTAssertTrue(result.isPossibleMatch()) 55 | } 56 | 57 | func test_whenNotMatches_thenReturnsNoMatch() { 58 | let keyword = Keyword("checking prefix") 59 | 60 | let result = keyword.matches(prefix: "example") 61 | 62 | XCTAssertTrue(result.isNoMatch()) 63 | } 64 | 65 | func test_whenMatches_thenReturnsExactMatch() { 66 | let keyword = Keyword("checking prefix") 67 | 68 | let result = keyword.matches(prefix: "checking prefix") 69 | 70 | verifyMatch(expectation: "checking prefix", result: result) 71 | } 72 | 73 | func test_whenMatchesAndContinues_thenReturnsExactMatch() { 74 | let keyword = Keyword("checking prefix") 75 | 76 | let result = keyword.matches(prefix: "checking prefix with extra content") 77 | 78 | verifyMatch(expectation: "checking prefix", result: result) 79 | } 80 | 81 | // MARK: OpenKeyword 82 | 83 | func test_whenCreatingOpenKeyword_thenTheTypeIsSetCorrectly() { 84 | let keyword = OpenKeyword("checking prefix") 85 | 86 | XCTAssertEqual(keyword.type, .openingStatement) 87 | XCTAssertEqual(keyword.name, "checking prefix") 88 | } 89 | 90 | // MARK: CloseKeyword 91 | 92 | func test_whenCreatingCloseKeyword_thenTheTypeIsSetCorrectly() { 93 | let keyword = CloseKeyword("checking prefix") 94 | 95 | XCTAssertEqual(keyword.type, .closingStatement) 96 | XCTAssertEqual(keyword.name, "checking prefix") 97 | } 98 | 99 | // MARK: Match performance 100 | 101 | func test_whenShortKeywordMatchesShortInput_thenPerformsWell() { 102 | let keyword = Keyword("=") 103 | self.measure { 104 | _ = keyword.matches(prefix: "= asd") 105 | } 106 | } 107 | 108 | func test_whenShortKeywordNotMatchesInput_thenPerformsWell() { 109 | let keyword = Keyword("=") 110 | self.measure { 111 | _ = keyword.matches(prefix: "checking prefix") 112 | } 113 | } 114 | 115 | func test_whenShortKeywordHureInput_thenPerformsWell() { 116 | let keyword = Keyword("=") 117 | self.measure { 118 | _ = keyword.matches(prefix: """ 119 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. 120 | 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. 121 | It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. 122 | 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. 123 | """) 124 | } 125 | } 126 | 127 | func test_whenLargeKeywordMatchesShortInput_thenPerformsWell() { 128 | let keyword = Keyword("this is an example to match") 129 | self.measure { 130 | _ = keyword.matches(prefix: "this is an example to match and some other things") 131 | } 132 | } 133 | 134 | func test_whenLargeKeywordNotMatchesInput_thenPerformsWell() { 135 | let keyword = Keyword("this is an example to match") 136 | self.measure { 137 | _ = keyword.matches(prefix: "x and y") 138 | } 139 | } 140 | 141 | func test_whenLargeKeywordHureInput_thenPerformsWell() { 142 | let keyword = Keyword("=") 143 | self.measure { 144 | _ = keyword.matches(prefix: """ 145 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. 146 | 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. 147 | It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. 148 | 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. 149 | """) 150 | } 151 | } 152 | 153 | // MARK: Helpers 154 | 155 | func verifyMatch(expectation: String, result: MatchResult) { 156 | if case .exactMatch(let length, let output, let variables) = result { 157 | XCTAssertEqual(length, expectation.count) 158 | XCTAssertEqual(output as! String, expectation) 159 | XCTAssertTrue(variables.isEmpty) 160 | } else { 161 | fatalError("No match") 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /Tests/EvalTests/UnitTests/LiteralTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Eval 2 | import XCTest 3 | 4 | class LiteralTests: XCTestCase { 5 | 6 | // MARK: init 7 | 8 | func test_whenInitialisedWithBlock_thenParameterIsSaved() { 9 | let block: (LiteralBody) -> Double? = { Double($0.value) } 10 | 11 | let literal = Literal(convert: block) 12 | 13 | XCTAssertNotNil(literal.convert) 14 | } 15 | 16 | func test_whenInitialisedWithValue_thenConvertBlockIsSaved() { 17 | let literal = Literal("true", convertsTo: false) 18 | 19 | XCTAssertNotNil(literal.convert) 20 | } 21 | 22 | // MARK: convert 23 | 24 | func test_whenConverting_thenCallsBlock() { 25 | let block: (LiteralBody) -> Int? = { _ in 123 } 26 | let literal = Literal(convert: block) 27 | 28 | let result = literal.convert(input: "asd", interpreter: TypedInterpreter()) 29 | 30 | XCTAssertEqual(result!, 123) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/EvalTests/UnitTests/MatchResultTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Eval 2 | import XCTest 3 | 4 | class MatchResultTests: XCTestCase { 5 | 6 | // MARK: isMatch 7 | 8 | func test_whenMatchIsExactMatch_thenIsMatchReturnsTrue() { 9 | let matchResult = MatchResult.exactMatch(length: 1, output: "a", variables: [:]) 10 | 11 | XCTAssertTrue(matchResult.isMatch()) 12 | } 13 | 14 | func test_whenMatchIsNotExactMatch_thenIsMatchReturnsFalse() { 15 | let matchResult = MatchResult.anyMatch(exhaustive: true) 16 | 17 | XCTAssertFalse(matchResult.isMatch()) 18 | } 19 | 20 | // MARK: isPossibleMatch 21 | 22 | func test_whenMatchIsPossibleyMatch_thenIsPossibleMatchReturnsTrue() { 23 | let matchResult = MatchResult.possibleMatch 24 | 25 | XCTAssertTrue(matchResult.isPossibleMatch()) 26 | } 27 | 28 | func test_whenMatchIsNotPossibleyMatch_thenIsPossibleMatchReturnsFalse() { 29 | let matchResult = MatchResult.noMatch 30 | 31 | XCTAssertFalse(matchResult.isPossibleMatch()) 32 | } 33 | 34 | // MARK: isNoMatch 35 | 36 | func test_whenMatchIsNoMatch_thenIsNoMatchReturnsTrue() { 37 | let matchResult = MatchResult.noMatch 38 | 39 | XCTAssertTrue(matchResult.isNoMatch()) 40 | } 41 | 42 | func test_whenMatchIsNotNoMatch_thenIsNoMatchReturnsFalse() { 43 | let matchResult = MatchResult.anyMatch(exhaustive: false) 44 | 45 | XCTAssertFalse(matchResult.isNoMatch()) 46 | } 47 | 48 | // MARK: isAnyMatch 49 | 50 | func test_whenMatchIsAnyMatch_thenIsAnyMatchReturnsTrue() { 51 | let matchResult = MatchResult.anyMatch(exhaustive: true) 52 | 53 | XCTAssertTrue(matchResult.isAnyMatch(exhaustive: true)) 54 | } 55 | 56 | func test_whenMatchIsAnyMatch_thenIsAnyMatchWithParameterReturnsTrue() { 57 | let matchResult = MatchResult.anyMatch(exhaustive: false) 58 | 59 | XCTAssertTrue(matchResult.isAnyMatch(exhaustive: false)) 60 | } 61 | 62 | func test_whenMatchIsNotAnyMatch_thenIsAnyMatchReturnsFalse() { 63 | let matchResult = MatchResult.possibleMatch 64 | 65 | XCTAssertFalse(matchResult.isAnyMatch()) 66 | } 67 | 68 | // MARK: Equality 69 | 70 | func test_whenTwoAnyMatchesAreCompared_thenResultIsEqual() { 71 | let one = MatchResult.anyMatch(exhaustive: false) 72 | let two = MatchResult.anyMatch(exhaustive: false) 73 | 74 | XCTAssertTrue(one == two) 75 | } 76 | 77 | func test_whenTwoAnyMatchesWithDifferentPropertiesAreCompared_thenResultIsNotEqual() { 78 | let one = MatchResult.anyMatch(exhaustive: false) 79 | let two = MatchResult.anyMatch(exhaustive: true) 80 | 81 | XCTAssertFalse(one == two) 82 | } 83 | 84 | func test_whenTwoNoMatchesAreCompared_thenResultIsEqual() { 85 | let one = MatchResult.noMatch 86 | let two = MatchResult.noMatch 87 | 88 | XCTAssertTrue(one == two) 89 | } 90 | 91 | func test_whenTwoPossibleMatchesAreCompared_thenResultIsEqual() { 92 | let one = MatchResult.possibleMatch 93 | let two = MatchResult.possibleMatch 94 | 95 | XCTAssertTrue(one == two) 96 | } 97 | 98 | func test_whenTwoExactMatchesAreCompared_thenResultIsEqual() { 99 | let one = MatchResult.exactMatch(length: 1, output: "a", variables: [:]) 100 | let two = MatchResult.exactMatch(length: 1, output: "a", variables: [:]) 101 | 102 | XCTAssertTrue(one == two) 103 | } 104 | 105 | func test_whenTwoExactMatchesWithDifferentPropertiesAreCompared_thenResultIsNotEqual() { 106 | let one = MatchResult.exactMatch(length: 1, output: "a", variables: [:]) 107 | let two = MatchResult.exactMatch(length: 2, output: "b", variables: ["1": 2]) 108 | 109 | XCTAssertFalse(one == two) 110 | } 111 | 112 | func test_whenTwoDifferentMatchesAreCompared_thenResultIsNotEqual() { 113 | let one = MatchResult.exactMatch(length: 1, output: "a", variables: [:]) 114 | let two = MatchResult.anyMatch(exhaustive: true) 115 | 116 | XCTAssertFalse(one == two) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Tests/EvalTests/UnitTests/MatchStatementTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Eval 2 | import XCTest 3 | import class Eval.Pattern 4 | 5 | class MatchStatementTests: XCTestCase { 6 | 7 | func test_whenMatchingOne_returnsMatch() { 8 | let input = "input" 9 | let matcher = Pattern([Keyword("in")]) { _ in 1 } 10 | 11 | let result1 = matcher.matches(string: input, interpreter: DummyInterpreter(), context: Context()) 12 | let result2 = matchStatement(amongst: [matcher], in: input, interpreter: DummyInterpreter(), context: Context()) 13 | 14 | XCTAssertTrue(result1 == result2) 15 | } 16 | 17 | func test_whenMatchingTwo_returnsMatch() { 18 | let input = "input" 19 | let matcher1 = Pattern([Keyword("in")]) { _ in 1 } 20 | let matcher2 = Pattern([Keyword("on")]) { _ in 2 } 21 | 22 | let result = matchStatement(amongst: [matcher1, matcher2], in: input, interpreter: DummyInterpreter(), context: Context()) 23 | 24 | XCTAssertTrue(result == MatchResult.exactMatch(length: 2, output: 1, variables: [:])) 25 | } 26 | 27 | func test_whenMatchingTwoMatches_returnsTheFirstMatch() { 28 | let input = "input" 29 | let matcher1 = Pattern([Keyword("in")]) { _ in 1 } 30 | let matcher2 = Pattern([Keyword("inp")]) { _ in 2 } 31 | 32 | let result = matchStatement(amongst: [matcher1, matcher2], in: input, interpreter: DummyInterpreter(), context: Context()) 33 | 34 | XCTAssertTrue(result == MatchResult.exactMatch(length: 2, output: 1, variables: [:])) 35 | } 36 | 37 | func test_whenMatchingInvalid_returnsNoMatch() { 38 | let input = "xxx" 39 | let matcher1 = Pattern([Keyword("in")]) { _ in 1 } 40 | let matcher2 = Pattern([Keyword("on")]) { _ in 2 } 41 | 42 | let result = matchStatement(amongst: [matcher1, matcher2], in: input, interpreter: DummyInterpreter(), context: Context()) 43 | 44 | XCTAssertTrue(result == MatchResult.noMatch) 45 | } 46 | 47 | func test_whenMatchingPrefix_returnsPossibleMatch() { 48 | let input = "i" 49 | let matcher1 = Pattern([Keyword("in")]) { _ in 1 } 50 | let matcher2 = Pattern([Keyword("on")]) { _ in 2 } 51 | 52 | let result = matchStatement(amongst: [matcher1, matcher2], in: input, interpreter: DummyInterpreter(), context: Context()) 53 | 54 | XCTAssertTrue(result == MatchResult.possibleMatch) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Tests/EvalTests/UnitTests/MatcherTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Eval 2 | import XCTest 3 | 4 | class MatcherTests: XCTestCase { 5 | 6 | // MARK: isEmbedded 7 | 8 | func test_whenEmbedding_thenIsEmbeddedReturnsTrue() { 9 | let opening = Keyword("(", type: .openingStatement) 10 | let closing = Keyword(")", type: .closingStatement) 11 | let processor = VariableProcessor(interpreter: DummyInterpreter(), context: Context()) 12 | let matcher = Matcher(pattern: pattern([opening, closing]), processor: processor) 13 | 14 | let input = "(input(random))" 15 | let result = matcher.isEmbedded(element: closing, in: input, at: input.startIndex) 16 | 17 | XCTAssertTrue(result) 18 | } 19 | 20 | func test_whenNotEmbedding_thenIsEmbeddedReturnsFalse() { 21 | let opening = Keyword("(", type: .openingStatement) 22 | let closing = Keyword(")", type: .closingStatement) 23 | let processor = VariableProcessor(interpreter: DummyInterpreter(), context: Context()) 24 | let matcher = Matcher(pattern: pattern([opening, closing]), processor: processor) 25 | 26 | let input = "input" 27 | let result = matcher.isEmbedded(element: opening, in: input, at: input.startIndex) 28 | 29 | XCTAssertFalse(result) 30 | } 31 | 32 | func test_whenEmbeddingButLate_thenIsEmbeddedReturnsFalse() { 33 | let opening = Keyword("(", type: .openingStatement) 34 | let closing = Keyword(")", type: .closingStatement) 35 | let processor = VariableProcessor(interpreter: DummyInterpreter(), context: Context()) 36 | let matcher = Matcher(pattern: pattern([opening, closing]), processor: processor) 37 | 38 | let input = "input(random)" 39 | let result = matcher.isEmbedded(element: closing, in: input, at: input.index(input.startIndex, offsetBy: 12)) 40 | 41 | XCTAssertFalse(result) 42 | } 43 | 44 | // MARK: positionOfClosingTag 45 | 46 | func test_whenEmbedding_thenClosingPositionIsValid() { 47 | let opening = Keyword("(", type: .openingStatement) 48 | let closing = Keyword(")", type: .closingStatement) 49 | let processor = VariableProcessor(interpreter: DummyInterpreter(), context: Context()) 50 | let matcher = Matcher(pattern: pattern([opening, closing]), processor: processor) 51 | 52 | let input = "(input(random))" 53 | XCTAssertEqual(matcher.positionOfClosingTag(in: input, from: input.startIndex), input.index(input.startIndex, offsetBy: 14)) 54 | XCTAssertEqual(matcher.positionOfClosingTag(in: input, from: input.index(after: input.startIndex)), input.index(input.startIndex, offsetBy: 13)) 55 | } 56 | 57 | func test_whenNotEmbedding_thenClosingPositionIsNil() { 58 | let opening = Keyword("(", type: .openingStatement) 59 | let closing = Keyword(")", type: .closingStatement) 60 | let processor = VariableProcessor(interpreter: DummyInterpreter(), context: Context()) 61 | let matcher = Matcher(pattern: pattern([opening, closing]), processor: processor) 62 | 63 | let input = "input" 64 | XCTAssertNil(matcher.positionOfClosingTag(in: input, from: input.startIndex)) 65 | } 66 | 67 | func test_whenEmbeddingButLate_thenClosingPositionIsNil() { 68 | let opening = Keyword("(", type: .openingStatement) 69 | let closing = Keyword(")", type: .closingStatement) 70 | let processor = VariableProcessor(interpreter: DummyInterpreter(), context: Context()) 71 | let matcher = Matcher(pattern: pattern([opening, closing]), processor: processor) 72 | 73 | let input = "(input(random))" 74 | XCTAssertNil(matcher.positionOfClosingTag(in: input, from: input.index(input.startIndex, offsetBy: 8))) 75 | } 76 | 77 | private func pattern(_ elements: [PatternElement]) -> Eval.Pattern { 78 | return Pattern(elements) { _ in "" } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Tests/EvalTests/UnitTests/PatternTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Eval 2 | import XCTest 3 | import class Eval.Pattern 4 | 5 | class PatternTests: XCTestCase { 6 | 7 | // MARK: init 8 | 9 | class DummyElement: PatternElement { 10 | func matches(prefix: String, options: PatternOptions) -> MatchResult { 11 | return .noMatch 12 | } 13 | } 14 | 15 | func test_whenInitialised_thenElementsAndMatcherAreSet() { 16 | let element = DummyElement() 17 | let matcherBlock: MatcherBlock = { _ in 1 } 18 | 19 | let matcher = Pattern([element], matcher: matcherBlock) 20 | 21 | XCTAssertTrue(element === matcher.elements[0] as! DummyElement) 22 | //XCTAssertTrue(matcherBlock === matcher.matcher) 23 | } 24 | 25 | func test_whenInitialisedWithVariableOnTheLastPlace_thenLastVariableBecomesNotShortest() { 26 | let element = DummyElement() 27 | let variable = GenericVariable("name") 28 | 29 | let matcher = Pattern([element, variable]) { _ in "x" } 30 | 31 | let result = matcher.elements[1] as! GenericVariable 32 | XCTAssertTrue(element === matcher.elements[0] as! DummyElement) 33 | XCTAssertFalse(variable === result) 34 | XCTAssertTrue(result.options.contains(.exhaustiveMatch)) 35 | } 36 | 37 | // MARK: matches 38 | 39 | // swiftlint:disable:next function_body_length 40 | func test_whenMatching_thenExpectingAppropriateResults() { 41 | // swiftlint:disable:next nesting 42 | typealias TestCase = (elements: [PatternElement], input: String, expectedResult: MatchResult) 43 | 44 | let keyword = Keyword("ok") 45 | let variable = GenericVariable("name", options: .notInterpreted) 46 | let variableLongest = GenericVariable("name", options: [.notInterpreted, .exhaustiveMatch]) 47 | let variable2 = GenericVariable("last", options: .notInterpreted) 48 | 49 | let testCases: [TestCase] = [ 50 | ([keyword], "invalid", .noMatch), 51 | ([keyword], "o", .possibleMatch), 52 | ([keyword], "ok", .exactMatch(length: 2, output: 1, variables: [:])), 53 | ([keyword], "okokok", .exactMatch(length: 2, output: 1, variables: [:])), 54 | 55 | ([variable], "anything", .exactMatch(length: 8, output: 1, variables: ["name": "anything"])), 56 | ([variableLongest], "anything", .exactMatch(length: 8, output: 1, variables: ["name": "anything"])), 57 | 58 | ([variable, keyword], "invalid", .possibleMatch), 59 | ([variable, keyword], "invalid o", .possibleMatch), 60 | ([variable, keyword], "invalid ok", .exactMatch(length: 10, output: 1, variables: ["name": "invalid"])), 61 | ([variable, keyword], "invalidxok", .exactMatch(length: 10, output: 1, variables: ["name": "invalidx"])), 62 | ([variable, keyword], "invalidxok extra", .exactMatch(length: 10, output: 1, variables: ["name": "invalidx"])), 63 | 64 | ([keyword, variable], "xokthen", .noMatch), 65 | ([keyword, variable], "o", .possibleMatch), 66 | ([keyword, variable], "oi", .noMatch), 67 | ([keyword, variable], "ok", .exactMatch(length: 2, output: 1, variables: ["name": ""])), 68 | ([keyword, variable], "ok then", .exactMatch(length: 7, output: 1, variables: ["name": "then"])), 69 | ([keyword, variable], "ok,then", .exactMatch(length: 7, output: 1, variables: ["name": ",then"])), 70 | 71 | ([keyword, variable, keyword], "o", .possibleMatch), 72 | ([keyword, variable, keyword], "ok", .possibleMatch), 73 | ([keyword, variable, keyword], "oko", .possibleMatch), 74 | ([keyword, variable, keyword], "okok", .exactMatch(length: 4, output: 1, variables: ["name": ""])), 75 | ([keyword, variable, keyword], "okxok", .exactMatch(length: 5, output: 1, variables: ["name": "x"])), 76 | ([keyword, variable, keyword], "okokok", .exactMatch(length: 4, output: 1, variables: ["name": ""])), 77 | ([keyword, variableLongest, keyword], "ok", .possibleMatch), 78 | ([keyword, variableLongest, keyword], "okx", .possibleMatch), 79 | ([keyword, variableLongest, keyword], "oko", .possibleMatch), 80 | 81 | ([keyword, variableLongest, keyword], "okok", .possibleMatch), 82 | ([keyword, variableLongest, keyword], "okxok", .possibleMatch), 83 | 84 | ([variable, keyword, variable2], "xo", .possibleMatch), 85 | ([variable, keyword, variable2], "xok", .exactMatch(length: 3, output: 1, variables: ["name": "x", "last": ""])), 86 | ([variable, keyword, variable2], "xoky", .exactMatch(length: 4, output: 1, variables: ["name": "x", "last": "y"])), 87 | ([variable, keyword, variable2], "xoky and rest", .exactMatch(length: 13, output: 1, variables: ["name": "x", "last": "y and rest"])), 88 | ([variable, keyword, variable2], "xokoky", .exactMatch(length: 6, output: 1, variables: ["name": "x", "last": "oky"])) 89 | ] 90 | 91 | for testCase in testCases { 92 | let matcher = Pattern(testCase.elements) { _ in 1 } 93 | let result = matcher.matches(string: testCase.input, interpreter: DummyInterpreter(), context: Context()) 94 | XCTAssertTrue(result == testCase.expectedResult, "\(testCase.input) should have resulted in \(testCase.expectedResult) but got \(result) instead") 95 | } 96 | } 97 | 98 | func test_whenMatchingForwards_thenExpectingAppropriateResults() { 99 | let matcher = Pattern([Variable("lhs"), Keyword("-"), Variable("rhs")]) { 100 | guard let lhs = $0.variables["lhs"] as? Int, let rhs = $0.variables["rhs"] as? Int else { return nil } 101 | return lhs - rhs 102 | } 103 | let function = Function(patterns: [matcher]) 104 | let interpreter = TypedInterpreter(dataTypes: [DataType(type: Int.self, literals: [Literal { Int($0.value) }]) { String($0.value) }], functions: [function]) 105 | XCTAssertEqual(interpreter.evaluate("3 - 2 - 1") as! Int, 2) 106 | } 107 | 108 | func test_whenMatchingBackwards_thenExpectingAppropriateResults() { 109 | let matcher = Pattern([Variable("lhs"), Keyword("-"), Variable("rhs")], options: .backwardMatch) { 110 | guard let lhs = $0.variables["lhs"] as? Int, let rhs = $0.variables["rhs"] as? Int else { return nil } 111 | return lhs - rhs 112 | } 113 | let function = Function(patterns: [matcher]) 114 | let interpreter = TypedInterpreter(dataTypes: [DataType(type: Int.self, literals: [Literal { Int($0.value) }]) { String($0.value) }], functions: [function]) 115 | XCTAssertEqual(interpreter.evaluate("3 - 2 - 1") as! Int, 0) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Tests/EvalTests/UnitTests/TemplateInterpreterTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Eval 2 | import XCTest 3 | import class Eval.Pattern 4 | 5 | class StringTemplateInterpreterTests: XCTestCase { 6 | 7 | // MARK: init 8 | 9 | func test_whenInitialised_thenPropertiesAreSaved() { 10 | let matcher = Pattern>([Keyword("in")]) { _ in "a" } 11 | let statements = [matcher] 12 | let interpreter = TypedInterpreter() 13 | let context = Context() 14 | 15 | let stringTemplateInterpreter = StringTemplateInterpreter(statements: statements, 16 | interpreter: interpreter, 17 | context: context) 18 | 19 | XCTAssertEqual(stringTemplateInterpreter.statements.count, 1) 20 | XCTAssertTrue(statements[0] === stringTemplateInterpreter.statements[0]) 21 | XCTAssertTrue(interpreter === stringTemplateInterpreter.typedInterpreter) 22 | XCTAssertTrue(context === stringTemplateInterpreter.context) 23 | XCTAssertFalse(stringTemplateInterpreter.typedInterpreter.context === stringTemplateInterpreter.context) 24 | } 25 | 26 | func test_whenInitialised_thenTypedAndTemplateInterpreterDoNotShareTheSameContext() { 27 | let stringTemplateInterpreter = StringTemplateInterpreter(statements: [], interpreter: TypedInterpreter(), context: Context()) 28 | 29 | XCTAssertFalse(stringTemplateInterpreter.typedInterpreter.context === stringTemplateInterpreter.context) 30 | } 31 | 32 | // MARK: evaluate 33 | 34 | func test_whenEvaluates_thenTransformationHappens() { 35 | let matcher = Pattern>([Keyword("in")]) { _ in "contains" } 36 | let interpreter = StringTemplateInterpreter(statements: [matcher], 37 | interpreter: TypedInterpreter(), 38 | context: Context()) 39 | 40 | let result = interpreter.evaluate("a in b") 41 | 42 | XCTAssertEqual(result, "a contains b") 43 | } 44 | 45 | func test_whenEvaluates_thenUsesGlobalContext() { 46 | let matcher = Pattern>([Keyword("{somebody}")]) { $0.context.variables["person"] as? String } 47 | let interpreter = StringTemplateInterpreter(statements: [matcher], 48 | interpreter: TypedInterpreter(), 49 | context: Context(variables: ["person": "you"])) 50 | 51 | let result = interpreter.evaluate("{somebody} + me") 52 | 53 | XCTAssertEqual(result, "you + me") 54 | } 55 | 56 | // MARK: evaluate with context 57 | 58 | func test_whenEvaluatesWithContext_thenUsesLocalContext() { 59 | let matcher = Pattern>([Keyword("{somebody}")]) { $0.context.variables["person"] as? String } 60 | let interpreter = StringTemplateInterpreter(statements: [matcher], 61 | interpreter: TypedInterpreter(), 62 | context: Context()) 63 | 64 | let result = interpreter.evaluate("{somebody} + me", context: Context(variables: ["person": "you"])) 65 | 66 | XCTAssertEqual(result, "you + me") 67 | } 68 | 69 | func test_whenEvaluatesWithContext_thenLocalOverridesGlobalContext() { 70 | let matcher = Pattern>([Keyword("{somebody}")]) { $0.context.variables["person"] as? String } 71 | let interpreter = StringTemplateInterpreter(statements: [matcher], 72 | interpreter: TypedInterpreter(), 73 | context: Context(variables: ["person": "nobody"])) 74 | 75 | let result = interpreter.evaluate("{somebody} + me", context: Context(variables: ["person": "you"])) 76 | 77 | XCTAssertEqual(result, "you + me") 78 | } 79 | 80 | // MARK: TemplateVariable 81 | 82 | func test_whenUsingTemplateVariable_thenTransformationHappens() { 83 | let matcher = Pattern>([Keyword("{"), TemplateVariable("person"), Keyword("}")]) { _ in "you" } 84 | let interpreter = StringTemplateInterpreter(statements: [matcher], 85 | interpreter: TypedInterpreter(), 86 | context: Context()) 87 | 88 | let result = interpreter.evaluate("{somebody} + me") 89 | 90 | XCTAssertEqual(result, "you + me") 91 | } 92 | 93 | func test_whenUsingTemplateVariableWithNilResult_thenTransformationNotHappens() { 94 | let matcher = Pattern>([Keyword("{"), TemplateVariable("person"), Keyword("}")]) { _ in nil } 95 | let interpreter = StringTemplateInterpreter(statements: [matcher], 96 | interpreter: TypedInterpreter(), 97 | context: Context()) 98 | 99 | let result = interpreter.evaluate("{somebody} + me") 100 | 101 | XCTAssertEqual(result, "{somebody} + me") 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Tests/EvalTests/UnitTests/TypedInterpreterTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Eval 2 | import XCTest 3 | 4 | class TypedInterpreterTests: XCTestCase { 5 | 6 | // MARK: init 7 | 8 | func test_whenInitialised_thenPropertiesAreSaved() { 9 | let dataTypes = [DataType(type: String.self, literals: []) { $0.value }] 10 | let functions = [Function([Keyword("a")]) { _ in "a" } ] 11 | let context = Context() 12 | 13 | let interpreter = TypedInterpreter(dataTypes: dataTypes, 14 | functions: functions, 15 | context: context) 16 | 17 | XCTAssertEqual(interpreter.dataTypes.count, 1) 18 | XCTAssertTrue(dataTypes[0] === interpreter.dataTypes[0] as! DataType) 19 | 20 | XCTAssertEqual(interpreter.functions.count, 1) 21 | XCTAssertTrue(functions[0] === interpreter.functions[0] as! Function) 22 | 23 | XCTAssertTrue(context === interpreter.context) 24 | } 25 | 26 | // MARK: evaluate 27 | 28 | func test_whenEvaluates_thenTransformationHappens() { 29 | let interpreter = TypedInterpreter(dataTypes: [DataType(type: Int.self, literals: [Literal { Int($0.value) }]) { String($0.value) }], 30 | functions: [Function([Variable("lhs"), Keyword("plus"), Variable("rhs")]) { ($0.variables["lhs"] as! Int) + ($0.variables["rhs"] as! Int) } ], 31 | context: Context()) 32 | 33 | let result = interpreter.evaluate("1 plus 2") 34 | 35 | XCTAssertEqual(result as! Int, 3) 36 | } 37 | 38 | func test_whenEvaluates_thenUsesGlobalContext() { 39 | let interpreter = TypedInterpreter(dataTypes: [DataType(type: Int.self, literals: [Literal { Int($0.value) }]) { String($0.value) }], 40 | functions: [Function([Variable("lhs"), Keyword("plus"), Variable("rhs")]) { ($0.variables["lhs"] as! Int) + ($0.variables["rhs"] as! Int) } ], 41 | context: Context(variables: ["a": 2])) 42 | 43 | let result = interpreter.evaluate("1 plus a") 44 | 45 | XCTAssertEqual(result as! Int, 3) 46 | } 47 | 48 | // MARK: evaluate with context 49 | 50 | func test_whenEvaluatesWithContext_thenUsesLocalContext() { 51 | let interpreter = TypedInterpreter(dataTypes: [DataType(type: Int.self, literals: [Literal { Int($0.value) }]) { String($0.value) }], 52 | functions: [Function([Variable("lhs"), Keyword("plus"), Variable("rhs")]) { ($0.variables["lhs"] as! Int) + ($0.variables["rhs"] as! Int) } ], 53 | context: Context()) 54 | 55 | let result = interpreter.evaluate("1 plus a", context: Context(variables: ["a": 2])) 56 | 57 | XCTAssertEqual(result as! Int, 3) 58 | } 59 | 60 | func test_whenEvaluatesWithContext_thenLocalOverridesGlobalContext() { 61 | let interpreter = TypedInterpreter(dataTypes: [DataType(type: Int.self, literals: [Literal { Int($0.value) }]) { String($0.value) }], 62 | functions: [Function([Variable("lhs"), Keyword("plus"), Variable("rhs")]) { ($0.variables["lhs"] as! Int) + ($0.variables["rhs"] as! Int) } ], 63 | context: Context(variables: ["a": 1])) 64 | 65 | let result = interpreter.evaluate("1 plus a", context: Context(variables: ["a": 2])) 66 | 67 | XCTAssertEqual(result as! Int, 3) 68 | } 69 | 70 | // MARK: print 71 | 72 | func test_whenPrintingDataType_thenReturnsItsBlock() { 73 | let interpreter = TypedInterpreter(dataTypes: [DataType(type: Int.self, literals: [Literal { Int($0.value) }]) { String($0.value) }], 74 | functions: [], 75 | context: Context()) 76 | 77 | let result = interpreter.print(1) 78 | 79 | XCTAssertEqual(result, "1") 80 | } 81 | 82 | func test_whenPrintingUnknownDataType_thenReturnsDescription() { 83 | let interpreter = TypedInterpreter(dataTypes: [DataType(type: Int.self, literals: [Literal { Int($0.value) }]) { String($0.value) }], 84 | functions: [], 85 | context: Context()) 86 | 87 | let result = interpreter.print(true) 88 | 89 | XCTAssertEqual(result, true.description) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Tests/EvalTests/UnitTests/UtilTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Eval 2 | import XCTest 3 | 4 | class UtilTests: XCTestCase { 5 | 6 | // MARK: plus operator on Elements 7 | 8 | class DummyX: PatternElement, Equatable { 9 | func matches(prefix: String, options: PatternOptions) -> MatchResult { return .noMatch } 10 | static func == (lhs: DummyX, rhs: DummyX) -> Bool { return true } 11 | } 12 | 13 | class DummyY: PatternElement, Equatable { 14 | func matches(prefix: String, options: PatternOptions) -> MatchResult { return .possibleMatch } 15 | static func == (lhs: DummyY, rhs: DummyY) -> Bool { return true } 16 | } 17 | 18 | func test_whenApplyingPlusOperatorOnElements_thenItCreatedAnArray() { 19 | let element1 = DummyX() 20 | let element2 = DummyY() 21 | 22 | let result = element1 + element2 23 | 24 | XCTAssertEqual(result.count, 2) 25 | XCTAssertEqual(result[0] as! DummyX, element1) 26 | XCTAssertEqual(result[1] as! DummyY, element2) 27 | } 28 | 29 | // MARK: plus operator on Array 30 | 31 | func test_whenApplyingPlusOperatorOnArrayWithItem_thenItCreatesAMergedArray() { 32 | let result = [1, 2, 3] + 4 33 | 34 | XCTAssertEqual(result, [1, 2, 3, 4]) 35 | } 36 | 37 | func test_whenApplyingPlusEqualsOperatorOnArrayWithItem_thenItCreatesAMergedArray() { 38 | var array = [1, 2, 3] 39 | array += 4 40 | 41 | XCTAssertEqual(array, [1, 2, 3, 4]) 42 | } 43 | 44 | // MARK: String trim 45 | 46 | func test_whenTrimminString_thenRemovesWhitespaces() { 47 | XCTAssertEqual(" asd ".trim(), "asd") 48 | XCTAssertEqual(" \t asd \n ".trim(), "asd") 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/EvalTests/UnitTests/VariableProcessor.swift: -------------------------------------------------------------------------------- 1 | @testable import Eval 2 | import XCTest 3 | 4 | class VariableProcessorTests: XCTestCase { 5 | 6 | // init 7 | 8 | func test_whenInitialising_thenSetsParametersCorrectly() { 9 | let interpreter = DummyInterpreter() 10 | let context = Context() 11 | let processor = VariableProcessor(interpreter: interpreter, context: context) 12 | 13 | XCTAssertTrue(interpreter === processor.interpreter) 14 | XCTAssertTrue(context === processor.context) 15 | } 16 | 17 | // process 18 | 19 | func test_whenProcessing_thenUsesMap() { 20 | let variable: VariableValue = (metadata: GenericVariable("name") { _ in "xyz" }, value: "asd") 21 | let processor = VariableProcessor(interpreter: DummyInterpreter(), context: Context()) 22 | 23 | let result = processor.process(variable) 24 | 25 | XCTAssertEqual(result as! String, "xyz") 26 | } 27 | 28 | func test_whenProcessingAndInterpreted_thenUsesInterpreter() { 29 | let variable: VariableValue = (metadata: GenericVariable("name", options: .notTrimmed), value: "asd") 30 | let processor = VariableProcessor(interpreter: DummyInterpreter(), context: Context()) 31 | 32 | let result = processor.process(variable) 33 | 34 | XCTAssertEqual(result as! String, "a") 35 | } 36 | 37 | func test_whenProcessingAndNotTrimmed_thenDoesNotTrim() { 38 | let variable: VariableValue = (metadata: GenericVariable("name", options: [.notTrimmed, .notInterpreted]), value: " asd ") 39 | let processor = VariableProcessor(interpreter: DummyInterpreter(), context: Context()) 40 | 41 | let result = processor.process(variable) 42 | 43 | XCTAssertEqual(result as! String, " asd ") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Tests/EvalTests/UnitTests/VariableTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Eval 2 | import XCTest 3 | 4 | class VariableTests: XCTestCase { 5 | 6 | // MARK: init 7 | 8 | func test_whenInitialised_thenPropertiesAreSet() { 9 | let variable = GenericVariable("name", options: .acceptsNilValue) { _ in nil } 10 | 11 | XCTAssertEqual(variable.name, "name") 12 | XCTAssertEqual(variable.options, .acceptsNilValue) 13 | XCTAssertNotNil(variable.map) 14 | } 15 | 16 | func test_whenInitialised_thenDefaultAreSetCorrectly() { 17 | let variable = GenericVariable("name") 18 | 19 | XCTAssertEqual(variable.name, "name") 20 | XCTAssertEqual(variable.options, []) 21 | XCTAssertNotNil(variable.map) 22 | } 23 | 24 | // MARK: matches 25 | 26 | func test_whenCallingMatches_thenReturnAny() { 27 | let variable = GenericVariable("name") 28 | 29 | XCTAssertTrue(variable.matches(prefix: "asd").isAnyMatch()) 30 | } 31 | 32 | // MARK: mapped 33 | 34 | func test_whenCallingMatched_thenCreatesANewVariable() { 35 | let variable = GenericVariable("name") 36 | let result = variable.mapped { Double($0) } 37 | 38 | XCTAssertNotNil(result) 39 | XCTAssertEqual(variable.name, result.name) 40 | XCTAssertEqual(variable.options, result.options) 41 | XCTAssertEqual(result.performMap(input: "1", interpreter: TypedInterpreter()) as! Double, 1) 42 | } 43 | 44 | // MARK: performMap 45 | 46 | func test_whenCallingPerformMap_thenUsesMapClosure() { 47 | let variable = GenericVariable("name") { _ in 123 } 48 | let result = variable.performMap(input: 1, interpreter: DummyInterpreter()) 49 | 50 | XCTAssertEqual(result as! Int, 123) 51 | } 52 | 53 | // MARK: matches performance 54 | 55 | func test_whenCallingMatchesWithShortInput_thenPerformsEffectively() { 56 | let variable = GenericVariable("name") 57 | 58 | XCTAssertTrue(variable.matches(prefix: "asd").isAnyMatch()) 59 | } 60 | 61 | func test_whenCallingMatchesWithLargeInput_thenPerformsEffectively() { 62 | let variable = GenericVariable("name") 63 | 64 | XCTAssertTrue(variable.matches(prefix: """ 65 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. 66 | 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. 67 | It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. 68 | 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. 69 | """).isAnyMatch()) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Tests/EvalTests/Utils.swift: -------------------------------------------------------------------------------- 1 | import Eval 2 | import Foundation 3 | 4 | class DummyInterpreter: Interpreter { 5 | typealias VariableEvaluator = DummyInterpreter 6 | typealias EvaluatedType = String 7 | 8 | var context: Context 9 | var interpreterForEvaluatingVariables: DummyInterpreter { return self } 10 | 11 | func evaluate(_ expression: String) -> String { return "a" } 12 | func evaluate(_ expression: String, context: Context) -> String { return "a" } 13 | init() { context = Context() } 14 | } 15 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | @testable import InterpreterTests 2 | import XCTest 3 | 4 | XCTMain([ 5 | testCase(InterpreterTests.allTests) 6 | ]) 7 | -------------------------------------------------------------------------------- /github_rsa.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tevelee/Eval/df80bd880d561f1f12bdbac087f835bb13916d5e/github_rsa.enc --------------------------------------------------------------------------------