portable_expressions 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +304 -0
- data/Rakefile +12 -0
- data/lib/portable_expressions/environment.rb +49 -0
- data/lib/portable_expressions/evaluator.rb +15 -0
- data/lib/portable_expressions/expression.rb +55 -0
- data/lib/portable_expressions/modules/serializable.rb +19 -0
- data/lib/portable_expressions/scalar.rb +25 -0
- data/lib/portable_expressions/variable.rb +26 -0
- data/lib/portable_expressions/version.rb +5 -0
- data/lib/portable_expressions.rb +47 -0
- metadata +87 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d220e43573965935526913968ea27b528aed8eda49f98411f532b9f8c6d134da
|
4
|
+
data.tar.gz: 87bacf2336c7bee890f8dedb7a14db1d79bd87d4678acc33be9a79cf93cd76e8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 64009910d3f1eab111f64c7c211ed02b4e3a27c94108bcc7cf8f98b7f8548255a1215185a46b0a70c021e18d8b6301dfcccbe3af625764258fe7e0515a2c8783
|
7
|
+
data.tar.gz: eeda6228dbdd334fef7e3f87089e910073e3405d77f0bb426f7e6a56390a89bb0faff70ea2a38a4fe924087fe7f88adbc23e6d0dd1f7177a0ae918e2f5fc8b56
|
data/.rubocop.yml
ADDED
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 Omkar Moghe
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,304 @@
|
|
1
|
+
# PortableExpressions 🍱
|
2
|
+
|
3
|
+
A simple and flexible pure Ruby library for building and evaluating expressions. Expressions can be serialized to and built from JSON strings for portability.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Install the gem and add to the application's Gemfile by executing:
|
8
|
+
|
9
|
+
`bundle add portable_expressions`
|
10
|
+
|
11
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
12
|
+
|
13
|
+
`gem install portable_expressions`
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
> [!IMPORTANT]
|
18
|
+
> When using the gem, all references to the models below must be prefixed with `PortableExpressions::`. This is omitted in the README for simplicity.
|
19
|
+
|
20
|
+
### Scalar
|
21
|
+
|
22
|
+
A `Scalar` is the simplest object that can be evaluated. It holds a single `value`. When used in an `Expression`, this `value` must respond to the symbol (i.e. support the method) defined by the `Expression#operator`.
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
Scalar.new(1)
|
26
|
+
Scalar.new("some string")
|
27
|
+
```
|
28
|
+
|
29
|
+
### Variable
|
30
|
+
|
31
|
+
A `Variable` represents a named value stored in the `Environment`. Unlike `Scalars`, `Variables` have no value until they are evaluated by an `Environment`. Evaluating a `Variable` that isn't present in the `Environment` will result in a `MissingVariableError`.
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
variable_a = Variable.new("variable_a")
|
35
|
+
variable_b = Variable.new("variable_b")
|
36
|
+
|
37
|
+
environment = Environment.new(
|
38
|
+
"variable_a" => 1
|
39
|
+
)
|
40
|
+
environment.evaluate(variable_a) #=> 1
|
41
|
+
environment.evaluate(variable_b) #=> MissingVariableError
|
42
|
+
```
|
43
|
+
|
44
|
+
### Expression
|
45
|
+
|
46
|
+
An expression represents 2 or more `operands` that are reduced using a defined `operator`. The `operands` of an `Expression` can be `Scalars`, `Variables`, or other `Expressions`. All `operands` must respond to the symbol (i.e. support the method) defined by the `Expression#operator`. Just like `Variables`, `Expressions` have non value until they're evaluated by an `Environment`.
|
47
|
+
|
48
|
+
Evaluating an `Expression` does the following:
|
49
|
+
1. all `operands` are first evaluated in order
|
50
|
+
1. all resulting _values_ are reduced using the symbol defined by the `operator`
|
51
|
+
|
52
|
+
In this way evaluation is "lazy"; it won't evaluate a `Variable` or `Expression` until the `operand` is about to be used.
|
53
|
+
|
54
|
+
An `Expression` can store its result back into the `Environment` by defining an `output`.
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
# addition
|
58
|
+
addition = Expression.new(:+, Scalar.new(1), Scalar.new(2))
|
59
|
+
|
60
|
+
# multiplication
|
61
|
+
multiplication = Expression.new(:*, Variable.new("variable_a"), Scalar.new(2))
|
62
|
+
|
63
|
+
# storing output
|
64
|
+
storing_output = Expression.new(:+, Scalar.new(1), Scalar.new(2), output: "one_plus_two")
|
65
|
+
|
66
|
+
environment = Environment.new(
|
67
|
+
"variable_a" => 2
|
68
|
+
)
|
69
|
+
environment.evaluate(addition) #=> 3
|
70
|
+
environment.evaluate(multiplication) #=> 4
|
71
|
+
environment.evaluate(storing_output) #=> 3
|
72
|
+
|
73
|
+
environment.variables
|
74
|
+
#=> { "variable_a" => 2, "one_plus_two" => 3 }
|
75
|
+
```
|
76
|
+
|
77
|
+
#### Special `operators`
|
78
|
+
|
79
|
+
Some operators, like logical `&&` and `||` are not methods in Ruby, so we pass a special string/symbol that PortableExpressions understands.
|
80
|
+
- `&&` is represented by `:and`
|
81
|
+
- `||` is represented by `:or`
|
82
|
+
|
83
|
+
### Environment
|
84
|
+
|
85
|
+
The `Environment` holds state in the form of a `variables` hash and can evaluate `Expressions`, `Scalars`, and `Variables` within a context. The environment handles updates to the state as `Expressions` run.
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
environment = Environment.new(
|
89
|
+
"variable_a" => 1,
|
90
|
+
"variable_b" => 2,
|
91
|
+
)
|
92
|
+
|
93
|
+
environment.evaluate(Variable.new("variable_a"))
|
94
|
+
#=> 1
|
95
|
+
environment.evaluate(Variable.new("variable_c"))
|
96
|
+
#=> MissingVariableError "Environment missing variable variable_c."
|
97
|
+
|
98
|
+
environment.evaluate(
|
99
|
+
Expression.new(
|
100
|
+
:+,
|
101
|
+
Variable.new("variable_a"),
|
102
|
+
Variable.new("variable_b"),
|
103
|
+
output: "variable_c" # defines where to store the result value
|
104
|
+
)
|
105
|
+
)
|
106
|
+
#=> 3
|
107
|
+
|
108
|
+
environment.variables
|
109
|
+
#=> { "variable_a" => 1, "variable_b" => 2, "variable_c" => 3 }
|
110
|
+
```
|
111
|
+
|
112
|
+
When evaluating multiple objects at a time, the value of the **last** object will be returned.
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
environment = Environment.new
|
116
|
+
environment.evaluate(
|
117
|
+
Scalar.new(1),
|
118
|
+
Expression.new(:+, Scalar.new(1), Scalar.new(2))
|
119
|
+
)
|
120
|
+
#=> 3
|
121
|
+
```
|
122
|
+
|
123
|
+
You can update or modify the `variables` hash directly at any time.
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
environment = Environment.new(
|
127
|
+
"variable_a" => 1
|
128
|
+
)
|
129
|
+
|
130
|
+
environment.evaluate(Variable.new("variable_a")) # => 1
|
131
|
+
environment.variables["variable_a"] = 2
|
132
|
+
environment.evaluate(Variable.new("variable_a")) # => 2
|
133
|
+
```
|
134
|
+
|
135
|
+
### Serialization (to JSON)
|
136
|
+
|
137
|
+
All models including the `Environment` support serialization via:
|
138
|
+
- `as_json`: builds a serializable `Hash` representation of the object
|
139
|
+
- `to_json`: builds a JSON `String` representing the object
|
140
|
+
|
141
|
+
All models have a **required** `object` key that indicates the type of object.
|
142
|
+
|
143
|
+
### Building (from JSON)
|
144
|
+
|
145
|
+
To parse a JSON string, use the `PortableExpressions.from_json` method.
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
environment_json = <<~JSON
|
149
|
+
{
|
150
|
+
"object": "PortableExpressions::Environment",
|
151
|
+
"variables": {
|
152
|
+
"score_a": 100
|
153
|
+
}
|
154
|
+
}
|
155
|
+
JSON
|
156
|
+
variable_json = <<~JSON
|
157
|
+
{
|
158
|
+
"object": "PortableExpressions::Variable",
|
159
|
+
"name": "score_a"
|
160
|
+
}
|
161
|
+
JSON
|
162
|
+
|
163
|
+
environment = PortableExpressions.from_json(environment_json)
|
164
|
+
variable_score_a = PortableExpressions.from_json(variable_json)
|
165
|
+
environment.evaluate(variable_score_a) #=> 100
|
166
|
+
```
|
167
|
+
|
168
|
+
### Beyond math
|
169
|
+
|
170
|
+
The examples throughout the README show simple arithmetic to illustrate the mechanics of the library. However, `Scalars` and `Variables` can hold any type of value that's JSON serializable, which allows for more complex use cases such as:
|
171
|
+
|
172
|
+
#### Logical statements
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
# variable_a > variable_b && variable_c
|
176
|
+
a_greater_than_b = Expression.new(
|
177
|
+
:>,
|
178
|
+
Variable.new("variable_a"),
|
179
|
+
Variable.new("variable_b"),
|
180
|
+
)
|
181
|
+
conditional = Expression.new(
|
182
|
+
:and,
|
183
|
+
a_greater_than_b,
|
184
|
+
Variable.new("variable_c"),
|
185
|
+
)
|
186
|
+
Environment.new(
|
187
|
+
"variable_a" => 2,
|
188
|
+
"variable_b" => 1,
|
189
|
+
"variable_c" => "truthy",
|
190
|
+
).evaluate(conditional)
|
191
|
+
#=> true
|
192
|
+
```
|
193
|
+
|
194
|
+
> [!TIP]
|
195
|
+
> Some operators have special symbols, see [special operators](#special-operators) for more details.
|
196
|
+
|
197
|
+
#### String manipulation
|
198
|
+
|
199
|
+
```ruby
|
200
|
+
# Define a reusable `Expression` using `Variables`.
|
201
|
+
repeat_count = Variable.new("repeat")
|
202
|
+
string_to_repeat = Variable.new("user_input")
|
203
|
+
repeater = Expression.new(:*, string_to_repeat, repeat_count)
|
204
|
+
|
205
|
+
# Get inputs from some HTTP controller (e.g. Rails)
|
206
|
+
|
207
|
+
# GET /repeater?repeat=3&user_input=cool
|
208
|
+
Environment.new(**params).evaluate(repeater) #=> "coolcoolcool"
|
209
|
+
# GET /repeater?repeat=3&user_input=alright
|
210
|
+
Environment.new(**params).evaluate(repeater) #=> "alrightalrightalright"
|
211
|
+
```
|
212
|
+
|
213
|
+
#### Authorization policies
|
214
|
+
|
215
|
+
First, we define a portable and reusable policies.
|
216
|
+
|
217
|
+
```ruby
|
218
|
+
# This is a composable policy that checks if a user has permissions for a requested resource and action.
|
219
|
+
user_permissions = Variable.new("user_permissions")
|
220
|
+
resource = Variable.new("resource")
|
221
|
+
action = Variable.new("action")
|
222
|
+
requested_permission = Expression.new(:+, resource, Scalar.new("."), action)
|
223
|
+
user_has_permission = Expression.new(:include?, user_permissions, requested_permission, output: "user_has_permission")
|
224
|
+
|
225
|
+
# Another composable policy that checks if the resource belongs to a user.
|
226
|
+
resource_owner = Variable.new("resource_owner")
|
227
|
+
user_id = Variable.new("user_id")
|
228
|
+
user_owns_resource = Expression.new(:==, resource_owner, user_id, output: "user_owns_resource")
|
229
|
+
```
|
230
|
+
|
231
|
+
We might decide to combine the policies into a single one:
|
232
|
+
|
233
|
+
```ruby
|
234
|
+
user_owns_resource_and_has_permission = Expression.new(:and, user_owns_resource, user_has_permission)
|
235
|
+
|
236
|
+
# Write to a JSON file
|
237
|
+
File.write("user_owns_resource_and_has_permission.json", user_owns_resource_and_has_permission.to_json)
|
238
|
+
```
|
239
|
+
|
240
|
+
Or we might define a policy the relies on the `output` of other policies. This means that the `Environment` must run the dependencies first in order for their `output` to be available in the `Environment#variables`.
|
241
|
+
|
242
|
+
```ruby
|
243
|
+
user_owns_resource_and_has_permission = Expression.new(
|
244
|
+
:and,
|
245
|
+
Variable.new("user_owns_resource"),
|
246
|
+
Variable.new("user_has_permission")
|
247
|
+
)
|
248
|
+
|
249
|
+
# Each of these can be individually run
|
250
|
+
File.write("user_has_permission.json", user_has_permission.to_json)
|
251
|
+
File.write("user_owns_resource.json", user_owns_resource.to_json)
|
252
|
+
# This one relies on the previous 2 being run, or the corresponding variables being set in the `Environment`.
|
253
|
+
File.write("user_owns_resource_and_has_permission.json", user_owns_resource_and_has_permission.to_json)
|
254
|
+
```
|
255
|
+
|
256
|
+
These examples demonstrate portability via JSON files, but we can just as easily serve the policy directly to anyone who needs it via some HTTP controller:
|
257
|
+
|
258
|
+
```ruby
|
259
|
+
# E.g. Rails via an `ActionController`
|
260
|
+
render json: user_owns_resource_and_has_permission.as_json, :ok
|
261
|
+
|
262
|
+
# Elsewhere, in the requesting service
|
263
|
+
user_owns_resource_and_has_permission = PortableExpressions.from_json(response.body.to_s)
|
264
|
+
```
|
265
|
+
|
266
|
+
Then, some consumer with access to the user's permissions and context around the requested `resource` and `action` can execute the policy.
|
267
|
+
|
268
|
+
```ruby
|
269
|
+
environment = Environment.new(
|
270
|
+
"user_permissions" => user.permissions #=> ["blog.read", "blog.write", "comment.read", "comment.write"]
|
271
|
+
"resource" => some_model.resource_name #=> "comment"
|
272
|
+
"action" => "read"
|
273
|
+
"resource_owner" => some_model.user_id
|
274
|
+
"user_id" => user.id
|
275
|
+
)
|
276
|
+
|
277
|
+
# Combined policy
|
278
|
+
user_owns_resource_and_has_permission = PortableExpressions.from_json(
|
279
|
+
File.read("user_owns_resource_and_has_permission.json")
|
280
|
+
)
|
281
|
+
environment.evaluate(user_owns_resource_and_has_permission) #=> true
|
282
|
+
|
283
|
+
# Individual policies
|
284
|
+
user_has_permission = PortableExpressions.from_json(File.read("user_has_permission.json"))
|
285
|
+
user_owns_resource = PortableExpressions.from_json(File.read("user_owns_resource.json"))
|
286
|
+
user_owns_resource_and_has_permission = PortableExpressions.from_json(
|
287
|
+
File.read("user_owns_resource_and_has_permission.json")
|
288
|
+
)
|
289
|
+
environment.evaluate(user_has_permission, user_owns_resource, user_owns_resource_and_has_permission) #=> true
|
290
|
+
```
|
291
|
+
|
292
|
+
## Development
|
293
|
+
|
294
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
295
|
+
|
296
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
297
|
+
|
298
|
+
## Contributing
|
299
|
+
|
300
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/omkarmoghe/portable_expressions.
|
301
|
+
|
302
|
+
## License
|
303
|
+
|
304
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PortableExpressions
|
4
|
+
# The `Environment` holds state in the form of a `variables` hash and can evaluate `Expressions`, `Scalars`, and
|
5
|
+
# `Variables` within a context.
|
6
|
+
class Environment
|
7
|
+
include Serializable
|
8
|
+
|
9
|
+
attr_reader :variables
|
10
|
+
|
11
|
+
# @param variables [Hash]
|
12
|
+
def initialize(**variables)
|
13
|
+
@variables = variables
|
14
|
+
end
|
15
|
+
|
16
|
+
# Evaluates each object. Returns the value of the last object
|
17
|
+
# @param objects [Expression, Variable, Scalar] 1 or more object to evaluate.
|
18
|
+
def evaluate(*objects)
|
19
|
+
objects.map { |object| evaluate_one(object) }.last
|
20
|
+
end
|
21
|
+
|
22
|
+
def as_json
|
23
|
+
super.merge(
|
24
|
+
variables: variables
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def evaluate_one(object) # rubocop:disable Metrics/MethodLength
|
31
|
+
case object
|
32
|
+
when Scalar
|
33
|
+
object.value
|
34
|
+
when Variable
|
35
|
+
variables.fetch(object.name) do |key|
|
36
|
+
raise MissingVariableError, "Environment missing variable #{key}."
|
37
|
+
end
|
38
|
+
when Expression
|
39
|
+
value = object.operands
|
40
|
+
.map { |operand| Evaluator.new(evaluate(operand)) }
|
41
|
+
.reduce(object.operator)
|
42
|
+
|
43
|
+
variables[object.output] = value if object.output
|
44
|
+
|
45
|
+
value
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PortableExpressions
|
4
|
+
# Used to wrap `operands` when evaluating an `Expression`. This allows us to "extend" the functionality of an object
|
5
|
+
# without polluting the app wide definition.
|
6
|
+
class Evaluator < SimpleDelegator
|
7
|
+
def and(other)
|
8
|
+
__getobj__ && other.__getobj__
|
9
|
+
end
|
10
|
+
|
11
|
+
def or(other)
|
12
|
+
__getobj__ || other.__getobj__
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PortableExpressions
|
4
|
+
# An expression represents 2 or more `operands` that are reduced using a defined `operator`. The `operands` of an
|
5
|
+
# `Expression` can be `Scalars`, `Variables`, or other `Expressions`. All `operands` must respond to the symbol (i.e.
|
6
|
+
# support the method) defined by the `Expression#operator`.
|
7
|
+
class Expression
|
8
|
+
include Serializable
|
9
|
+
|
10
|
+
ALLOWED_OPERANDS = [
|
11
|
+
Expression,
|
12
|
+
Scalar,
|
13
|
+
Variable
|
14
|
+
].freeze
|
15
|
+
|
16
|
+
attr_reader :operator, :operands
|
17
|
+
attr_accessor :output # Sometimes you may want to conditionally set an `output` after initialization.
|
18
|
+
|
19
|
+
# @param operator [String, Symbol] Mathematical operator to `reduce` the `operands` array with.
|
20
|
+
# @param *operands [Variable, Expressions] 2 or more Variables, Scalars, or Expressions
|
21
|
+
# @param output [String] The variable to write the expressions output to
|
22
|
+
def initialize(operator, *operands, output: nil)
|
23
|
+
@operator = operator.to_sym
|
24
|
+
@operands = operands
|
25
|
+
@output = output
|
26
|
+
|
27
|
+
validate!
|
28
|
+
end
|
29
|
+
|
30
|
+
# TODO(@omkarmoghe): This string representation might not make the most sense for non-math expressions.
|
31
|
+
def to_s
|
32
|
+
"(#{operands.join(" #{operator} ")})"
|
33
|
+
end
|
34
|
+
|
35
|
+
def as_json
|
36
|
+
super.merge(
|
37
|
+
operator: operator.to_s,
|
38
|
+
operands: operands.map(&:as_json),
|
39
|
+
output: output
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def validate!
|
46
|
+
raise InvalidOperandError, "Must provide 2 or more operands." unless operands.length >= 2
|
47
|
+
|
48
|
+
unless (operands.map(&:class) - ALLOWED_OPERANDS).empty?
|
49
|
+
raise InvalidOperandError, "Operands must be one of #{ALLOWED_OPERANDS.inspect}."
|
50
|
+
end
|
51
|
+
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PortableExpressions
|
4
|
+
module Serializable
|
5
|
+
def as_json
|
6
|
+
{
|
7
|
+
object: self.class.name
|
8
|
+
}
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_json(pretty: false)
|
12
|
+
if pretty
|
13
|
+
JSON.pretty_generate(as_json)
|
14
|
+
else
|
15
|
+
JSON.generate(as_json)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PortableExpressions
|
4
|
+
# A `Scalar` is the simplest object that can be evaluated. It holds a single `value`. When used in an `Expression`,
|
5
|
+
# this `value` must respond to the symbol (i.e. support the method) defined by the `Expression#operator`.
|
6
|
+
class Scalar
|
7
|
+
include Serializable
|
8
|
+
|
9
|
+
attr_reader :value
|
10
|
+
|
11
|
+
def initialize(value)
|
12
|
+
@value = value
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
value.to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
def as_json
|
20
|
+
super.merge(
|
21
|
+
value: value
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PortableExpressions
|
4
|
+
# A `Variable` represents a named value stored in the `Environment`. Unlike `Scalars`, `Variables` have no value
|
5
|
+
# until they are evaluated by an `Environment`. Evaluating a `Variable` that isn't present in the `Environment` will
|
6
|
+
# result in a `MissingVariableError`.
|
7
|
+
class Variable
|
8
|
+
include Serializable
|
9
|
+
|
10
|
+
attr_reader :name
|
11
|
+
|
12
|
+
def initialize(name)
|
13
|
+
@name = name
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
name
|
18
|
+
end
|
19
|
+
|
20
|
+
def as_json
|
21
|
+
super.merge(
|
22
|
+
name: name
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Ruby lib
|
4
|
+
require "delegate"
|
5
|
+
require "json"
|
6
|
+
|
7
|
+
require_relative "portable_expressions/version"
|
8
|
+
require_relative "portable_expressions/modules/serializable"
|
9
|
+
require_relative "portable_expressions/evaluator"
|
10
|
+
require_relative "portable_expressions/scalar"
|
11
|
+
require_relative "portable_expressions/variable"
|
12
|
+
require_relative "portable_expressions/expression"
|
13
|
+
require_relative "portable_expressions/environment"
|
14
|
+
|
15
|
+
module PortableExpressions
|
16
|
+
Error = Class.new(StandardError)
|
17
|
+
|
18
|
+
DeserializationError = Class.new(Error)
|
19
|
+
InvalidOperandError = Class.new(Error)
|
20
|
+
InvalidOperatorError = Class.new(Error)
|
21
|
+
MissingVariableError = Class.new(Error)
|
22
|
+
|
23
|
+
# @param json [String, Hash]
|
24
|
+
# @return [Expression, Scalar, Variable, Environment]
|
25
|
+
def self.from_json(json)
|
26
|
+
json = JSON.parse(json) if json.is_a?(String)
|
27
|
+
|
28
|
+
case json["object"]
|
29
|
+
when Environment.name
|
30
|
+
Environment.new(**json["variables"])
|
31
|
+
when Expression.name
|
32
|
+
operator = json["operator"].to_sym
|
33
|
+
operands_json = json["operands"]
|
34
|
+
operands = operands_json.map { |operand_json| from_json(operand_json) }
|
35
|
+
|
36
|
+
Expression.new(operator, *operands)
|
37
|
+
when Variable.name
|
38
|
+
Variable.new(json["name"])
|
39
|
+
when Scalar.name
|
40
|
+
Scalar.new(json["value"])
|
41
|
+
else
|
42
|
+
raise DeserializationError, "Object class #{json["object"]} does not support deserialization."
|
43
|
+
end
|
44
|
+
rescue JSON::ParserError => e
|
45
|
+
raise DeserializationError, "Unable to parse JSON: #{e.message}."
|
46
|
+
end
|
47
|
+
end
|
metadata
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: portable_expressions
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Omkar Moghe
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-03-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: minitest
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.16'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.16'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rubocop
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.21'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.21'
|
41
|
+
description:
|
42
|
+
email:
|
43
|
+
- yo@omkr.dev
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- ".rubocop.yml"
|
49
|
+
- CHANGELOG.md
|
50
|
+
- LICENSE.txt
|
51
|
+
- README.md
|
52
|
+
- Rakefile
|
53
|
+
- lib/portable_expressions.rb
|
54
|
+
- lib/portable_expressions/environment.rb
|
55
|
+
- lib/portable_expressions/evaluator.rb
|
56
|
+
- lib/portable_expressions/expression.rb
|
57
|
+
- lib/portable_expressions/modules/serializable.rb
|
58
|
+
- lib/portable_expressions/scalar.rb
|
59
|
+
- lib/portable_expressions/variable.rb
|
60
|
+
- lib/portable_expressions/version.rb
|
61
|
+
homepage: https://github.com/omkarmoghe/portable_expressions
|
62
|
+
licenses:
|
63
|
+
- MIT
|
64
|
+
metadata:
|
65
|
+
homepage_uri: https://github.com/omkarmoghe/portable_expressions
|
66
|
+
source_code_uri: https://github.com/omkarmoghe/portable_expressions
|
67
|
+
changelog_uri: https://github.com/omkarmoghe/portable_expressions/blob/main/CHANGELOG.md
|
68
|
+
post_install_message:
|
69
|
+
rdoc_options: []
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 3.0.0
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
requirements: []
|
83
|
+
rubygems_version: 3.4.1
|
84
|
+
signing_key:
|
85
|
+
specification_version: 4
|
86
|
+
summary: A simple and flexible pure Ruby library for building and evaluating expressions.
|
87
|
+
test_files: []
|