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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 125dde58282251c7692d030e90352e098cf1785bc38904cf6021098d073d2f8a
4
- data.tar.gz: 6bdf7fb784623b94cde049148294a1513e59207ccde0fd683869bbb4cc639a6d
3
+ metadata.gz: e06314aa51799f9434268858022138a8a871b4f58332907b4be0f5d187885073
4
+ data.tar.gz: 8e5e28527de0e411f38ca91b660cb6a43c77a955af2a0780227c9401d4fdfe35
5
5
  SHA512:
6
- metadata.gz: 2b5a17cbde5d54178e39eed6600fdff431f5fdfa0515af09f79f4711eb19b67148a87e669a973755d2afa9546c2fcd5e6e14e15b29d1ddc4908834bc24ca4570
7
- data.tar.gz: fb31b5c3f4fb3d90204cbfa815a5bf3aff537eb8611645f5099c79b775f1fd6d8f6427dc4ea603a8b7f670278aae61fe7b7e6e009e7136528fd75ab2149c0c82
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
- Gemfile, initializer, then an ordinary condition. Bundler loads the gem — no extra `require` unless you want sugar on `String`.
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
- def urgent?
51
- Jev.feels?(body, SupportEmail, :urgent)
52
- end
53
- end
54
- ```
46
+ include Jev::Model
55
47
 
56
- To use `#feels?` on strings across the app, require the String extension in the initializer:
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
- Jev.define :urgent, "Requires immediate attention or action"
67
- Jev.define SupportEmail, :urgent, "Outage, customers cannot sign in"
51
+ email.feels?(:urgent)
52
+ email.feels(:urgent)
68
53
  ```
69
54
 
70
- ```ruby
71
- class SupportEmail < ApplicationRecord
72
- def urgent?
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(Email)
118
+ Jev.definitions(Comment)
138
119
  ```
139
120
 
140
- `define` replaces an existing name in that scope. `Jev.feels?(text, Email, :urgent)` uses the Email definition, then falls back to the global `:urgent` if Email has none. `definitions` / `definitions(Email)` return frozen copies.
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) is an ordinary condition, not an SDK session.",
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[scope] ||= {})[key] = value
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
- scoped = @definitions.dig(scope, key) if scope
23
- return scoped if scoped
24
- return unless fallback || scope.nil?
23
+ return @definitions.dig(nil, key) if scope.nil?
25
24
 
26
- @definitions.dig(nil, key)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Jev
4
- VERSION = "0.2.0"
4
+ VERSION = "1.0.0"
5
5
  end
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 scope_or_name.is_a?(Module)
26
- raise ArgumentError, "description is required" if description.nil?
27
-
28
- registry.define(scope_or_name, name_or_description, description)
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
- raise ArgumentError, "scope must be a Module" unless scope_or_name.is_a?(Module)
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 scope_or_predicate.is_a?(Module)
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.2.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/main/README.md
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: []