philosophal 1.1.0 → 1.3.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: d09ac65f9fd7fa000c82fa1d9e77c85e22866d2a9a1ffdfa1a63e19b36cb909b
4
- data.tar.gz: bdf93344392beabea229e214b1403e88749fd51839e68c9e675fb9566fad5e52
3
+ metadata.gz: d1bb80908d227d997cf25e816e00335f9c61328aa4eb285ee7c51632adfcc9cb
4
+ data.tar.gz: 56ca323ff898cb43b8604850e0ea94cd5b323c236d365a70ad8d36e9321bd01f
5
5
  SHA512:
6
- metadata.gz: f729b3f0a820e25d39e9de2eecd47cc7cd7bc12fa32cdc6306c81b02418f98507a3d814582f638822834dc3f9cae055b00db909a3cc66c3112c7ccb8118aaf90
7
- data.tar.gz: a1ab12ea326c278c6d7eab6b555605642f0ce34d30d27782efe4a6991cbd3fc3b63917cbef00a8b21133b47e096056b2ba34234b40017239d556880960fa67f0
6
+ metadata.gz: 2cf9f37bd111eb6ca5c021a5fefd0da9b7cf48e624adf5ac9d12c57f885315b327ae1539946744317fbd120623455b82fe5d9aa3186a0613d9cb72f2102785ec
7
+ data.tar.gz: 70f39c48ee39f045a7208e141b1cd56c7cff8f6871e17a855628d2b85007ee7fe850df89fa084615eb26585078a5f0b4e2623e45b1d0befb59c4a9f62af4ba3c
data/CHANGELOG.md CHANGED
@@ -1,4 +1,14 @@
1
- ## [Unreleased]
1
+ ## [1.3.0] - 2026-10-19
2
+
3
+ - Breaking: minimum required Ruby version is now **3.3** (was 3.1)
4
+ - Breaking: assigning `nil` no longer converts it (e.g. to `0` or `""`); the property is reset and its reader returns the default value
5
+ - Fix: converting a Numeric to **DateTime** no longer requires ActiveSupport (`Time.zone`)
6
+ - Fix: properties declared in a subclass no longer leak into the parent class
7
+ - Fix: a property set to `false` is no longer replaced by its default value, and predicate methods (`prop?`) take the default into account
8
+ - Fix: **ArrayOf** and **HashOf** instances are now really shared for the same subtypes, and **HashOf** accepts anonymous classes
9
+ - Fix: descriptions are no longer evaluated as Ruby code (parentheses or `#{...}` in a description broke or ran code)
10
+ - Fix: `json.load` no longer modifies the given hash or array, and accepts frozen hashes
11
+ - Fix: `override: true` now also replaces a method defined in the class itself before `cprop`
2
12
 
3
13
  ## [1.0.0] - 2025-03-24
4
14
 
@@ -87,7 +87,7 @@ module Philosophal
87
87
  return obj.to_datetime if obj.respond_to?(:to_datetime)
88
88
  return DateTime.new(*obj) if obj.is_a?(Array)
89
89
  return DateTime.parse(obj) if obj.is_a?(String)
90
- return Time.zone.at(obj).to_datetime if obj.is_a?(Numeric)
90
+ return Time.at(obj).to_datetime if obj.is_a?(Numeric)
91
91
 
92
92
  raise Philosophal::TypeError
