typesafe-jev 0.2.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/CHANGELOG.md +9 -0
- data/LICENSE +0 -0
- data/README.md +14 -0
- data/lib/typesafe/choice.rb +49 -0
- data/lib/typesafe/noul.rb +60 -0
- data/lib/typesafe/question.rb +69 -0
- data/lib/typesafe/score.rb +46 -0
- data/lib/typesafe/version.rb +5 -0
- data/lib/typesafe.rb +12 -0
- metadata +80 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7645167a374f6815e9a4ee6b96b56d54f611a3a08b274467f963dedd66e786ca
|
|
4
|
+
data.tar.gz: 0d66633fe64b5061b9f1bf7cb59fdf74f73d711c332bb34f7bbf0be1a042b85c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6de70dbd0412a10808feb2f79e1635462056a0c8069306b3fd063167b15ff0e6706f64fc22719e3cdd47f21a3fff4a70c64d368660eb1491c5abd5b4240e7baf
|
|
7
|
+
data.tar.gz: fd391b3ecd54101ea40a0923d618ee6e9a884ae54c3c9876d9e3ef5ed3f3a8741b81b7ae032ebe3cf9e109cdf96df468bd3741995c94a6d0b949266ed2711622
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
- Add question classes: `Typesafe::Noul`, `Typesafe::Choice`, `Typesafe::Score` with the abstract `Typesafe::Question` base. Frozen, immutable value objects that validate their inputs and serialize to the TypeSafe API shape via `#to_h`/`#to_json`.
|
|
6
|
+
|
|
7
|
+
## 0.1.0
|
|
8
|
+
|
|
9
|
+
- Initial gem scaffold.
|
data/LICENSE
ADDED
|
File without changes
|
data/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typesafe
|
|
4
|
+
# A question that selects one option from a defined, unordered set.
|
|
5
|
+
# The answer includes the selected option, the probability distribution
|
|
6
|
+
# across all options, and a confidence.
|
|
7
|
+
#
|
|
8
|
+
# Typesafe::Choice.new("Which team should handle this?",
|
|
9
|
+
# criteria: {
|
|
10
|
+
# billing: "Payment or subscription issues",
|
|
11
|
+
# technical: "Bugs or integration problems"
|
|
12
|
+
# })
|
|
13
|
+
class Choice < Question
|
|
14
|
+
# @return [Hash] the option keys mapped to their descriptions.
|
|
15
|
+
attr_reader :criteria
|
|
16
|
+
|
|
17
|
+
# @param instructions [String] the question asked about the state.
|
|
18
|
+
# @param criteria [Hash] the options: keys (String or Symbol) mapped to
|
|
19
|
+
# non-empty String descriptions.
|
|
20
|
+
def initialize(instructions, criteria:)
|
|
21
|
+
@criteria = normalize_criteria(criteria)
|
|
22
|
+
super(instructions)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def type
|
|
26
|
+
"choice"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_h
|
|
30
|
+
super.merge(criteria: @criteria)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def normalize_criteria(criteria)
|
|
36
|
+
unless criteria.is_a?(Hash) && !criteria.empty?
|
|
37
|
+
raise ArgumentError, "criteria must be a non-empty Hash of option keys to descriptions"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
criteria.each_with_object({}) do |(key, value), normalized|
|
|
41
|
+
unless key.is_a?(String) || key.is_a?(Symbol)
|
|
42
|
+
raise ArgumentError, "criteria keys must be Strings or Symbols"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
normalized[key] = validate_string(value, "criteria[#{key.inspect}]")
|
|
46
|
+
end.freeze
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typesafe
|
|
4
|
+
# A yes/no question. The answer is the probability that the answer is yes.
|
|
5
|
+
#
|
|
6
|
+
# Typesafe::Noul.new("Does the customer request a refund?")
|
|
7
|
+
#
|
|
8
|
+
# +criteria+ optionally clarifies what yes and no mean:
|
|
9
|
+
#
|
|
10
|
+
# Typesafe::Noul.new("Is the message urgent?",
|
|
11
|
+
# criteria: { true: "Conveys urgency", false: "No time pressure" })
|
|
12
|
+
class Noul < Question
|
|
13
|
+
# @return [Hash, nil] the optional +{ true:, false: }+ descriptions.
|
|
14
|
+
attr_reader :criteria
|
|
15
|
+
|
|
16
|
+
# @param instructions [String] the yes/no question asked about the state.
|
|
17
|
+
# @param criteria [Hash, nil] optional +{ true:, false: }+ descriptions of
|
|
18
|
+
# what yes and no mean. Keys may be Strings or Symbols and are
|
|
19
|
+
# normalized to Symbols.
|
|
20
|
+
def initialize(instructions, criteria: nil)
|
|
21
|
+
@criteria = criteria.nil? ? nil : normalize_criteria(criteria)
|
|
22
|
+
super(instructions)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def type
|
|
26
|
+
"noul"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [Hash] the API shape; +criteria+ is omitted when not given.
|
|
30
|
+
def to_h
|
|
31
|
+
@criteria ? super.merge(criteria: @criteria) : super
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def normalize_criteria(criteria)
|
|
37
|
+
unless criteria.is_a?(Hash)
|
|
38
|
+
raise ArgumentError, "criteria must be a Hash with :true and :false keys"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
normalized = {}
|
|
42
|
+
criteria.each do |key, value|
|
|
43
|
+
unless key.is_a?(String) || key.is_a?(Symbol)
|
|
44
|
+
raise ArgumentError, "criteria keys must be Strings or Symbols"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
sym = key.to_sym
|
|
48
|
+
raise ArgumentError, "criteria has duplicate key #{sym.inspect}" if normalized.key?(sym)
|
|
49
|
+
|
|
50
|
+
normalized[sym] = validate_string(value, "criteria[#{key.inspect}]")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
unless normalized.keys.sort == %i[false true]
|
|
54
|
+
raise ArgumentError, "criteria must have exactly :true and :false keys"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
normalized.freeze
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typesafe
|
|
4
|
+
# Abstract base class for the three question types understood by the
|
|
5
|
+
# TypeSafe System One API: {Noul}, {Choice}, and {Score}.
|
|
6
|
+
#
|
|
7
|
+
# A question is an immutable value object. The question ID used to identify
|
|
8
|
+
# its answer is not part of the object: it is the Hash key under which the
|
|
9
|
+
# question is sent in a request, e.g.
|
|
10
|
+
#
|
|
11
|
+
# questions = { refund_requested: Typesafe::Noul.new("Does the customer request a refund?") }
|
|
12
|
+
class Question
|
|
13
|
+
# @return [String] the question asked about the state.
|
|
14
|
+
attr_reader :instructions
|
|
15
|
+
|
|
16
|
+
# @param instructions [String] the question asked about the state.
|
|
17
|
+
# @raise [ArgumentError] if +instructions+ is not a non-empty String.
|
|
18
|
+
def initialize(instructions)
|
|
19
|
+
unless instructions.is_a?(String) && !instructions.strip.empty?
|
|
20
|
+
raise ArgumentError, "instructions must be a non-empty String"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
@instructions = instructions.dup.freeze
|
|
24
|
+
validate!
|
|
25
|
+
freeze
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [String] the API question type tag ("noul", "choice", or "score").
|
|
29
|
+
# @raise [NotImplementedError] on the abstract base class.
|
|
30
|
+
def type
|
|
31
|
+
raise NotImplementedError, "#{self.class} must implement #type"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @return [Hash] the question shape expected by the TypeSafe API.
|
|
35
|
+
def to_h
|
|
36
|
+
{ type: type, instructions: instructions }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [String] the question serialized as JSON for the TypeSafe API.
|
|
40
|
+
def to_json(state = nil)
|
|
41
|
+
JSON.generate(to_h, state)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Questions compare equal when they are of the same class and serialize
|
|
45
|
+
# to the same API shape.
|
|
46
|
+
def ==(other)
|
|
47
|
+
other.class == self.class && other.to_h == to_h
|
|
48
|
+
end
|
|
49
|
+
alias eql? ==
|
|
50
|
+
|
|
51
|
+
def hash
|
|
52
|
+
[self.class, to_h].hash
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# Hook for subclasses to validate their own attributes before freezing.
|
|
58
|
+
def validate!; end
|
|
59
|
+
|
|
60
|
+
# Validates that +value+ is a non-empty String and returns a frozen copy.
|
|
61
|
+
def validate_string(value, name)
|
|
62
|
+
unless value.is_a?(String) && !value.strip.empty?
|
|
63
|
+
raise ArgumentError, "#{name} must be a non-empty String"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
value.dup.freeze
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typesafe
|
|
4
|
+
# A question that positions the state along an ordered spectrum of levels.
|
|
5
|
+
# The answer includes the score, the legend (levels by number), the
|
|
6
|
+
# probability distribution across levels, and a confidence.
|
|
7
|
+
#
|
|
8
|
+
# Typesafe::Score.new("How frustrated is the customer?",
|
|
9
|
+
# criteria: [
|
|
10
|
+
# "Calm, just stating facts",
|
|
11
|
+
# "Frustrated but civil",
|
|
12
|
+
# "Very angry, strong language"
|
|
13
|
+
# ])
|
|
14
|
+
class Score < Question
|
|
15
|
+
# @return [Array<String>] the ordered levels of the spectrum.
|
|
16
|
+
attr_reader :criteria
|
|
17
|
+
|
|
18
|
+
# @param instructions [String] the question asked about the state.
|
|
19
|
+
# @param criteria [Array<String>] the ordered levels; a non-empty array
|
|
20
|
+
# of non-empty Strings, ordered from lowest to highest.
|
|
21
|
+
def initialize(instructions, criteria:)
|
|
22
|
+
@criteria = normalize_criteria(criteria)
|
|
23
|
+
super(instructions)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def type
|
|
27
|
+
"score"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_h
|
|
31
|
+
super.merge(criteria: @criteria)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def normalize_criteria(criteria)
|
|
37
|
+
unless criteria.is_a?(Array) && !criteria.empty?
|
|
38
|
+
raise ArgumentError, "criteria must be a non-empty Array of Strings"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
criteria.each_with_index.map do |level, index|
|
|
42
|
+
validate_string(level, "criteria[#{index}]")
|
|
43
|
+
end.freeze
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/typesafe.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
require_relative "typesafe/version"
|
|
6
|
+
require_relative "typesafe/question"
|
|
7
|
+
require_relative "typesafe/noul"
|
|
8
|
+
require_relative "typesafe/choice"
|
|
9
|
+
require_relative "typesafe/score"
|
|
10
|
+
|
|
11
|
+
module Typesafe
|
|
12
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: typesafe-jev
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- dtheo
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rake
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '13.0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '13.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rspec
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.13'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.13'
|
|
40
|
+
description: Use Jev, TypeSafe's System One model, from Ruby or Rails.
|
|
41
|
+
email:
|
|
42
|
+
- dtheo@users.noreply.github.com
|
|
43
|
+
executables: []
|
|
44
|
+
extensions: []
|
|
45
|
+
extra_rdoc_files: []
|
|
46
|
+
files:
|
|
47
|
+
- CHANGELOG.md
|
|
48
|
+
- LICENSE
|
|
49
|
+
- README.md
|
|
50
|
+
- lib/typesafe.rb
|
|
51
|
+
- lib/typesafe/choice.rb
|
|
52
|
+
- lib/typesafe/noul.rb
|
|
53
|
+
- lib/typesafe/question.rb
|
|
54
|
+
- lib/typesafe/score.rb
|
|
55
|
+
- lib/typesafe/version.rb
|
|
56
|
+
homepage: https://docs.typesafe.ai
|
|
57
|
+
licenses:
|
|
58
|
+
- MIT
|
|
59
|
+
metadata:
|
|
60
|
+
homepage_uri: https://docs.typesafe.ai
|
|
61
|
+
source_code_uri: https://github.com/dtheo/typesafe-jev-ruby
|
|
62
|
+
changelog_uri: https://github.com/dtheo/typesafe-jev-ruby/blob/main/CHANGELOG.md
|
|
63
|
+
rdoc_options: []
|
|
64
|
+
require_paths:
|
|
65
|
+
- lib
|
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '3.1'
|
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
requirements: []
|
|
77
|
+
rubygems_version: 3.6.9
|
|
78
|
+
specification_version: 4
|
|
79
|
+
summary: Ruby client for Jev, TypeSafe's System One model.
|
|
80
|
+
test_files: []
|