├── .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("\(name)>")]) {
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
--------------------------------------------------------------------------------