93
93
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philosophal
4
+ module Loaders
5
+ class LoadError < StandardError; end
6
+ end
7
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philosophal
4
+ module Loaders
5
+ class JsonLoader
6
+ def initialize(klass)
7
+ @klass = klass
8
+ end
9
+
10
+ def load(json)
11
+ return load_from_hash(json) if json.is_a?(Hash)
12
+ return load_from_array(json) if json.is_a?(Array)
13
+
14
+ raise LoadError, 'invalid json parameter' unless json.is_a?(String)
15
+
16
+ first_char = json[0]
17
+
18
+ return load_from_json(json) if first_char == '{'
19
+
20
+ return load_from_json_collection(json) if first_char == '['
21
+
22
+ load_from_path(json)
23
+ end
24
+
25
+ private
26
+
27
+ def load_from_array(array)
28
+ raise LoadError, 'invalid json parameter' if array.find { |hash| !hash.is_a?(Hash) }
29
+
30
+ array.map do |hash|
31
+ load_from_hash(hash)
32
+ end
33
+ end
34
+
35
+ def load_from_hash(hash)
36
+ obj = @klass.new
37
+
38
+ hash.each do |key, value|
39
+ setter = setter_from_key(key)
40
+ next unless obj.respond_to?(setter)
41
+
42
+ obj.public_send(setter, value)
43
+ end
44
+
45
+ obj
46
+ end
47
+
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}="
55
+ end
56
+
57
+ def load_from_json(str)
58
+ hash = JSON.parse(str, symbolize_names: true)
59
+ load_from_hash(hash)
60
+ end
61
+
62
+ def load_from_json_collection(json)
63
+ array = JSON.parse(json, symbolize_names: true)
64
+ load_from_array(array)
65
+ end
66
+
67
+ def load_from_path(path)
68
+ raise LoadError, "no such file #{path}" unless File.exist?(path)
69
+
70
+ str = File.read(path)
71
+ load_from_json(str)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philosophal
4
+ module Loaders
5
+ autoload :JsonLoader, 'philosophal/loaders/json_loader'
6
+ autoload :LoadError, 'philosophal/loaders/errors'
7
+ end
8
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philosophal
4
+ module Properties
5
+ module InstanceMethods
6
+ def cprop?(property_name, klass)
7
+ property = self.class.philosophal_properties.properties_index[property_name.to_sym]
8
+ return false unless property
9
+
10
+ klass = klass.class unless klass.is_a?(Class)
11
+ property.type == klass
12
+ end
13
+
14
+ def philosophal_inspect(light = false)
15
+ keys_map = philosophal_inspect_map
16
+
17
+ if light
18
+ keys_map.reject! do |_, v|
19
+ v.nil? || (v.respond_to?(:empty?) && v.empty?)
20
+ end
21
+ end
22
+
23
+ keys_str = keys_map.map { |k, v| [k, v.inspect].join(': ') }.join(', ')
24
+ "#{self.class}:#{format('0x00%x', object_id << 1)} #{keys_str}"
25
+ end
26
+ alias cprop_inspect philosophal_inspect
27
+
28
+ def philosophal_inspect_map
29
+ philosophal_values.to_h do |property|
30
+ [
31
+ property.name,
32
+ send(property.name)
33
+ ]
34
+ end
35
+ end
36
+ alias cprop_inspect_map philosophal_inspect_map
37
+
38
+ def philosophal_values
39
+ self.class.philosophal_properties.properties_index.values
40
+ end
41
+
42
+ def to_json_hash
43
+ philosophal_values.select(&:json).to_h do |property|
44
+ [
45
+ property.json_name,
46
+ send(property.name)
47
+ ]
48
+ end
49
+ end
50
+
51
+ def to_json(*_args)
52
+ to_json_hash.to_json
53
+ end
54
+ end
55
+ end
56
+ end
@@ -10,6 +10,14 @@ module Philosophal
10
10
  @mutex = Mutex.new
11
11
  end
12
12
 
13
+ # Called by #dup: a subclass gets its own index, so its properties do not leak into the parent.
14
+ def initialize_copy(source)
15
+ super
16
+ @properties_index = source.properties_index.dup
17
+ @mutex = Mutex.new
18
+ clear_caches
19
+ end
20
+
13
21
  attr_reader :properties_index
14
22
 
15
23
  def [](key)
@@ -23,17 +31,38 @@ module Philosophal
23
31
  def <<(value)
24
32
  @mutex.synchronize do
25
33
  @properties_index[value.name] = value
34
+ clear_caches
26
35
  end
27
36
 
28
37
  self
29
38
  end
30
39
 
31
40
  def immutables
32
- @properties_index.select { |_, v| v.immutable }
41
+ return @immutables if defined?(@immutables)
42
+
43
+ @immutables = @properties_index.select { |_, v| v.immutable }
33
44
  end
34
45
 
35
46
  def mutables
