jev-feels 0.2.0 → 1.0.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 +4 -4
- data/README.md +10 -29
- data/context7.json +1 -1
- data/lib/jev/model.rb +51 -0
- data/lib/jev/registry.rb +52 -7
- data/lib/jev/version.rb +1 -1
- data/lib/jev.rb +14 -13
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e06314aa51799f9434268858022138a8a871b4f58332907b4be0f5d187885073
|
|
4
|
+
data.tar.gz: 8e5e28527de0e411f38ca91b660cb6a43c77a955af2a0780227c9401d4fdfe35
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 11ff5c82f81642c4e60c1c39f4567de4b3d4e4a7c0ab6bd31a4a19be9cca47b5e2642f1b1388b2cc507ab45b563d02e7ea09767f489954ef1b3bb3d57659a798
|
|
7
|
+
data.tar.gz: 184c045587028d305a7fa8310c298687998981b655ded9e71e8c7990cb30bfeeaecac59bed11940ef3903bde5dfd49eb99a78c37e0c7e3a83f5a8274e8692589
|
data/README.md
CHANGED
|
@@ -27,7 +27,7 @@ end
|
|
|
27
27
|
|
|
28
28
|
## Ruby on Rails
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
Configure once, declare predicates on the model, ask the record:
|
|
31
31
|
|
|
32
32
|
```ruby
|
|
33
33
|
# Gemfile
|
|
@@ -39,41 +39,22 @@ gem "jev-feels"
|
|
|
39
39
|
Jev.configure do |config|
|
|
40
40
|
config.api_key = ENV.fetch("JEV_API_KEY")
|
|
41
41
|
end
|
|
42
|
-
|
|
43
|
-
Jev.define :urgent, "Requires immediate attention or action"
|
|
44
|
-
Jev.define SupportEmail, :urgent, "Outage, customers cannot sign in"
|
|
45
|
-
Jev.define Comment, :urgent, "Legal takedown or self-harm"
|
|
46
42
|
```
|
|
47
43
|
|
|
48
44
|
```ruby
|
|
49
45
|
class SupportEmail < ApplicationRecord
|
|
50
|
-
|
|
51
|
-
Jev.feels?(body, SupportEmail, :urgent)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
```
|
|
46
|
+
include Jev::Model
|
|
55
47
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
```ruby
|
|
59
|
-
# config/initializers/jev.rb
|
|
60
|
-
require "jev-feels/string"
|
|
61
|
-
|
|
62
|
-
Jev.configure do |config|
|
|
63
|
-
config.api_key = ENV.fetch("JEV_API_KEY")
|
|
48
|
+
feels :body, :urgent, "Outage, customers cannot sign in"
|
|
64
49
|
end
|
|
65
50
|
|
|
66
|
-
|
|
67
|
-
|
|
51
|
+
email.feels?(:urgent)
|
|
52
|
+
email.feels(:urgent)
|
|
68
53
|
```
|
|
69
54
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
body.feels?(SupportEmail, :urgent)
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
```
|
|
55
|
+
The class is the scope, the first argument is the field. No model name, no `email.body.feels?`.
|
|
56
|
+
|
|
57
|
+
String sugar is still opt-in (`using Jev::Feels` or `require "jev-feels/string"`) if you want `body.feels?(:urgent)` on a raw string.
|
|
77
58
|
|
|
78
59
|
## String sugar
|
|
79
60
|
|
|
@@ -134,10 +115,10 @@ Jev.define Comment, :urgent, "Legal takedown or self-harm"
|
|
|
134
115
|
Jev.definition(:urgent)
|
|
135
116
|
Jev.definition(Email, :urgent)
|
|
136
117
|
Jev.definitions
|
|
137
|
-
Jev.definitions(
|
|
118
|
+
Jev.definitions(Comment)
|
|
138
119
|
```
|
|
139
120
|
|
|
140
|
-
`define` replaces an existing name in that scope. `Jev.feels?(text, Email, :urgent)` uses
|
|
121
|
+
`define` replaces an existing name in that scope. Scopes are stored by class name, so `Email`, `"Email"` and `:Email` are the same key. `include Jev::Model` plus `feels :body, :urgent, "..."` does the same define and remembers the field. `Jev.feels?(text, Email, :urgent)` uses Email's definition, then a superclass, then the global `:urgent`. `definitions` returns a frozen copy.
|
|
141
122
|
|
|
142
123
|
## Configuration
|
|
143
124
|
|
data/context7.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://context7.com/schema/context7.json",
|
|
3
3
|
"projectTitle": "jev-feels",
|
|
4
|
-
"description": "Ruby-like semantic text predicates via Jev. Jev.feels?(:urgent)
|
|
4
|
+
"description": "Ruby-like semantic text predicates via Jev. Jev.feels?(:urgent) and model#feels?(:urgent) are ordinary conditions, not an SDK session.",
|
|
5
5
|
"excludeFolders": ["spec"]
|
|
6
6
|
}
|
data/lib/jev/model.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jev
|
|
4
|
+
module Model
|
|
5
|
+
def self.included(base)
|
|
6
|
+
base.extend ClassMethods
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module ClassMethods
|
|
10
|
+
def inherited(subclass)
|
|
11
|
+
super
|
|
12
|
+
subclass.extend ClassMethods
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def feels(attribute, name, description)
|
|
16
|
+
raise ArgumentError, "attribute must be a Symbol" unless attribute.is_a?(Symbol)
|
|
17
|
+
|
|
18
|
+
Jev.define(self, name, description)
|
|
19
|
+
(@jev_feels_attributes ||= {})[name.to_sym] = attribute
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def jev_feels_attribute(name)
|
|
23
|
+
current = self
|
|
24
|
+
while current && current != Object
|
|
25
|
+
found = current.instance_variable_get(:@jev_feels_attributes)&.[](name)
|
|
26
|
+
return found if found
|
|
27
|
+
|
|
28
|
+
current = current.superclass
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def feels(predicate)
|
|
34
|
+
Jev.feels(jev_text_for(predicate), self.class, predicate)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def feels?(predicate, threshold: Jev.configuration.threshold)
|
|
38
|
+
Jev.feels?(jev_text_for(predicate), self.class, predicate, threshold: threshold)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def jev_text_for(predicate)
|
|
44
|
+
attribute = self.class.jev_feels_attribute(predicate.to_sym) || raise(
|
|
45
|
+
UndefinedDefinition, "Undefined Jev definition: #{predicate.inspect} for #{self.class}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
public_send(attribute).to_s
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/jev/registry.rb
CHANGED
|
@@ -10,29 +10,74 @@ module Jev
|
|
|
10
10
|
def define(scope, name, description)
|
|
11
11
|
key = name.to_sym
|
|
12
12
|
value = description.to_s.dup.freeze
|
|
13
|
+
scope_key = storage_key(scope)
|
|
13
14
|
@mutex.synchronize do
|
|
14
|
-
(@definitions[
|
|
15
|
+
(@definitions[scope_key] ||= {})[key] = value
|
|
15
16
|
end
|
|
16
17
|
value
|
|
17
18
|
end
|
|
18
19
|
|
|
19
|
-
def fetch(name, scope: nil, fallback: true)
|
|
20
|
+
def fetch(name, scope: nil, fallback: true, inherit: true)
|
|
20
21
|
key = name.to_sym
|
|
21
22
|
@mutex.synchronize do
|
|
22
|
-
|
|
23
|
-
return scoped if scoped
|
|
24
|
-
return unless fallback || scope.nil?
|
|
23
|
+
return @definitions.dig(nil, key) if scope.nil?
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
lookup_keys(scope, inherit: inherit).each do |scope_key|
|
|
26
|
+
found = @definitions.dig(scope_key, key)
|
|
27
|
+
return found if found
|
|
28
|
+
end
|
|
29
|
+
return @definitions.dig(nil, key) if fallback
|
|
30
|
+
|
|
31
|
+
nil
|
|
27
32
|
end
|
|
28
33
|
end
|
|
29
34
|
|
|
30
35
|
def all(scope = nil)
|
|
31
|
-
@mutex.synchronize { (@definitions[scope] || {}).dup.freeze }
|
|
36
|
+
@mutex.synchronize { (@definitions[storage_key(scope)] || {}).dup.freeze }
|
|
32
37
|
end
|
|
33
38
|
|
|
34
39
|
def reset!
|
|
35
40
|
@mutex.synchronize { @definitions.clear }
|
|
36
41
|
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def storage_key(scope)
|
|
46
|
+
return if scope.nil?
|
|
47
|
+
|
|
48
|
+
key =
|
|
49
|
+
case scope
|
|
50
|
+
when String, Symbol then scope.to_s
|
|
51
|
+
when Module then scope.name || scope
|
|
52
|
+
else raise ArgumentError, "scope must be a Module, String, or Symbol"
|
|
53
|
+
end
|
|
54
|
+
raise ArgumentError, "scope must be a non-empty String" if key.is_a?(String) && key.empty?
|
|
55
|
+
|
|
56
|
+
key
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def lookup_keys(scope, inherit:)
|
|
60
|
+
keys = [storage_key(scope)]
|
|
61
|
+
return keys unless inherit
|
|
62
|
+
|
|
63
|
+
klass = scope.is_a?(Class) ? scope : constantize(scope)
|
|
64
|
+
return keys unless klass.is_a?(Class)
|
|
65
|
+
|
|
66
|
+
current = klass.superclass
|
|
67
|
+
while current && current != Object
|
|
68
|
+
keys << (current.name || current)
|
|
69
|
+
current = current.superclass
|
|
70
|
+
end
|
|
71
|
+
keys.uniq
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def constantize(name)
|
|
75
|
+
name = name.to_s if name.is_a?(Symbol)
|
|
76
|
+
return unless name.is_a?(String)
|
|
77
|
+
|
|
78
|
+
name.split("::").reduce(Object) { |mod, part| mod.const_get(part, false) }
|
|
79
|
+
rescue NameError
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
37
82
|
end
|
|
38
83
|
end
|
data/lib/jev/version.rb
CHANGED
data/lib/jev.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "jev/registry"
|
|
|
7
7
|
require_relative "jev/transport"
|
|
8
8
|
require_relative "jev/client"
|
|
9
9
|
require_relative "jev/feels"
|
|
10
|
+
require_relative "jev/model"
|
|
10
11
|
|
|
11
12
|
module Jev
|
|
12
13
|
class << self
|
|
@@ -22,30 +23,26 @@ module Jev
|
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
def define(scope_or_name, name_or_description, description = nil)
|
|
25
|
-
if
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
else
|
|
30
|
-
raise ArgumentError, "wrong number of arguments (given 3, expected 2)" unless description.nil?
|
|
26
|
+
if description.nil?
|
|
27
|
+
if scope_or_name.is_a?(Module) || name_or_description.is_a?(Symbol)
|
|
28
|
+
raise ArgumentError, "description is required"
|
|
29
|
+
end
|
|
31
30
|
|
|
32
31
|
registry.define(nil, scope_or_name, name_or_description)
|
|
32
|
+
else
|
|
33
|
+
registry.define(scope_or_name, name_or_description, description)
|
|
33
34
|
end
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
def definition(scope_or_name, name = nil)
|
|
37
38
|
if name.nil?
|
|
38
|
-
registry.fetch(scope_or_name, scope: nil, fallback: false)
|
|
39
|
+
registry.fetch(scope_or_name, scope: nil, fallback: false, inherit: false)
|
|
39
40
|
else
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
registry.fetch(name, scope: scope_or_name, fallback: false)
|
|
41
|
+
registry.fetch(name, scope: scope_or_name, fallback: false, inherit: false)
|
|
43
42
|
end
|
|
44
43
|
end
|
|
45
44
|
|
|
46
45
|
def definitions(scope = nil)
|
|
47
|
-
raise ArgumentError, "scope must be a Module" unless scope.nil? || scope.is_a?(Module)
|
|
48
|
-
|
|
49
46
|
registry.all(scope)
|
|
50
47
|
end
|
|
51
48
|
|
|
@@ -86,11 +83,15 @@ module Jev
|
|
|
86
83
|
|
|
87
84
|
def unpack_predicate(scope_or_predicate, predicate)
|
|
88
85
|
return [nil, scope_or_predicate] if predicate.nil?
|
|
89
|
-
raise ArgumentError, "scope must be a Module" unless
|
|
86
|
+
raise ArgumentError, "scope must be a Module, String, or Symbol" unless scoped?(scope_or_predicate)
|
|
90
87
|
|
|
91
88
|
[scope_or_predicate, predicate]
|
|
92
89
|
end
|
|
93
90
|
|
|
91
|
+
def scoped?(value)
|
|
92
|
+
value.is_a?(Module) || value.is_a?(String) || value.is_a?(Symbol)
|
|
93
|
+
end
|
|
94
|
+
|
|
94
95
|
def resolve(predicate, scope: nil)
|
|
95
96
|
case predicate
|
|
96
97
|
when Symbol
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jev-feels
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Maxim Veysgeym
|
|
@@ -31,6 +31,7 @@ files:
|
|
|
31
31
|
- lib/jev/configuration.rb
|
|
32
32
|
- lib/jev/errors.rb
|
|
33
33
|
- lib/jev/feels.rb
|
|
34
|
+
- lib/jev/model.rb
|
|
34
35
|
- lib/jev/registry.rb
|
|
35
36
|
- lib/jev/transport.rb
|
|
36
37
|
- lib/jev/version.rb
|
|
@@ -40,7 +41,7 @@ licenses:
|
|
|
40
41
|
metadata:
|
|
41
42
|
homepage_uri: https://github.com/Qew7/jev-feels
|
|
42
43
|
source_code_uri: https://github.com/Qew7/jev-feels
|
|
43
|
-
changelog_uri: https://github.com/Qew7/jev-feels/blob/
|
|
44
|
+
changelog_uri: https://github.com/Qew7/jev-feels/blob/master/README.md
|
|
44
45
|
allowed_push_host: https://rubygems.org
|
|
45
46
|
post_install_message:
|
|
46
47
|
rdoc_options: []
|