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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d1bb80908d227d997cf25e816e00335f9c61328aa4eb285ee7c51632adfcc9cb
4
- data.tar.gz: 56ca323ff898cb43b8604850e0ea94cd5b323c236d365a70ad8d36e9321bd01f
3
+ metadata.gz: d588fe0288b3b4d953de700a9218a607b4e7fc23d3946a8e34b4a1f90a7f1265
4
+ data.tar.gz: cf23c471421f8661992e575bfef9edb21e2a659962c0f54ec1618f0047b23b87
5
5
  SHA512:
6
- metadata.gz: 2cf9f37bd111eb6ca5c021a5fefd0da9b7cf48e624adf5ac9d12c57f885315b327ae1539946744317fbd120623455b82fe5d9aa3186a0613d9cb72f2102785ec
7
- data.tar.gz: 70f39c48ee39f045a7208e141b1cd56c7cff8f6871e17a855628d2b85007ee7fe850df89fa084615eb26585078a5f0b4e2623e45b1d0befb59c4a9f62af4ba3c
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
- obj_downcase = obj.downcase
106
- return true if obj_downcase == 'true'
107
- return false if obj_downcase == 'false'
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
- convert_to_array(obj).tap do |new_obj|
124
- new_obj.map! do |item|
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
- convert_to_hash(obj).tap do |new_obj|
138
- new_obj.transform_keys! { |key| send(key_convert_method, key) }
139
- new_obj.transform_values! { |value| send(value_convert_method, value) }
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
- real_key = @klass.philosophal_properties.json_names_h[key]
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
- philosophal_values.to_h do |property|
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
- philosophal_values.select(&:json).to_h do |property|
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
- __check_philosophal_options__(name, transform:, immutable:, override:, description:, json:)
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
- return unless method_defined?(method_name, false) || private_method_defined?(method_name, false)
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
- remove_method(method_name)
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.'
@@ -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} = self.class.philosophal_properties[:#{@name.name}].check_conversion(value)
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} = self.class.philosophal_properties[:#{@name.name}].check_conversion(value).freeze
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? ? self.class.philosophal_properties[:#{@name.name}].default_value : @#{@name.name}
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
- # The description is read from the schema rather than written into the generated code,
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
- self.class.philosophal_properties[:#{@name.name}].description
199
+ #{constant_name}.description
124
200
  end
125
201
  RUBY
126
202
  end
@@ -10,7 +10,7 @@ module Philosophal
10
10
  end
11
11
 
12
12
  def self.self_method?(method)
13
- method.to_s.end_with?('!')
13
+ method.end_with?('!')
14
14
  end
15
15
  end
16
16
  end
@@ -22,7 +22,7 @@ module Philosophal
22
22
  end
23
23
 
24
24
  def ===(array)
25
- array.is_a?(Array) && !array.find { |entry| !entry.is_a?(@subtype) }
25
+ array.is_a?(Array) && array.all?(@subtype)
26
26
  end
27
27
  end
28
28
  end
@@ -8,7 +8,7 @@ module Philosophal
8
8
  TRUE_FALSE_SET = Set[true, false]
9
9
 
10
10
  def ===(value)
11
- TRUE_FALSE_SET.include?(value)
11
+ true.equal?(value) || false.equal?(value)
12
12
  end
13
13
 
14
14
  freeze
@@ -22,9 +22,11 @@ module Philosophal
22
22
  end
23
23
 
24
24
  def ===(hash)
25
- hash.is_a?(Hash) &&
26
- !hash.keys.find { |key| !key.is_a?(@subtype[:key_type]) } &&
27
- !hash.values.find { |value| !value.is_a?(@subtype[:value_type]) }
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.type, value) # rubocop:disable Style/CaseEquality
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(type, value)
32
- convert_method, subtype = Convertor.convert_method_for(type)
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,5 @@
1
+ dependencies:
2
+ - name: date
3
+ - name: json
4
+ - name: pathname
5
+ - name: time
@@ -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,4 @@
1
+ module Philosophal
2
+ class ArgumentError < ::ArgumentError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Philosophal
2
+ class TypeError < ::TypeError
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Philosophal
2
+ module Loaders
3
+ class LoadError < StandardError
4
+ end
5
+ end
6
+ 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,4 @@
1
+ module Philosophal
2
+ module Loaders
3
+ end
4
+ 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,7 @@
1
+ module Philosophal
2
+ class Transform
3
+ def self.make: (transform method, untyped value) -> untyped
4
+
5
+ def self.self_method?: (Symbol method) -> bool
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ module Philosophal
2
+ module Types
3
+ class AnyType
4
+ Instance: AnyType
5
+
6
+ def ===: (untyped _value) -> true
7
+ end
8
+ end
9
+ 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,11 @@
1
+ module Philosophal
2
+ module Types
3
+ class BooleanType
4
+ Instance: BooleanType
5
+
6
+ TRUE_FALSE_SET: Set[bool]
7
+
8
+ def ===: (untyped value) -> bool
9
+ end
10
+ end
11
+ 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
@@ -0,0 +1,11 @@
1
+ module Philosophal
2
+ module Types
3
+ def _Any: () -> AnyType
4
+
5
+ def _Boolean: () -> BooleanType
6
+
7
+ def _ArrayOf: (Module subtype) -> ArrayOfType
8
+
9
+ def _HashOf: (Module key_type, Module value_type) -> HashOfType
10
+ end
11
+ end
@@ -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: 1.3.0
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-10-20 00:00:00.000000000 Z
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