36
- @properties_index.reject { |_, v| v.immutable }
47
+ return @mutables if defined?(@mutables)
48
+
49
+ @mutables = @properties_index.reject { |_, v| v.immutable }
50
+ end
51
+
52
+ def json_names_h
53
+ return @json_names_h if defined?(@json_names_h)
54
+
55
+ @json_names_h = @properties_index.to_h do |key, property|
56
+ [property.json_name, key]
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def clear_caches
63
+ %i[@immutables @mutables @json_names_h].each do |cache|
64
+ remove_instance_variable(cache) if instance_variable_defined?(cache)
65
+ end
37
66
  end
38
67
  end
39
68
  end
@@ -3,27 +3,21 @@
3
3
  module Philosophal
4
4
  module Properties
5
5
  autoload :Schema, 'philosophal/properties/schema'
6
+ autoload :InstanceMethods, 'philosophal/properties/instance_methods'
7
+ autoload :JSONLoader, 'philosophal/loaders/json_loader'
6
8
 
7
9
  include Philosophal::Types
8
10
 
9
- def cprop(name, type, default: nil, transform: nil, immutable: false, description: nil)
10
- default.freeze if default && !(default.is_a?(Proc) || default.frozen?)
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
13
 
12
- if transform && !(transform.is_a?(Proc) || transform.is_a?(Symbol))
13
- raise Philosophal::ArgumentError, "transform param must be a Symbol object or a Proc (#{name})."
14
- end
15
-
16
- unless Philosophal::Types::BooleanType::TRUE_FALSE_SET.include?(immutable)
17
- raise Philosophal::ArgumentError, 'immutable param must be true or false.'
18
- end
19
-
20
- if description
21
- raise Philosophal::ArgumentError, 'description param must be a string.' unless description.is_a?(String)
22
-
23
- description.freeze unless description.frozen?
24
- end
14
+ default.freeze unless default.is_a?(Proc)
15
+ description&.freeze
16
+ json = json.to_sym if json.is_a?(String)
25
17
 
26
- property = __philosophal_property_class__.new(name:, type:, default:, transform:, immutable:, description:)
18
+ property = __philosophal_property_class__.new(
19
+ name:, type:, default:, transform:, immutable:, description:, json:, override:
20
+ )
27
21
 
28
22
  philosophal_properties << property
29
23
  __define_philosophal_methods__(property)
@@ -43,7 +37,9 @@ module Philosophal
43
37
  def philosophal_descriptions
44
38
  return @philosophal_descriptions if defined?(@philosophal_descriptions)
45
39
 
