philosophal 1.3.0 → 2.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/CHANGELOG.md +14 -0
- data/README.md +39 -0
- data/lib/philosophal/convertor.rb +8 -11
- data/lib/philosophal/loaders/json_loader.rb +1 -6
- data/lib/philosophal/properties/instance_methods.rb +22 -2
- data/lib/philosophal/properties/schema.rb +32 -1
- data/lib/philosophal/properties.rb +22 -7
- data/lib/philosophal/property.rb +85 -9
- data/lib/philosophal/transform.rb +1 -1
- data/lib/philosophal/types/array_of_type.rb +1 -1
- data/lib/philosophal/types/boolean_type.rb +1 -1
- data/lib/philosophal/types/hash_of_type.rb +5 -3
- data/lib/philosophal.rb +3 -3
- data/sig/manifest.yaml +5 -0
- data/sig/philosophal/convertor.rbs +35 -0
- data/sig/philosophal/errors/argument_error.rbs +4 -0
- data/sig/philosophal/errors/type_error.rbs +4 -0
- data/sig/philosophal/loaders/errors.rbs +6 -0
- data/sig/philosophal/loaders/json_loader.rbs +29 -0
- data/sig/philosophal/loaders.rbs +4 -0
- data/sig/philosophal/properties/instance_methods.rbs +26 -0
- data/sig/philosophal/properties/schema.rbs +47 -0
- data/sig/philosophal/properties.rbs +59 -0
- data/sig/philosophal/property.rbs +98 -0
- data/sig/philosophal/transform.rbs +7 -0
- data/sig/philosophal/types/any_type.rbs +9 -0
- data/sig/philosophal/types/array_of_type.rbs +23 -0
- data/sig/philosophal/types/boolean_type.rbs +11 -0
- data/sig/philosophal/types/hash_of_type.rbs +25 -0
- data/sig/philosophal/types.rbs +11 -0
- data/sig/philosophal.rbs +12 -0
- metadata +20 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d588fe0288b3b4d953de700a9218a607b4e7fc23d3946a8e34b4a1f90a7f1265
|
|
4
|
+
data.tar.gz: cf23c471421f8661992e575bfef9edb21e2a659962c0f54ec1618f0047b23b87
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ae95691e4be606dbca97f8c3a1e3ccf96b34a310b95a5f07567cc8e2c80f37491ae92e03ecc4d60cfc851c0147d8b63ea0b9bb4cc16f64b1c32e06b1fdb33ed4
|
|
7
|
+
data.tar.gz: 3db4c56bea72d05e0e7a2d75a65842d567e6f2c33f8a37ffcdf2a82374c404c6cc37ce3dec376e94f004b55d3dd6182de8a15518f962d7f70f7aa26effceeb39
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [2.0.0] - 2026-11-03
|
|
2
|
+
|
|
3
|
+
- Add: new `track_changes:` option for `cprop`, which defines `prop_was` (the value at the last `changes_applied`) and `prop_changed?` (the value differs from it)
|
|
4
|
+
- Add: `changes_applied` records the current values of the tracked properties as the reference
|
|
5
|
+
- Add: `changed_properties` returns a hash of the tracked properties changed since the last `changes_applied`, with their new value
|
|
6
|
+
- Add: RBS type signatures (`sig/`), shipped with the gem
|
|
7
|
+
- Fix: **ArrayOf** and **HashOf** no longer accept `nil` or `false` entries of the wrong type without converting them
|
|
8
|
+
- Fix: converting to **ArrayOf** or **HashOf** no longer modifies the given array or hash
|
|
9
|
+
- Perf: the reader of a property without default is now a plain `attr_reader`
|
|
10
|
+
- Perf: writers, and readers falling back to their default, no longer look up their property in the schema
|
|
11
|
+
- Breaking: redeclaring in a subclass, with a default and `override: false`, a property no longer applies the subclass default or type (the parent methods are kept)
|
|
12
|
+
- Perf: fewer lookups and allocations when converting, loading JSON, `to_json_hash`, `changes_applied` and `changed_properties`
|
|
13
|
+
- Fix: `Properties::Schema` now defines `each`, so its Enumerable methods (`map`, `select`, `find`...) work and yield the properties
|
|
14
|
+
|
|
1
15
|
## [1.3.0] - 2026-10-19
|
|
2
16
|
|
|
3
17
|
- Breaking: minimum required Ruby version is now **3.3** (was 3.1)
|
data/README.md
CHANGED
|
@@ -28,3 +28,42 @@ maxime.age = '40'
|
|
|
28
28
|
puts maxime.inspect
|
|
29
29
|
#<Person:0x00007da7088eed88 @first_name="maxime", @last_name="DÉSÉCOT", @age=40>
|
|
30
30
|
````
|
|
31
|
+
|
|
32
|
+
### Tracking changes
|
|
33
|
+
|
|
34
|
+
With `track_changes: true`, a property also gets `<name>_was` and `<name>_changed?`.
|
|
35
|
+
Call `changes_applied` once the object is in a known state (built, loaded, saved...):
|
|
36
|
+
`<name>_was` returns the value at that moment, and `<name>_changed?` tells whether the value differs from it.
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
class Article
|
|
40
|
+
extend Philosophal::Properties
|
|
41
|
+
|
|
42
|
+
cprop :title, String, track_changes: true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
article = Article.new
|
|
46
|
+
article.title = 'Draft'
|
|
47
|
+
article.title_changed? # => true (no reference yet)
|
|
48
|
+
article.changes_applied
|
|
49
|
+
article.title_changed? # => false
|
|
50
|
+
article.title = 'Review'
|
|
51
|
+
article.title = 'Final'
|
|
52
|
+
article.title_was # => "Draft"
|
|
53
|
+
article.title_changed? # => true
|
|
54
|
+
article.title = 'Draft'
|
|
55
|
+
article.title_changed? # => false
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`changed_properties` returns the tracked properties that changed, with their new value:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
article.title = 'Final'
|
|
62
|
+
article.changed_properties # => { title: "Final" }
|
|
63
|
+
article.changes_applied
|
|
64
|
+
article.changed_properties # => {}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Before the first `changes_applied`, the reference is "never assigned": every assigned property is changed.
|
|
68
|
+
`<name>_was` returns the stored value, without falling back to the default.
|
|
69
|
+
`json.load` does not call `changes_applied`: call it on the loaded objects if they must start unchanged.
|
|
@@ -102,9 +102,9 @@ module Philosophal
|
|
|
102
102
|
return false if obj.zero?
|
|
103
103
|
|
|
104
104
|
elsif obj.is_a?(String)
|
|
105
|
-
|
|
106
|
-
return true if
|
|
107
|
-
return false if
|
|
105
|
+
# casecmp compares ASCII only, which is enough for 'true' and 'false', and allocates no string.
|
|
106
|
+
return true if obj.casecmp('true') == 0 # rubocop:disable Style/NumericPredicate
|
|
107
|
+
return false if obj.casecmp('false') == 0 # rubocop:disable Style/NumericPredicate
|
|
108
108
|
|
|
109
109
|
end
|
|
110
110
|
raise Philosophal::TypeError
|
|
@@ -120,11 +120,8 @@ module Philosophal
|
|
|
120
120
|
subtype_convert_method = convert_method_for(subtype)
|
|
121
121
|
raise Philosophal::TypeError unless subtype_convert_method
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
send(subtype_convert_method, item)
|
|
126
|
-
end
|
|
127
|
-
end
|
|
123
|
+
# map, not map!: Array#to_a returns the given array itself, which must not be modified.
|
|
124
|
+
convert_to_array(obj).map { |item| send(subtype_convert_method, item) }
|
|
128
125
|
end
|
|
129
126
|
|
|
130
127
|
def convert_to_hash_of(obj, subtype)
|
|
@@ -134,9 +131,9 @@ module Philosophal
|
|
|
134
131
|
value_convert_method = convert_method_for(subtype[:value_type])
|
|
135
132
|
raise Philosophal::TypeError unless value_convert_method
|
|
136
133
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
134
|
+
# A new hash is built: Hash#to_h returns the given hash itself, which must not be modified.
|
|
135
|
+
convert_to_hash(obj).to_h do |key, value|
|
|
136
|
+
[send(key_convert_method, key), send(value_convert_method, value)]
|
|
140
137
|
end
|
|
141
138
|
end
|
|
142
139
|
end
|
|
@@ -46,12 +46,7 @@ module Philosophal
|
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def setter_from_key(key)
|
|
49
|
-
|
|
50
|
-
property = @klass.philosophal_properties[real_key]
|
|
51
|
-
|
|
52
|
-
return :"#{property.name}=" if property
|
|
53
|
-
|
|
54
|
-
:"#{key}="
|
|
49
|
+
@klass.philosophal_properties.json_setters[key] || :"#{key}="
|
|
55
50
|
end
|
|
56
51
|
|
|
57
52
|
def load_from_json(str)
|
|
@@ -26,7 +26,7 @@ module Philosophal
|
|
|
26
26
|
alias cprop_inspect philosophal_inspect
|
|
27
27
|
|
|
28
28
|
def philosophal_inspect_map
|
|
29
|
-
|
|
29
|
+
self.class.philosophal_properties.to_h do |property|
|
|
30
30
|
[
|
|
31
31
|
property.name,
|
|
32
32
|
send(property.name)
|
|
@@ -39,8 +39,28 @@ module Philosophal
|
|
|
39
39
|
self.class.philosophal_properties.properties_index.values
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# Properties declared with track_changes: true whose value changed since the last changes_applied,
|
|
43
|
+
# with their new value.
|
|
44
|
+
def changed_properties
|
|
45
|
+
self.class.philosophal_properties.tracked.each_with_object({}) do |property, changes|
|
|
46
|
+
next unless send(property.changed_method_name)
|
|
47
|
+
|
|
48
|
+
changes[property.name] = send(property.name)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Records the current values as the reference of the tracked properties: none of them is changed anymore.
|
|
53
|
+
# The values are duplicated, so modifying one in place is still seen as a change.
|
|
54
|
+
def changes_applied
|
|
55
|
+
self.class.philosophal_properties.tracked.each do |property|
|
|
56
|
+
instance_variable_set(property.was_variable_name, instance_variable_get(property.variable_name).dup)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
42
62
|
def to_json_hash
|
|
43
|
-
|
|
63
|
+
self.class.philosophal_properties.json_properties.to_h do |property|
|
|
44
64
|
[
|
|
45
65
|
property.json_name,
|
|
46
66
|
send(property.name)
|
|
@@ -28,6 +28,14 @@ module Philosophal
|
|
|
28
28
|
@properties_index.keys
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# Yields each property in declaration order; Enumerable builds on it.
|
|
32
|
+
def each(&)
|
|
33
|
+
return enum_for(:each) { @properties_index.size } unless block_given?
|
|
34
|
+
|
|
35
|
+
@properties_index.each_value(&)
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
|
|
31
39
|
def <<(value)
|
|
32
40
|
@mutex.synchronize do
|
|
33
41
|
@properties_index[value.name] = value
|
|
@@ -57,10 +65,33 @@ module Philosophal
|
|
|
57
65
|
end
|
|
58
66
|
end
|
|
59
67
|
|
|
68
|
+
# Maps each JSON name to the writer of its property.
|
|
69
|
+
def json_setters
|
|
70
|
+
return @json_setters if defined?(@json_setters)
|
|
71
|
+
|
|
72
|
+
@json_setters = @properties_index.each_value.to_h do |property|
|
|
73
|
+
[property.json_name, property.writer_method_name]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Properties declared with json: (exported by to_json_hash).
|
|
78
|
+
def json_properties
|
|
79
|
+
return @json_properties if defined?(@json_properties)
|
|
80
|
+
|
|
81
|
+
@json_properties = @properties_index.each_value.select(&:json)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Properties declared with track_changes: true.
|
|
85
|
+
def tracked
|
|
86
|
+
return @tracked if defined?(@tracked)
|
|
87
|
+
|
|
88
|
+
@tracked = @properties_index.each_value.select(&:track_changes?)
|
|
89
|
+
end
|
|
90
|
+
|
|
60
91
|
private
|
|
61
92
|
|
|
62
93
|
def clear_caches
|
|
63
|
-
%i[@immutables @mutables @json_names_h].each do |cache|
|
|
94
|
+
%i[@immutables @mutables @json_names_h @json_setters @json_properties @tracked].each do |cache|
|
|
64
95
|
remove_instance_variable(cache) if instance_variable_defined?(cache)
|
|
65
96
|
end
|
|
66
97
|
end
|
|
@@ -8,15 +8,16 @@ module Philosophal
|
|
|
8
8
|
|
|
9
9
|
include Philosophal::Types
|
|
10
10
|
|
|
11
|
-
def cprop(name, type, default: nil, transform: nil, immutable: false, description: nil, json: false, override: true
|
|
12
|
-
|
|
11
|
+
def cprop(name, type, default: nil, transform: nil, immutable: false, description: nil, json: false, override: true,
|
|
12
|
+
track_changes: false)
|
|
13
|
+
__check_philosophal_options__(name, transform:, immutable:, override:, description:, json:, track_changes:)
|
|
13
14
|
|
|
14
15
|
default.freeze unless default.is_a?(Proc)
|
|
15
16
|
description&.freeze
|
|
16
17
|
json = json.to_sym if json.is_a?(String)
|
|
17
18
|
|
|
18
19
|
property = __philosophal_property_class__.new(
|
|
19
|
-
name:, type:, default:, transform:, immutable:, description:, json:, override:
|
|
20
|
+
name:, type:, default:, transform:, immutable:, description:, json:, override:, track_changes:
|
|
20
21
|
)
|
|
21
22
|
|
|
22
23
|
philosophal_properties << property
|
|
@@ -61,10 +62,20 @@ module Philosophal
|
|
|
61
62
|
end
|
|
62
63
|
|
|
63
64
|
def __define_philosophal_methods__(new_property)
|
|
65
|
+
__set_philosophal_property_constant__(new_property)
|
|
64
66
|
code = __generate_philosophal_methods__(new_property)
|
|
65
67
|
__philosophal_extension__.module_eval(code)
|
|
66
68
|
end
|
|
67
69
|
|
|
70
|
+
# The constant is replaced when the property is redeclared in the same class.
|
|
71
|
+
def __set_philosophal_property_constant__(property)
|
|
72
|
+
extension = __philosophal_extension__
|
|
73
|
+
constant_name = property.constant_name
|
|
74
|
+
extension.send(:remove_const, constant_name) if extension.const_defined?(constant_name, false)
|
|
75
|
+
extension.const_set(constant_name, property)
|
|
76
|
+
extension.private_constant(constant_name)
|
|
77
|
+
end
|
|
78
|
+
|
|
68
79
|
def __philosophal_extension__
|
|
69
80
|
@__philosophal_extension__ ||= Module.new { include Philosophal::Properties::InstanceMethods }
|
|
70
81
|
end
|
|
@@ -86,17 +97,21 @@ module Philosophal
|
|
|
86
97
|
end
|
|
87
98
|
|
|
88
99
|
# Generated methods live in an included module, so a method defined in the class itself
|
|
89
|
-
# would hide them: it is removed to let the property override it.
|
|
100
|
+
# would hide them: it is removed to let the property override it. The method generated by a
|
|
101
|
+
# previous declaration of the property is removed too, so redefining it raises no warning.
|
|
90
102
|
def __remove_philosophal_own_method__(method_name)
|
|
91
|
-
|
|
103
|
+
[self, __philosophal_extension__].each do |owner|
|
|
104
|
+
next unless owner.method_defined?(method_name, false) || owner.private_method_defined?(method_name, false)
|
|
92
105
|
|
|
93
|
-
|
|
106
|
+
owner.remove_method(method_name)
|
|
107
|
+
end
|
|
94
108
|
end
|
|
95
109
|
|
|
96
|
-
def __check_philosophal_options__(name, transform:, immutable:, override:, description:, json:)
|
|
110
|
+
def __check_philosophal_options__(name, transform:, immutable:, override:, description:, json:, track_changes:)
|
|
97
111
|
__check_philosophal_transform_option__(name, transform)
|
|
98
112
|
__check_philosophal_boolean_option__(:override, override)
|
|
99
113
|
__check_philosophal_boolean_option__(:immutable, immutable)
|
|
114
|
+
__check_philosophal_boolean_option__(:track_changes, track_changes)
|
|
100
115
|
|
|
101
116
|
if description && !description.is_a?(String)
|
|
102
117
|
raise Philosophal::ArgumentError, 'description param must be a string.'
|
data/lib/philosophal/property.rb
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Philosophal
|
|
4
4
|
class Property
|
|
5
|
-
def initialize(name:, type:, default:, transform:, immutable:, description:, json:, override
|
|
5
|
+
def initialize(name:, type:, default:, transform:, immutable:, description:, json:, override:,
|
|
6
|
+
track_changes: false)
|
|
6
7
|
@name = name
|
|
7
8
|
@type = type
|
|
8
9
|
@default = default
|
|
@@ -11,9 +12,10 @@ module Philosophal
|
|
|
11
12
|
@description = description
|
|
12
13
|
@json = json
|
|
13
14
|
@override = override
|
|
15
|
+
@track_changes = track_changes
|
|
14
16
|
end
|
|
15
17
|
|
|
16
|
-
attr_reader :name, :type, :default, :transform, :immutable, :description, :json, :override
|
|
18
|
+
attr_reader :name, :type, :default, :transform, :immutable, :description, :json, :override, :track_changes
|
|
17
19
|
|
|
18
20
|
def json_name
|
|
19
21
|
return @json if @json.is_a?(Symbol)
|
|
@@ -40,18 +42,43 @@ module Philosophal
|
|
|
40
42
|
@override
|
|
41
43
|
end
|
|
42
44
|
|
|
45
|
+
def track_changes?
|
|
46
|
+
@track_changes
|
|
47
|
+
end
|
|
48
|
+
|
|
43
49
|
def check_conversion(value)
|
|
44
50
|
Philosophal.convert(self, value)
|
|
45
51
|
end
|
|
46
52
|
|
|
53
|
+
# The Convertor method (and subtype) for the type, looked up once since the type never changes.
|
|
54
|
+
def convert_method
|
|
55
|
+
return @convert_method if defined?(@convert_method)
|
|
56
|
+
|
|
57
|
+
@convert_method = Philosophal::Convertor.convert_method_for(@type)
|
|
58
|
+
end
|
|
59
|
+
|
|
47
60
|
def method_generators
|
|
48
61
|
generators = {
|
|
49
62
|
writer_method_name => immutable ? :generate_immutable_writer_method : :generate_writer_method,
|
|
50
|
-
reader_method_name => :generate_reader_method
|
|
63
|
+
reader_method_name => default? ? :generate_reader_method : :generate_plain_reader_method
|
|
51
64
|
}
|
|
52
65
|
generators[boolean_method_name] = :generate_boolean_method if type == Philosophal::Types::BooleanType::Instance
|
|
53
66
|
generators[description_method_name] = :generate_description_method if description
|
|
54
|
-
generators
|
|
67
|
+
generators.merge(change_tracking_generators)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def change_tracking_generators
|
|
71
|
+
return {} unless track_changes?
|
|
72
|
+
|
|
73
|
+
{ was_method_name => :generate_was_method, changed_method_name => :generate_changed_method }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Private constant of the generated methods module holding this property, so the writers
|
|
77
|
+
# reach it without looking it up in the schema.
|
|
78
|
+
def constant_name
|
|
79
|
+
return @constant_name if defined? @constant_name
|
|
80
|
+
|
|
81
|
+
@constant_name = :"PHILOSOPHAL_PROPERTY_#{@name.name}"
|
|
55
82
|
end
|
|
56
83
|
|
|
57
84
|
def writer_method_name
|
|
@@ -63,7 +90,7 @@ module Philosophal
|
|
|
63
90
|
def generate_writer_method(buffer)
|
|
64
91
|
buffer << <<~RUBY
|
|
65
92
|
def #{writer_method_name.name}(value)
|
|
66
|
-
@#{@name.name} =
|
|
93
|
+
@#{@name.name} = #{constant_name}.check_conversion(value)
|
|
67
94
|
rescue Philosophal::TypeError => error
|
|
68
95
|
error.set_backtrace(caller(1))
|
|
69
96
|
raise
|
|
@@ -75,7 +102,7 @@ module Philosophal
|
|
|
75
102
|
buffer << <<~RUBY
|
|
76
103
|
def #{writer_method_name.name}(value)
|
|
77
104
|
raise FrozenError, "can't modify frozen variable @#{@name.name}" if defined?(@#{@name.name})
|
|
78
|
-
@#{@name.name} =
|
|
105
|
+
@#{@name.name} = #{constant_name}.check_conversion(value).freeze
|
|
79
106
|
rescue Philosophal::TypeError => error
|
|
80
107
|
error.set_backtrace(caller(1))
|
|
81
108
|
raise
|
|
@@ -87,10 +114,23 @@ module Philosophal
|
|
|
87
114
|
@name
|
|
88
115
|
end
|
|
89
116
|
|
|
117
|
+
def variable_name
|
|
118
|
+
return @variable_name if defined? @variable_name
|
|
119
|
+
|
|
120
|
+
@variable_name = :"@#{@name.name}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Without a default the reader only returns the variable: attr_reader is the fastest form.
|
|
124
|
+
# The default is not looked up, so a subclass redeclaring the property with a default and
|
|
125
|
+
# override: false keeps this reader and does not get its default (not supported).
|
|
126
|
+
def generate_plain_reader_method(buffer)
|
|
127
|
+
buffer << "attr_reader :#{reader_method_name.name}\n"
|
|
128
|
+
end
|
|
129
|
+
|
|
90
130
|
def generate_reader_method(buffer)
|
|
91
131
|
buffer << <<~RUBY
|
|
92
132
|
def #{reader_method_name.name}
|
|
93
|
-
@#{@name.name}.nil? ?
|
|
133
|
+
@#{@name.name}.nil? ? #{constant_name}.default_value : @#{@name.name}
|
|
94
134
|
end
|
|
95
135
|
RUBY
|
|
96
136
|
end
|
|
@@ -115,12 +155,48 @@ module Philosophal
|
|
|
115
155
|
@description_method_name = :"#{@name.name}_description"
|
|
116
156
|
end
|
|
117
157
|
|
|
118
|
-
|
|
158
|
+
def was_method_name
|
|
159
|
+
return @was_method_name if defined? @was_method_name
|
|
160
|
+
|
|
161
|
+
@was_method_name = :"#{@name.name}_was"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def was_variable_name
|
|
165
|
+
return @was_variable_name if defined? @was_variable_name
|
|
166
|
+
|
|
167
|
+
@was_variable_name = :"@__philosophal_#{@name.name}_was__"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Returns the stored value at the last changes_applied (nil before), without falling back to the default.
|
|
171
|
+
def generate_was_method(buffer)
|
|
172
|
+
buffer << <<~RUBY
|
|
173
|
+
def #{was_method_name.name}
|
|
174
|
+
#{was_variable_name}
|
|
175
|
+
end
|
|
176
|
+
RUBY
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def changed_method_name
|
|
180
|
+
return @changed_method_name if defined? @changed_method_name
|
|
181
|
+
|
|
182
|
+
@changed_method_name = :"#{@name.name}_changed?"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# True when the stored value differs from the one recorded at the last changes_applied.
|
|
186
|
+
def generate_changed_method(buffer)
|
|
187
|
+
buffer << <<~RUBY
|
|
188
|
+
def #{changed_method_name.name}
|
|
189
|
+
@#{@name.name} != #{was_variable_name}
|
|
190
|
+
end
|
|
191
|
+
RUBY
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# The description is read from the property constant rather than written into the generated code,
|
|
119
195
|
# so it is never evaluated as Ruby.
|
|
120
196
|
def generate_description_method(buffer)
|
|
121
197
|
buffer << <<~RUBY
|
|
122
198
|
def #{description_method_name.name}
|
|
123
|
-
|
|
199
|
+
#{constant_name}.description
|
|
124
200
|
end
|
|
125
201
|
RUBY
|
|
126
202
|
end
|
|
@@ -22,9 +22,11 @@ module Philosophal
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def ===(hash)
|
|
25
|
-
hash.is_a?(Hash)
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
return false unless hash.is_a?(Hash)
|
|
26
|
+
|
|
27
|
+
key_type = @subtype[:key_type]
|
|
28
|
+
value_type = @subtype[:value_type]
|
|
29
|
+
hash.all? { |key, value| key.is_a?(key_type) && value.is_a?(value_type) }
|
|
28
30
|
end
|
|
29
31
|
end
|
|
30
32
|
end
|
data/lib/philosophal.rb
CHANGED
|
@@ -22,14 +22,14 @@ module Philosophal
|
|
|
22
22
|
# nil means "no value": it is neither converted nor transformed, so the reader falls back to the default.
|
|
23
23
|
return if value.nil?
|
|
24
24
|
|
|
25
|
-
converted = property.type === value ? value : convert_to_type(property
|
|
25
|
+
converted = property.type === value ? value : convert_to_type(property, value) # rubocop:disable Style/CaseEquality
|
|
26
26
|
return converted unless property.transform?
|
|
27
27
|
|
|
28
28
|
Transform.make(property.transform, converted)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
def self.convert_to_type(
|
|
32
|
-
convert_method, subtype =
|
|
31
|
+
def self.convert_to_type(property, value)
|
|
32
|
+
convert_method, subtype = property.convert_method
|
|
33
33
|
raise Philosophal::TypeError unless convert_method
|
|
34
34
|
|
|
35
35
|
if subtype
|
data/sig/manifest.yaml
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Philosophal
|
|
2
|
+
class Convertor
|
|
3
|
+
METHOD_TYPE_MAP: Hash[untyped, Symbol]
|
|
4
|
+
|
|
5
|
+
def self.convert_method_for: (type_spec type) -> (Symbol | [Symbol?, untyped] | nil)
|
|
6
|
+
|
|
7
|
+
def self.convert_to_symbol: (untyped obj) -> Symbol
|
|
8
|
+
|
|
9
|
+
def self.convert_to_string: (untyped obj) -> String
|
|
10
|
+
|
|
11
|
+
def self.convert_to_integer: (untyped obj) -> Integer
|
|
12
|
+
|
|
13
|
+
def self.convert_to_float: (untyped obj) -> Float
|
|
14
|
+
|
|
15
|
+
def self.convert_to_array: (untyped obj) -> Array[untyped]
|
|
16
|
+
|
|
17
|
+
def self.convert_to_hash: (untyped obj) -> Hash[untyped, untyped]
|
|
18
|
+
|
|
19
|
+
def self.convert_to_time: (untyped obj) -> Time
|
|
20
|
+
|
|
21
|
+
def self.convert_to_date: (untyped obj) -> Date
|
|
22
|
+
|
|
23
|
+
def self.convert_to_date_time: (untyped obj) -> DateTime
|
|
24
|
+
|
|
25
|
+
def self.convert_to_any: [T] (T obj) -> T
|
|
26
|
+
|
|
27
|
+
def self.convert_to_boolean: (untyped obj) -> bool
|
|
28
|
+
|
|
29
|
+
def self.convert_to_pathname: (untyped obj) -> Pathname
|
|
30
|
+
|
|
31
|
+
def self.convert_to_array_of: (untyped obj, Module subtype) -> Array[untyped]
|
|
32
|
+
|
|
33
|
+
def self.convert_to_hash_of: (untyped obj, Types::HashOfType::subtype subtype) -> Hash[untyped, untyped]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Philosophal
|
|
2
|
+
module Loaders
|
|
3
|
+
class JsonLoader
|
|
4
|
+
@klass: untyped
|
|
5
|
+
|
|
6
|
+
def initialize: (Properties klass) -> void
|
|
7
|
+
|
|
8
|
+
# A Hash or a JSON object string returns one instance, an Array or a JSON array string returns a list,
|
|
9
|
+
# any other String is read as a path to a JSON file.
|
|
10
|
+
def load: (Hash[Symbol, untyped] json) -> untyped
|
|
11
|
+
| (Array[Hash[Symbol, untyped]] json) -> Array[untyped]
|
|
12
|
+
| (String json) -> untyped
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def load_from_array: (Array[Hash[Symbol, untyped]] array) -> Array[untyped]
|
|
17
|
+
|
|
18
|
+
def load_from_hash: (Hash[Symbol, untyped] hash) -> untyped
|
|
19
|
+
|
|
20
|
+
def setter_from_key: (Symbol key) -> Symbol
|
|
21
|
+
|
|
22
|
+
def load_from_json: (String str) -> untyped
|
|
23
|
+
|
|
24
|
+
def load_from_json_collection: (String json) -> Array[untyped]
|
|
25
|
+
|
|
26
|
+
def load_from_path: (String path) -> untyped
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Philosophal
|
|
2
|
+
module Properties
|
|
3
|
+
# Included in the instances of the classes that declare properties.
|
|
4
|
+
module InstanceMethods
|
|
5
|
+
def cprop?: (String | Symbol property_name, untyped klass) -> bool
|
|
6
|
+
|
|
7
|
+
def philosophal_inspect: (?boolish light) -> String
|
|
8
|
+
|
|
9
|
+
alias cprop_inspect philosophal_inspect
|
|
10
|
+
|
|
11
|
+
def philosophal_inspect_map: () -> Hash[Symbol, untyped]
|
|
12
|
+
|
|
13
|
+
alias cprop_inspect_map philosophal_inspect_map
|
|
14
|
+
|
|
15
|
+
def philosophal_values: () -> Array[Property]
|
|
16
|
+
|
|
17
|
+
def changed_properties: () -> Hash[Symbol, untyped]
|
|
18
|
+
|
|
19
|
+
def changes_applied: () -> self
|
|
20
|
+
|
|
21
|
+
def to_json_hash: () -> Hash[Symbol, untyped]
|
|
22
|
+
|
|
23
|
+
def to_json: (*untyped _args) -> String
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Philosophal
|
|
2
|
+
module Properties
|
|
3
|
+
class Schema
|
|
4
|
+
include Enumerable[Property]
|
|
5
|
+
|
|
6
|
+
@properties_index: Hash[Symbol, Property]
|
|
7
|
+
@mutex: Thread::Mutex
|
|
8
|
+
@immutables: Hash[Symbol, Property]
|
|
9
|
+
@mutables: Hash[Symbol, Property]
|
|
10
|
+
@json_names_h: Hash[Symbol, Symbol]
|
|
11
|
+
@json_setters: Hash[Symbol, Symbol]
|
|
12
|
+
@json_properties: Array[Property]
|
|
13
|
+
@tracked: Array[Property]
|
|
14
|
+
|
|
15
|
+
def initialize: (?properties_index: Hash[Symbol, Property]) -> void
|
|
16
|
+
|
|
17
|
+
def initialize_copy: (Schema source) -> void
|
|
18
|
+
|
|
19
|
+
attr_reader properties_index: Hash[Symbol, Property]
|
|
20
|
+
|
|
21
|
+
def []: (Symbol? key) -> Property?
|
|
22
|
+
|
|
23
|
+
def keys: () -> Array[Symbol]
|
|
24
|
+
|
|
25
|
+
def each: () { (Property) -> void } -> self
|
|
26
|
+
| () -> Enumerator[Property, self]
|
|
27
|
+
|
|
28
|
+
def <<: (Property value) -> self
|
|
29
|
+
|
|
30
|
+
def immutables: () -> Hash[Symbol, Property]
|
|
31
|
+
|
|
32
|
+
def mutables: () -> Hash[Symbol, Property]
|
|
33
|
+
|
|
34
|
+
def json_names_h: () -> Hash[Symbol, Symbol]
|
|
35
|
+
|
|
36
|
+
def json_setters: () -> Hash[Symbol, Symbol]
|
|
37
|
+
|
|
38
|
+
def json_properties: () -> Array[Property]
|
|
39
|
+
|
|
40
|
+
def tracked: () -> Array[Property]
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def clear_caches: () -> void
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module Philosophal
|
|
2
|
+
# Extended by the classes that declare properties.
|
|
3
|
+
module Properties : Module
|
|
4
|
+
include Philosophal::Types
|
|
5
|
+
|
|
6
|
+
@philosophal_properties: Schema
|
|
7
|
+
@philosophal_descriptions: Hash[Symbol, String]
|
|
8
|
+
@json: Loaders::JsonLoader
|
|
9
|
+
@__philosophal_extension__: Module
|
|
10
|
+
|
|
11
|
+
def cprop: (
|
|
12
|
+
Symbol name,
|
|
13
|
+
type_spec `type`,
|
|
14
|
+
?default: untyped,
|
|
15
|
+
?transform: transform?,
|
|
16
|
+
?immutable: bool,
|
|
17
|
+
?description: String?,
|
|
18
|
+
?json: bool | String | Symbol,
|
|
19
|
+
?override: bool,
|
|
20
|
+
?track_changes: bool
|
|
21
|
+
) -> untyped
|
|
22
|
+
|
|
23
|
+
def philosophal_properties: () -> Schema
|
|
24
|
+
|
|
25
|
+
def philosophal_descriptions: () -> Hash[Symbol, String]
|
|
26
|
+
|
|
27
|
+
alias cprop_descriptions philosophal_descriptions
|
|
28
|
+
|
|
29
|
+
def json: () -> Loaders::JsonLoader
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def __philosophal_property_class__: () -> singleton(Property)
|
|
34
|
+
|
|
35
|
+
def __define_philosophal_methods__: (Property new_property) -> untyped
|
|
36
|
+
|
|
37
|
+
def __set_philosophal_property_constant__: (Property property) -> void
|
|
38
|
+
|
|
39
|
+
def __philosophal_extension__: () -> Module
|
|
40
|
+
|
|
41
|
+
def __generate_philosophal_methods__: (Property new_property) -> String
|
|
42
|
+
|
|
43
|
+
def __remove_philosophal_own_method__: (Symbol method_name) -> void
|
|
44
|
+
|
|
45
|
+
def __check_philosophal_options__: (
|
|
46
|
+
Symbol name,
|
|
47
|
+
transform: untyped,
|
|
48
|
+
immutable: untyped,
|
|
49
|
+
override: untyped,
|
|
50
|
+
description: untyped,
|
|
51
|
+
json: untyped,
|
|
52
|
+
track_changes: untyped
|
|
53
|
+
) -> void
|
|
54
|
+
|
|
55
|
+
def __check_philosophal_transform_option__: (Symbol name, untyped transform) -> void
|
|
56
|
+
|
|
57
|
+
def __check_philosophal_boolean_option__: (Symbol option_name, untyped value) -> void
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
module Philosophal
|
|
2
|
+
class Property
|
|
3
|
+
@writer_method_name: Symbol
|
|
4
|
+
@constant_name: Symbol
|
|
5
|
+
@boolean_method_name: Symbol
|
|
6
|
+
@description_method_name: Symbol
|
|
7
|
+
@was_method_name: Symbol
|
|
8
|
+
@changed_method_name: Symbol
|
|
9
|
+
@variable_name: Symbol
|
|
10
|
+
@was_variable_name: Symbol
|
|
11
|
+
@convert_method: Symbol | [Symbol?, untyped] | nil
|
|
12
|
+
|
|
13
|
+
def initialize: (
|
|
14
|
+
name: Symbol,
|
|
15
|
+
type: type_spec,
|
|
16
|
+
default: untyped,
|
|
17
|
+
transform: transform?,
|
|
18
|
+
immutable: bool,
|
|
19
|
+
description: String?,
|
|
20
|
+
json: bool | Symbol,
|
|
21
|
+
override: bool,
|
|
22
|
+
?track_changes: bool
|
|
23
|
+
) -> void
|
|
24
|
+
|
|
25
|
+
attr_reader name: Symbol
|
|
26
|
+
|
|
27
|
+
attr_reader type: type_spec
|
|
28
|
+
|
|
29
|
+
attr_reader default: untyped
|
|
30
|
+
|
|
31
|
+
attr_reader transform: transform?
|
|
32
|
+
|
|
33
|
+
attr_reader immutable: bool
|
|
34
|
+
|
|
35
|
+
attr_reader description: String?
|
|
36
|
+
|
|
37
|
+
attr_reader json: bool | Symbol
|
|
38
|
+
|
|
39
|
+
attr_reader override: bool
|
|
40
|
+
|
|
41
|
+
attr_reader track_changes: bool
|
|
42
|
+
|
|
43
|
+
def json_name: () -> Symbol
|
|
44
|
+
|
|
45
|
+
def default?: () -> bool
|
|
46
|
+
|
|
47
|
+
def default_value: () -> untyped
|
|
48
|
+
|
|
49
|
+
def transform?: () -> bool
|
|
50
|
+
|
|
51
|
+
def override?: () -> bool
|
|
52
|
+
|
|
53
|
+
def track_changes?: () -> bool
|
|
54
|
+
|
|
55
|
+
def check_conversion: (untyped value) -> untyped
|
|
56
|
+
|
|
57
|
+
def convert_method: () -> (Symbol | [Symbol?, untyped] | nil)
|
|
58
|
+
|
|
59
|
+
# Maps each method name to define to the generator that writes its code.
|
|
60
|
+
def method_generators: () -> Hash[Symbol, Symbol]
|
|
61
|
+
|
|
62
|
+
def change_tracking_generators: () -> Hash[Symbol, Symbol]
|
|
63
|
+
|
|
64
|
+
def constant_name: () -> Symbol
|
|
65
|
+
|
|
66
|
+
def writer_method_name: () -> Symbol
|
|
67
|
+
|
|
68
|
+
def generate_writer_method: (String buffer) -> String
|
|
69
|
+
|
|
70
|
+
def generate_immutable_writer_method: (String buffer) -> String
|
|
71
|
+
|
|
72
|
+
def reader_method_name: () -> Symbol
|
|
73
|
+
|
|
74
|
+
def variable_name: () -> Symbol
|
|
75
|
+
|
|
76
|
+
def generate_plain_reader_method: (String buffer) -> String
|
|
77
|
+
|
|
78
|
+
def generate_reader_method: (String buffer) -> String
|
|
79
|
+
|
|
80
|
+
def boolean_method_name: () -> Symbol
|
|
81
|
+
|
|
82
|
+
def generate_boolean_method: (String buffer) -> String
|
|
83
|
+
|
|
84
|
+
def description_method_name: () -> Symbol
|
|
85
|
+
|
|
86
|
+
def generate_description_method: (String buffer) -> String
|
|
87
|
+
|
|
88
|
+
def was_method_name: () -> Symbol
|
|
89
|
+
|
|
90
|
+
def was_variable_name: () -> Symbol
|
|
91
|
+
|
|
92
|
+
def generate_was_method: (String buffer) -> String
|
|
93
|
+
|
|
94
|
+
def changed_method_name: () -> Symbol
|
|
95
|
+
|
|
96
|
+
def generate_changed_method: (String buffer) -> String
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Philosophal
|
|
2
|
+
module Types
|
|
3
|
+
class ArrayOfType
|
|
4
|
+
self.@memorization: Hash[Module, ArrayOfType]
|
|
5
|
+
|
|
6
|
+
def self.instance: (Module subtype) -> ArrayOfType
|
|
7
|
+
|
|
8
|
+
def self.memorization: () -> Hash[Module, ArrayOfType]
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def self.new: (Module subtype) -> ArrayOfType
|
|
13
|
+
|
|
14
|
+
public
|
|
15
|
+
|
|
16
|
+
attr_reader subtype: Module
|
|
17
|
+
|
|
18
|
+
def initialize: (Module subtype) -> void
|
|
19
|
+
|
|
20
|
+
def ===: (untyped array) -> bool
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Philosophal
|
|
2
|
+
module Types
|
|
3
|
+
class HashOfType
|
|
4
|
+
type subtype = { key_type: Module, value_type: Module }
|
|
5
|
+
|
|
6
|
+
self.@memorization: Hash[[Module, Module], HashOfType]
|
|
7
|
+
|
|
8
|
+
def self.instance: (Module key_type, Module value_type) -> HashOfType
|
|
9
|
+
|
|
10
|
+
def self.memorization: () -> Hash[[Module, Module], HashOfType]
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def self.new: (subtype subtype) -> HashOfType
|
|
15
|
+
|
|
16
|
+
public
|
|
17
|
+
|
|
18
|
+
attr_reader subtype: subtype
|
|
19
|
+
|
|
20
|
+
def initialize: (subtype subtype) -> void
|
|
21
|
+
|
|
22
|
+
def ===: (untyped hash) -> bool
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/sig/philosophal.rbs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Philosophal
|
|
2
|
+
# A type accepted by cprop: a class (String, Integer, ...) or one of the Philosophal::Types helpers.
|
|
3
|
+
type type_spec = Module | Types::AnyType | Types::BooleanType | Types::ArrayOfType | Types::HashOfType
|
|
4
|
+
|
|
5
|
+
type transform = Symbol | ^(untyped) -> untyped
|
|
6
|
+
|
|
7
|
+
def self.convert: (Property property, untyped value) -> untyped
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def self.convert_to_type: (Property property, untyped value) -> untyped
|
|
12
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philosophal
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Maxime Désécot
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-11-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: json
|
|
@@ -52,6 +52,24 @@ files:
|
|
|
52
52
|
- lib/philosophal/types/array_of_type.rb
|
|
53
53
|
- lib/philosophal/types/boolean_type.rb
|
|
54
54
|
- lib/philosophal/types/hash_of_type.rb
|
|
55
|
+
- sig/manifest.yaml
|
|
56
|
+
- sig/philosophal.rbs
|
|
57
|
+
- sig/philosophal/convertor.rbs
|
|
58
|
+
- sig/philosophal/errors/argument_error.rbs
|
|
59
|
+
- sig/philosophal/errors/type_error.rbs
|
|
60
|
+
- sig/philosophal/loaders.rbs
|
|
61
|
+
- sig/philosophal/loaders/errors.rbs
|
|
62
|
+
- sig/philosophal/loaders/json_loader.rbs
|
|
63
|
+
- sig/philosophal/properties.rbs
|
|
64
|
+
- sig/philosophal/properties/instance_methods.rbs
|
|
65
|
+
- sig/philosophal/properties/schema.rbs
|
|
66
|
+
- sig/philosophal/property.rbs
|
|
67
|
+
- sig/philosophal/transform.rbs
|
|
68
|
+
- sig/philosophal/types.rbs
|
|
69
|
+
- sig/philosophal/types/any_type.rbs
|
|
70
|
+
- sig/philosophal/types/array_of_type.rbs
|
|
71
|
+
- sig/philosophal/types/boolean_type.rbs
|
|
72
|
+
- sig/philosophal/types/hash_of_type.rbs
|
|
55
73
|
homepage: https://github.com/RaoH37/philosophal
|
|
56
74
|
licenses:
|
|
57
75
|
- GPL-3.0-only
|