json_logic_humanizable 0.0.1.beta1
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/CHANGELOG.md +6 -0
- data/CONTRIBUTION.md +28 -0
- data/LICENSE +21 -0
- data/README.md +139 -0
- data/lib/json_logic/config.rb +24 -0
- data/lib/json_logic/humanizable.rb +103 -0
- data/lib/json_logic/rule.rb +33 -0
- data/lib/json_logic/version.rb +5 -0
- data/lib/json_logic.rb +6 -0
- metadata +52 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9b3d6d686007b6c7b9677f087f792802a19756964a3b2b234da07765d3c2623f
|
|
4
|
+
data.tar.gz: 449bddb8f9471d7f8090f9c0be0a8331ee224de27b927b039224c060d5d0b60a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5050c96999ce0016559b7a73f9b34ffef9b4bd2af8414b7145158c65536664c1a9c82d43439a1744167b41869c33cd150ece438abe654df6e70af75f5277ef66
|
|
7
|
+
data.tar.gz: 8b006202a2b17a4b48cbb08612a31b98d2e59c7adcfe9491c1ec6dd8f0e20f3a961e5804fb2ee0fe5cf6f1ab41f52f2d16841635600e789b6b39f661c21a6369
|
data/CHANGELOG.md
ADDED
data/CONTRIBUTION.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Contribution Guide
|
|
2
|
+
|
|
3
|
+
**Contributions are very welcome!** Whether it's a small typo fix, a new operator label, a better example, or a larger refactor — your help makes this gem better. If you're unsure where to start, open an issue and we can figure it out together.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
- Ruby 2.7+
|
|
7
|
+
- No runtime dependencies
|
|
8
|
+
|
|
9
|
+
## How to contribute
|
|
10
|
+
1. Fork the repo and create a branch from `main`
|
|
11
|
+
2. Make your change (code, docs, or tests)
|
|
12
|
+
3. Include examples for any new operators or pretty mappings
|
|
13
|
+
4. Update `README.md` if public behavior changes
|
|
14
|
+
5. Run your tests
|
|
15
|
+
6. Open a Pull Request and describe:
|
|
16
|
+
- What changed and why
|
|
17
|
+
- Any breaking impacts
|
|
18
|
+
- Before/after output if applicable
|
|
19
|
+
|
|
20
|
+
## Code style
|
|
21
|
+
- Plain Ruby
|
|
22
|
+
- Public API stays stable:
|
|
23
|
+
- `JsonLogic::Rule.new(logic).humanize`
|
|
24
|
+
- `JsonLogic::Humanizable` mixin + `humanize_json_logic`
|
|
25
|
+
|
|
26
|
+
## Releases
|
|
27
|
+
- Bump `lib/json_logic/version.rb` using SemVer
|
|
28
|
+
- Update `CHANGELOG.md`
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
# json_logic_humanizable
|
|
4
|
+
|
|
5
|
+
**Translate JsonLogic rules into readable sentences**. Extension for [JsonLogic](https://jsonlogic.com/).
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## What
|
|
9
|
+
|
|
10
|
+
This gem converts JsonLogic Rules into readable sentences. It is framework-agnostic.
|
|
11
|
+
- A mixin for serializer/presenter classes.
|
|
12
|
+
- A small wrapper for one-off translations.
|
|
13
|
+
|
|
14
|
+
No dependencies. Works with a Ruby Hash or a Ruby Json string.
|
|
15
|
+
|
|
16
|
+
## How
|
|
17
|
+
|
|
18
|
+
Input: a JsonLogic rule (as a Hash or JSON string).
|
|
19
|
+
Output: human-readable text that explains the JsonLogic Rule.
|
|
20
|
+
|
|
21
|
+
Where this is useful:
|
|
22
|
+
|
|
23
|
+
If you found this gem, you likely already know where to use it in your app — the abstract use case is simple: you want to read a rule as text (e.g., show it in the UI, preview it, or share it elsewhere).
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
Add to your `Gemfile` if you need:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
gem "json_logic_humanizable"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then install:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
bundle install
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Examples
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
### Easy – Quick Start
|
|
43
|
+
```ruby
|
|
44
|
+
logic = {
|
|
45
|
+
"and" => [
|
|
46
|
+
{ ">=" => [ { "var" => "payment.amount" }, 50 ] },
|
|
47
|
+
{ "in" => [ { "var" => "payment.currency" }, %w[EUR USD] ] }
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
puts JsonLogic::Rule.new(logic).humanize
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Complex – With Config
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
|
|
58
|
+
JsonLogic::Config.operators["in"] = "is one of"
|
|
59
|
+
|
|
60
|
+
JsonLogic::Config.vars[/([^\.]+)$/] = ->(m) { m[1].tr("_", " ").split.map(&:capitalize).join(" ") }
|
|
61
|
+
|
|
62
|
+
logic = {
|
|
63
|
+
"and" => [
|
|
64
|
+
{ ">=" => [ { "var" => "payment.amount" }, 50 ] },
|
|
65
|
+
{ "fact" => "payment.currency", "operator" => "in", "value" => %w[EUR USD] },
|
|
66
|
+
{
|
|
67
|
+
"or" => [
|
|
68
|
+
{ "in" => [ { "var" => "customer.country" }, %w[LT LV EE] ] },
|
|
69
|
+
{ "==" => [ { "var" => "customer.vip" }, true ] }
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{ "!" => { "==" => [ { "var" => "customer.blacklisted" }, true ] } },
|
|
73
|
+
{ "<=" => [ { "var" => "risk.score" }, 300 ] },
|
|
74
|
+
{ "fact" => "chargeback.history_count", "operator" => "<=", "value" => 1 }
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
puts JsonLogic::Rule.new(logic).humanize
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Example output:
|
|
82
|
+
```
|
|
83
|
+
Amount is greater than or equal to 50 AND Currency is one of ["EUR", "USD"] AND Country is one of LT, LV, EE OR Vip is equal to true AND NOT (Blacklisted is equal to true) AND Score is less than or equal to 300 AND History Count is less than or equal to 1
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Serializer example
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
class RuleSerializer
|
|
90
|
+
include JsonLogic::Humanizable
|
|
91
|
+
|
|
92
|
+
attr_reader :condition
|
|
93
|
+
humanize_json_logic :condition
|
|
94
|
+
|
|
95
|
+
def initialize(condition)
|
|
96
|
+
@condition = condition
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
s = RuleSerializer.new({
|
|
101
|
+
"and" => [
|
|
102
|
+
{ ">=" => [ { "var" => "payment.amount" }, 50 ] },
|
|
103
|
+
{ "in" => [ { "var" => "payment.currency" }, %w[EUR USD] ] }
|
|
104
|
+
]
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
# RuleSerializer includes methods:
|
|
108
|
+
s.condition
|
|
109
|
+
s.condition_human
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Configuration
|
|
113
|
+
|
|
114
|
+
### Operators mapping
|
|
115
|
+
|
|
116
|
+
Configure labels as you prefer:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
JsonLogic::Config.operators["in"] = "is one of"
|
|
120
|
+
JsonLogic::Config.operators["=="] = "equals"
|
|
121
|
+
JsonLogic::Config.operators[">="] = "is at least"
|
|
122
|
+
JsonLogic::Config.operators["!"] = "NOT"
|
|
123
|
+
JsonLogic::Config.operators["and"] = "AND"
|
|
124
|
+
JsonLogic::Config.operators["or"] = "OR"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Variables mapping
|
|
128
|
+
|
|
129
|
+
Each entry is `[pattern, replacement]` where `pattern` is a `Regexp` or exact `String`, and `replacement` is a `String` or `Proc`.
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
JsonLogic::Config.vars[/([^\.]+)$/] = ->(m) { m[1].tr("_", " ").split.map(&:capitalize).join(" ") }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
This maps `payment.amount` → `Amount`, `customer.country` → `Country`, etc.
|
|
136
|
+
|
|
137
|
+
## Links
|
|
138
|
+
|
|
139
|
+
- JsonLogic site: https://jsonlogic.com/
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JsonLogic
|
|
4
|
+
module Config
|
|
5
|
+
class << self
|
|
6
|
+
attr_accessor :vars, :operators
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
Config.vars = {}
|
|
11
|
+
|
|
12
|
+
Config.operators = {
|
|
13
|
+
"==" => "is equal to",
|
|
14
|
+
"!=" => "is not equal to",
|
|
15
|
+
">" => "is greater than",
|
|
16
|
+
"<" => "is less than",
|
|
17
|
+
">=" => "is greater than or equal to",
|
|
18
|
+
"<=" => "is less than or equal to",
|
|
19
|
+
"in" => "is in",
|
|
20
|
+
"!" => "NOT",
|
|
21
|
+
"and"=> "AND",
|
|
22
|
+
"or" => "OR"
|
|
23
|
+
}
|
|
24
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JsonLogic
|
|
4
|
+
module Humanizable
|
|
5
|
+
def self.included(base)
|
|
6
|
+
base.extend ClassMethods
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module ClassMethods
|
|
10
|
+
def humanize_json_logic(*attrs, as: nil, suffix: "_human")
|
|
11
|
+
attrs.each do |attr|
|
|
12
|
+
method_name = (as || "#{attr}#{suffix}").to_sym
|
|
13
|
+
define_method(method_name) do
|
|
14
|
+
value = public_send(attr)
|
|
15
|
+
humanize_logic(value)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def humanize_logic(value)
|
|
22
|
+
return nil if value.nil?
|
|
23
|
+
obj = value.is_a?(String) ? parse_json(value) : value
|
|
24
|
+
explain(obj)
|
|
25
|
+
rescue JSON::ParserError
|
|
26
|
+
value
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def humanize(value)
|
|
30
|
+
humanize_logic(value)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def parse_json(str)
|
|
36
|
+
require "json"
|
|
37
|
+
JSON.parse(str)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
AND_OPERATORS = %w[and all].freeze
|
|
41
|
+
OR_OPERATORS = %w[or any].freeze
|
|
42
|
+
LOGIC_OPERATORS = (AND_OPERATORS + OR_OPERATORS).freeze
|
|
43
|
+
NOT_OPERATOR = "!"
|
|
44
|
+
IN_OPERATOR = "in"
|
|
45
|
+
VARIABLE_OPERATOR = "var"
|
|
46
|
+
FACT_OPERATOR = "fact"
|
|
47
|
+
|
|
48
|
+
def explain(logic)
|
|
49
|
+
return logic.to_s unless logic.is_a?(Hash)
|
|
50
|
+
operator, operands = logic.first
|
|
51
|
+
operands ||= []
|
|
52
|
+
case operator
|
|
53
|
+
when *LOGIC_OPERATORS
|
|
54
|
+
joiner = label(normalize(operator))
|
|
55
|
+
operands.map { |operand| explain(operand) rescue operand.inspect }.join(" #{joiner} ")
|
|
56
|
+
when NOT_OPERATOR
|
|
57
|
+
"#{label(NOT_OPERATOR)} (#{explain(operands)})"
|
|
58
|
+
when IN_OPERATOR
|
|
59
|
+
variable, list = operands
|
|
60
|
+
"#{pretty(variable)} #{label(IN_OPERATOR)} #{Array(list).join(', ')}"
|
|
61
|
+
when VARIABLE_OPERATOR
|
|
62
|
+
pretty(operands)
|
|
63
|
+
when FACT_OPERATOR
|
|
64
|
+
"#{pretty(operands)} #{label(logic['operator'])} #{logic['value']}"
|
|
65
|
+
else
|
|
66
|
+
binary(operator, operands) || logic.inspect
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def binary(operator, operands)
|
|
71
|
+
return unless valid_operands?(operands)
|
|
72
|
+
"#{pretty(operands.first)} #{label(operator)} #{operands[1]}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def valid_operands?(operands)
|
|
76
|
+
operands.is_a?(Array) && operands.size == 2 && operands.first.is_a?(Hash) && operands.first[VARIABLE_OPERATOR]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def normalize(operator)
|
|
80
|
+
return "and" if AND_OPERATORS.include?(operator)
|
|
81
|
+
return "or" if OR_OPERATORS.include?(operator)
|
|
82
|
+
operator
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def pretty(expression)
|
|
86
|
+
extract_variable_name(expression).tap do |base|
|
|
87
|
+
Array(Config.vars).each do |pattern, replacement|
|
|
88
|
+
if pattern.is_a?(Regexp) ? (match = pattern.match(base)) : base == pattern.to_s
|
|
89
|
+
return replacement.is_a?(Proc) ? replacement.call(match || base) : replacement
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def extract_variable_name(expression)
|
|
96
|
+
expression.is_a?(Hash) && expression[VARIABLE_OPERATOR] ? expression[VARIABLE_OPERATOR].to_s : expression.to_s
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def label(operator)
|
|
100
|
+
Config.operators&.[](operator) || operator
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "humanizable"
|
|
4
|
+
|
|
5
|
+
module JsonLogic
|
|
6
|
+
class Rule
|
|
7
|
+
include Humanizable
|
|
8
|
+
|
|
9
|
+
attr_reader :logic
|
|
10
|
+
|
|
11
|
+
def initialize(value)
|
|
12
|
+
@logic = value
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
humanize_json_logic :logic
|
|
16
|
+
|
|
17
|
+
def to_s
|
|
18
|
+
humanize_logic(@logic).to_s
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def humanize
|
|
22
|
+
to_s
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
def call(value)
|
|
27
|
+
new(value).humanize
|
|
28
|
+
end
|
|
29
|
+
alias [] call
|
|
30
|
+
alias text call
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/json_logic.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: json_logic_humanizable
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1.beta1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Tavrel Kate
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-10-04 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: An extension over JsonLogic that translates Rules to human text via a
|
|
14
|
+
mixin and a small Rule wrapper.
|
|
15
|
+
email:
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- CHANGELOG.md
|
|
21
|
+
- CONTRIBUTION.md
|
|
22
|
+
- LICENSE
|
|
23
|
+
- README.md
|
|
24
|
+
- lib/json_logic.rb
|
|
25
|
+
- lib/json_logic/config.rb
|
|
26
|
+
- lib/json_logic/humanizable.rb
|
|
27
|
+
- lib/json_logic/rule.rb
|
|
28
|
+
- lib/json_logic/version.rb
|
|
29
|
+
homepage:
|
|
30
|
+
licenses:
|
|
31
|
+
- MIT
|
|
32
|
+
metadata: {}
|
|
33
|
+
post_install_message:
|
|
34
|
+
rdoc_options: []
|
|
35
|
+
require_paths:
|
|
36
|
+
- lib
|
|
37
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
38
|
+
requirements:
|
|
39
|
+
- - ">="
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '2.7'
|
|
42
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
requirements: []
|
|
48
|
+
rubygems_version: 3.5.22
|
|
49
|
+
signing_key:
|
|
50
|
+
specification_version: 4
|
|
51
|
+
summary: Translate JsonLogic rules into readable sentences.
|
|
52
|
+
test_files: []
|