46
- @philosophal_descriptions = philosophal_properties.properties_index.values.select(&:description).to_h do |property|
40
+ @philosophal_descriptions = philosophal_properties.properties_index.values
41
+ .select(&:description)
42
+ .to_h do |property|
47
43
  [
48
44
  property.name,
49
45
  property.description
@@ -52,6 +48,12 @@ module Philosophal
52
48
  end
53
49
  alias cprop_descriptions philosophal_descriptions
54
50
 
51
+ def json
52
+ return @json if defined?(@json)
53
+
54
+ @json = Philosophal::Loaders::JsonLoader.new(self)
55
+ end
56
+
55
57
  private
56
58
 
57
59
  def __philosophal_property_class__
@@ -64,62 +66,57 @@ module Philosophal
64
66
  end
65
67
 
66
68
  def __philosophal_extension__
67
- if defined?(@__philosophal_extension__)
68
- @__philosophal_extension__
69
- else
70
- @__philosophal_extension__ = Module.new do
71
- def cprop?(property_name, klass)
72
- property = self.class.philosophal_properties.properties_index[property_name.to_sym]
73
- return false unless property
74
-
75
- klass = klass.class unless klass.is_a?(Class)
76
- property.type == klass
77
- end
78
-
79
- def philosophal_inspect(light = false)
80
- keys_map = philosophal_inspect_map
81
-
82
- if light
83
- keys_map.reject! do |_, v|
84
- v.nil? || (v.respond_to?(:empty?) && v.empty?)
85
- end
86
- end
87
-
88
- keys_str = keys_map.map { |k, v| [k, v.inspect].join(': ') }.join(', ')
89
- "#{self.class}:#{format('0x00%x', (object_id << 1))} #{keys_str}"
90
- end
91
- alias cprop_inspect philosophal_inspect
92
-
93
- def philosophal_inspect_map
94
- self.class.philosophal_properties.properties_index.values.to_h do |property|
95
- [
96
- property.name,
97
- send(property.name)
98
- ]
99
- end
100
- end
101
- alias cprop_inspect_map philosophal_inspect_map
102
- end
69
+ @__philosophal_extension__ ||= Module.new { include Philosophal::Properties::InstanceMethods }
70
+ end
71
+
72
+ def __generate_philosophal_methods__(new_property)
73
+ buffer = +''
74
+
75
+ new_property.method_generators.each do |method_name, generator|
76
+ next unless new_property.override? || !method_defined?(method_name)
77
+
78
+ __remove_philosophal_own_method__(method_name) if new_property.override?
79
+ new_property.public_send(generator, buffer)
103
80
  end
81
+
82
+ # puts buffer
83
+ return buffer if buffer.empty?
84
+
85
+ +"# frozen_string_philosophal: true\n" << buffer
86
+ end
87
+
88
+ # 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.
90
+ def __remove_philosophal_own_method__(method_name)
91
+ return unless method_defined?(method_name, false) || private_method_defined?(method_name, false)
92
+
93
+ remove_method(method_name)
104
94
  end
105
95
 
106
- def __generate_philosophal_methods__(new_property, buffer = +'')
107
- buffer << "# frozen_string_philosophal: true\n"
108
- if new_property.immutable
109
- new_property.generate_immutable_writer_method(buffer)
110
- else
111
- new_property.generate_writer_method(buffer)
96
+ def __check_philosophal_options__(name, transform:, immutable:, override:, description:, json:)
97
+ __check_philosophal_transform_option__(name, transform)
98
+ __check_philosophal_boolean_option__(:override, override)
99
+ __check_philosophal_boolean_option__(:immutable, immutable)
100
+
101
+ if description && !description.is_a?(String)
102
+ raise Philosophal::ArgumentError, 'description param must be a string.'
112
103
  end
113
104
 
114
- new_property.generate_reader_method(buffer)
105
+ return if !json || json == true || json.is_a?(String) || json.is_a?(Symbol)
106
+
107
+ raise Philosophal::ArgumentError, 'json param must be true or a String or a Symbol.'
108
+ end
115
109
 
116
- new_property.generate_boolean_method(buffer) if new_property.type == Philosophal::Types::BooleanType::Instance
110
+ def __check_philosophal_transform_option__(name, transform)
111
+ return if !transform || transform.is_a?(Proc) || transform.is_a?(Symbol)
117
112
 
118
- new_property.generate_description_method(buffer) if new_property.description
113
+ raise Philosophal::ArgumentError, "transform param must be a Symbol object or a Proc (#{name})."
114
+ end
119
115
 
120
- # puts buffer
116
+ def __check_philosophal_boolean_option__(option_name, value)
117
+ return if Philosophal::Types::BooleanType::TRUE_FALSE_SET.include?(value)
121
118
 
122
- buffer
119
+ raise Philosophal::ArgumentError, "#{option_name} param must be true or false."
123
120
  end
124
121
  end
125
122
  end
@@ -2,16 +2,24 @@
2
2
 
3
3
  module Philosophal
4
4
  class Property
5
- def initialize(name:, type:, default:, transform:, immutable:, description:)
5
+ def initialize(name:, type:, default:, transform:, immutable:, description:, json:, override:)
6
6
  @name = name
7
7
  @type = type
8
8
  @default = default
9
9
  @transform = transform
10
10
  @immutable = immutable
11
11
  @description = description
12
+ @json = json
13
+ @override = override
12
14
  end
13
15
 
14
- attr_reader :name, :type, :default, :transform, :immutable, :description
16
+ attr_reader :name, :type, :default, :transform, :immutable, :description, :json, :override
17
+
18
+ def json_name
19
+ return @json if @json.is_a?(Symbol)
20
+
21
+ @name
22
+ end
15
23
 
16
24
  def default?
17
25
  @default != nil
@@ -28,61 +36,93 @@ module Philosophal
28
36
  @transform != nil
29
37
  end
30
38
 
39
+ def override?
40
+ @override
41
+ end
42
+
31
43
  def check_conversion(value)
32
44
  Philosophal.convert(self, value)
33
45
  end
34
46
 
35
- def generate_writer_method(buffer = +'')
36
- buffer <<
37
- ' def ' << @name.name << "=(value)\n " \
38
- '@' << @name.name << ' = self.class.philosophal_properties[:' <<
39
- @name.name <<
40
- "].check_conversion(value)\n" \
41
- "rescue Philosophal::TypeError => error\n" \
42
- "error.set_backtrace(caller(1))\n raise\n" \
43
- "end\n"
44
- end
45
-
46
- def generate_immutable_writer_method(buffer = +'')
47
- buffer <<
48
- ' def ' << @name.name << "=(value)\n " \
49
- 'raise FrozenError, "can\'t modify frozen variable @' <<
50
- @name.name << '" if defined?(@' << @name.name << ")\n" \
51
- '@' << @name.name << ' = self.class.philosophal_properties[:' <<
52
- @name.name <<
53
- "].check_conversion(value).freeze\n" \
54
- "rescue Philosophal::TypeError => error\n" \
55
- "error.set_backtrace(caller(1))\n raise\n" \
56
- "end\n"
57
- end
58
-
59
- def generate_reader_method(buffer = +'')
60
- buffer <<
61
- ' def ' <<
62
- @name.name <<
63
- "\n " \
64
- '@' << @name.name << ' || self.class.philosophal_properties[:' <<
65
- @name.name <<
66
- "].default_value\n" \
67
- "end\n"
68
- end
69
-
70
- def generate_boolean_method(buffer = +'')
71
- buffer <<
72
- ' def ' <<
73
- @name.name <<
74
- "?\n " \
75
- '!!@' << @name.name <<
76
- "\nend\n"
77
- end
78
-
79
- def generate_description_method(buffer = +'')
80
- buffer <<
81
- ' def ' <<
82
- @name.name <<
83
- "_description\n " \
84
- '%(' << @description << ')' \
85
- "\nend\n"
47
+ def method_generators
48
+ generators = {
49
+ writer_method_name => immutable ? :generate_immutable_writer_method : :generate_writer_method,
50
+ reader_method_name => :generate_reader_method
51
+ }
52
+ generators[boolean_method_name] = :generate_boolean_method if type == Philosophal::Types::BooleanType::Instance
53
+ generators[description_method_name] = :generate_description_method if description
54
+ generators
55
+ end
56
+
57
+ def writer_method_name
58
+ return @writer_method_name if defined? @writer_method_name
59
+
60
+ @writer_method_name = :"#{@name.name}="
61
+ end
62
+
63
+ def generate_writer_method(buffer)
64
+ buffer << <<~RUBY
65
+ def #{writer_method_name.name}(value)
66
+ @#{@name.name} = self.class.philosophal_properties[:#{@name.name}].check_conversion(value)
67
+ rescue Philosophal::TypeError => error
68
+ error.set_backtrace(caller(1))
69
+ raise
70
+ end
71
+ RUBY
72
+ end
73
+
74
+ def generate_immutable_writer_method(buffer)
75
+ buffer << <<~RUBY
76
+ def #{writer_method_name.name}(value)
77
+ 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
79
+ rescue Philosophal::TypeError => error
80
+ error.set_backtrace(caller(1))
81
+ raise
82
+ end
83
+ RUBY
84
+ end
85
+
86
+ def reader_method_name
87
+ @name
88
+ end
89
+
90
+ def generate_reader_method(buffer)
91
+ buffer << <<~RUBY
92
+ def #{reader_method_name.name}
93
+ @#{@name.name}.nil? ? self.class.philosophal_properties[:#{@name.name}].default_value : @#{@name.name}
94
+ end
95
+ RUBY
96
+ end
97
+
98
+ def boolean_method_name
99
+ return @boolean_method_name if defined? @boolean_method_name
100
+
101
+ @boolean_method_name = :"#{@name.name}?"
102
+ end
103
+
104
+ def generate_boolean_method(buffer)
105
+ buffer << <<~RUBY
106
+ def #{boolean_method_name.name}
107
+ !!#{reader_method_name.name}
108
+ end
109
+ RUBY
110
+ end
111
+
112
+ def description_method_name
113
+ return @description_method_name if defined? @description_method_name
114
+
115
+ @description_method_name = :"#{@name.name}_description"
116
+ end
117
+
118
+ # The description is read from the schema rather than written into the generated code,
119
+ # so it is never evaluated as Ruby.
120
+ def generate_description_method(buffer)
121
+ buffer << <<~RUBY
122
+ def #{description_method_name.name}
123
+ self.class.philosophal_properties[:#{@name.name}].description
124
+ end
125
+ RUBY
86
126
  end
87
127
  end
88
128
  end
@@ -3,12 +3,14 @@
3
3
  module Philosophal
4
4
  class Transform
5
5
  def self.make(method, value)
6
- case method
7
- when Proc
8
- method.call(value)
9
- else
10
- value.send(method)
11
- end
6
+ return method.call(value) if method.is_a?(Proc)
7
+
8
+ result = value.send(method)
9
+ self_method?(method) ? value : result
10
+ end
11
+
12
+ def self.self_method?(method)
13
+ method.to_s.end_with?('!')
12
14
  end
13
15
  end
14
16
  end
@@ -4,7 +4,7 @@ module Philosophal
4
4
  module Types
5
5
  class ArrayOfType
6
6
  def self.instance(subtype)
7
- memorization.fetch(subtype, new(subtype).freeze)
7
+ memorization[subtype] ||= new(subtype).freeze
8
8
  end
9
9
 
10
10
  def self.memorization
@@ -13,11 +13,12 @@ module Philosophal
13
13
  @memorization = {}
14
14
  end
15
15
 
16
+ private_class_method :new
17
+
16
18
  attr_reader :subtype
17
19
 
18
20
  def initialize(subtype)
19
21
  @subtype = subtype
20
- self.class.memorization[subtype] = self
21
22
  end
22
23
 
23
24
  def ===(array)
@@ -4,8 +4,7 @@ module Philosophal
4
4
  module Types
5
5
  class HashOfType
6
6
  def self.instance(key_type, value_type)
7
- subtype = { key_type:, value_type: }
8
- memorization.fetch(Marshal.dump(subtype), new(subtype).freeze)
7
+ memorization[[key_type, value_type]] ||= new({ key_type:, value_type: }).freeze
9
8
  end
10
9
 
11
10
  def self.memorization
@@ -14,11 +13,12 @@ module Philosophal
14
13
  @memorization = {}
15
14
  end
16
15
 
16
+ private_class_method :new
17
+
17
18
  attr_reader :subtype
18
19
 
19
20
  def initialize(subtype)
20
21
  @subtype = subtype.freeze
21
- self.class.memorization[Marshal.dump(subtype)] = self
22
22
  end
23
23
 
24
24
  def ===(hash)
data/lib/philosophal.rb CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  require 'date'
4
4
  require 'pathname'
5
- require 'set'
6
5
  require 'time'
6
+ require 'json'
7
7
 
8
8
  module Philosophal
9
9
  autoload :Convertor, 'philosophal/convertor'
@@ -13,30 +13,30 @@ module Philosophal
13
13
  autoload :Property, 'philosophal/property'
14
14
 
15
15
  autoload :Types, 'philosophal/types'
16
+ autoload :Loaders, 'philosophal/loaders'
16
17
 
17
18
  autoload :TypeError, 'philosophal/errors/type_error'
18
19
  autoload :ArgumentError, 'philosophal/errors/argument_error'
19
20
 
20
21
  def self.convert(property, value)
21
- if property.type === value
22
- if property.transform?
23
- Transform.make(property.transform, value)
24
- else
25
- value
26
- end
27
- else
28
- convert_method, subtype = Convertor.convert_method_for(property.type)
29
- raise Philosophal::TypeError unless convert_method
22
+ # nil means "no value": it is neither converted nor transformed, so the reader falls back to the default.
23
+ return if value.nil?
24
+
25
+ converted = property.type === value ? value : convert_to_type(property.type, value) # rubocop:disable Style/CaseEquality
26
+ return converted unless property.transform?
30
27
 
31
- converted = if subtype
32
- Convertor.send(convert_method, value, subtype)
33
- else
34
- Convertor.send(convert_method, value)
35
- end
28
+ Transform.make(property.transform, converted)
29
+ end
36
30
 
37
- return converted unless property.transform?
31
+ def self.convert_to_type(type, value)
32
+ convert_method, subtype = Convertor.convert_method_for(type)
33
+ raise Philosophal::TypeError unless convert_method
38
34
 
39
- Transform.make(property.transform, converted)
35
+ if subtype
36
+ Convertor.send(convert_method, value, subtype)
37
+ else
38
+ Convertor.send(convert_method, value)
40
39
  end
41
40
  end
41
+ private_class_method :convert_to_type
42
42
  end
metadata CHANGED
@@ -1,35 +1,49 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: philosophal
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maxime Désécot
8
8
  autorequire:
9
- bindir: exe
9
+ bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-26 00:00:00.000000000 Z
12
- dependencies: []
13
- description: Auto convert value on setter method call
11
+ date: 2026-10-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ description: 'Declare typed attributes with cprop: values are converted, transformed,
28
+ defaulted or frozen on assignment, with JSON import/export.'
14
29
  email:
15
30
  - maxime.desecot@gmail.com
16
31
  executables: []
17
32
  extensions: []
18
33
  extra_rdoc_files: []
19
34
  files:
20
- - ".rubocop.yml"
21
- - ".ruby-version"
22
- - ".tool-versions"
23
35
  - CHANGELOG.md
24
36
  - LICENSE
25
37
  - README.md
26
- - Rakefile
27
- - VERSION
28
38
  - lib/philosophal.rb
29
39
  - lib/philosophal/convertor.rb
30
40
  - lib/philosophal/errors/argument_error.rb
31
41
  - lib/philosophal/errors/type_error.rb
42
+ - lib/philosophal/loaders.rb
43
+ - lib/philosophal/loaders/errors.rb
44
+ - lib/philosophal/loaders/json_loader.rb
32
45
  - lib/philosophal/properties.rb
46
+ - lib/philosophal/properties/instance_methods.rb
33
47
  - lib/philosophal/properties/schema.rb
34
48
  - lib/philosophal/property.rb
35
49
  - lib/philosophal/transform.rb
@@ -43,7 +57,7 @@ licenses:
43
57
  - GPL-3.0-only
44
58
  metadata:
45
59
  homepage_uri: https://github.com/RaoH37/philosophal
46
- source_code_uri: https://github.com/RaoH37/philosophal
60
+ bug_tracker_uri: https://github.com/RaoH37/philosophal/issues
47
61
  changelog_uri: https://github.com/RaoH37/philosophal/blob/main/CHANGELOG.md
48
62
  rubygems_mfa_required: 'true'
49
63
  post_install_message:
@@ -54,15 +68,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
54
68
  requirements:
55
69
  - - ">="
56
70
  - !ruby/object:Gem::Version
57
- version: '3.1'
71
+ version: '3.3'
58
72
  required_rubygems_version: !ruby/object:Gem::Requirement
59
73
  requirements:
60
74
  - - ">="
61
75
  - !ruby/object:Gem::Version
62
76
  version: '0'
63
77
  requirements: []
64
- rubygems_version: 3.3.27
78
+ rubygems_version: 3.5.22
65
79
  signing_key:
66
80
  specification_version: 4
67
- summary: Philosophal setter convertor
81
+ summary: Typed, auto-converting properties for Ruby classes
68
82
  test_files: []
data/.rubocop.yml DELETED
@@ -1,9 +0,0 @@
1
- require:
2
- - rubocop-minitest
3
- - rubocop-packaging
4
- - rubocop-performance
5
- - rubocop-md
6
-
7
- AllCops:
8
- TargetRubyVersion: 3.1
9
- NewCops: enable
data/.ruby-version DELETED
@@ -1 +0,0 @@
1
- 3.1.7
data/.tool-versions DELETED
@@ -1 +0,0 @@
1
- ruby 3.1.7
data/Rakefile DELETED
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'bundler/gem_tasks'
4
- require 'minitest/test_task'
5
-
6
- Minitest::TestTask.create
7
-
8
- require 'rubocop/rake_task'
9
-
10
- RuboCop::RakeTask.new
11
-
12
- task default: %i[test rubocop]
data/VERSION DELETED
@@ -1 +0,0 @@
1
- 1.1.0