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 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
@@ -0,0 +1,8 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.0
3
+
4
+ Style/StringLiterals:
5
+ EnforcedStyle: double_quotes
6
+
7
+ Style/StringLiteralsInInterpolation:
8
+ EnforcedStyle: double_quotes
data/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-03-27
4
+ - Initial release
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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PortableExpressions
4
+ VERSION = "0.1.0"
5
+ 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: []