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 +4 -4
- data/CHANGELOG.md +11 -1
- data/lib/philosophal/convertor.rb +1 -1
- data/lib/philosophal/loaders/errors.rb +7 -0
- data/lib/philosophal/loaders/json_loader.rb +75 -0
- data/lib/philosophal/loaders.rb +8 -0
- data/lib/philosophal/properties/instance_methods.rb +56 -0
- data/lib/philosophal/properties/schema.rb +31 -2
- data/lib/philosophal/properties.rb +61 -64
- data/lib/philosophal/property.rb +93 -53
- data/lib/philosophal/transform.rb +8 -6
- data/lib/philosophal/types/array_of_type.rb +3 -2
- data/lib/philosophal/types/hash_of_type.rb +3 -3
- data/lib/philosophal.rb +17 -17
- metadata +28 -14
- data/.rubocop.yml +0 -9
- data/.ruby-version +0 -1
- data/.tool-versions +0 -1
- data/Rakefile +0 -12
- data/VERSION +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d1bb80908d227d997cf25e816e00335f9c61328aa4eb285ee7c51632adfcc9cb
|
|
4
|
+
data.tar.gz: 56ca323ff898cb43b8604850e0ea94cd5b323c236d365a70ad8d36e9321bd01f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2cf9f37bd111eb6ca5c021a5fefd0da9b7cf48e624adf5ac9d12c57f885315b327ae1539946744317fbd120623455b82fe5d9aa3186a0613d9cb72f2102785ec
|
|
7
|
+
data.tar.gz: 70f39c48ee39f045a7208e141b1cd56c7cff8f6871e17a855628d2b85007ee7fe850df89fa084615eb26585078a5f0b4e2623e45b1d0befb59c4a9f62af4ba3c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
## [
|
|
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.
|
|
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,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,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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
+
def __check_philosophal_transform_option__(name, transform)
|
|
111
|
+
return if !transform || transform.is_a?(Proc) || transform.is_a?(Symbol)
|
|
117
112
|
|
|
118
|
-
|
|
113
|
+
raise Philosophal::ArgumentError, "transform param must be a Symbol object or a Proc (#{name})."
|
|
114
|
+
end
|
|
119
115
|
|
|
120
|
-
|
|
116
|
+
def __check_philosophal_boolean_option__(option_name, value)
|
|
117
|
+
return if Philosophal::Types::BooleanType::TRUE_FALSE_SET.include?(value)
|
|
121
118
|
|
|
122
|
-
|
|
119
|
+
raise Philosophal::ArgumentError, "#{option_name} param must be true or false."
|
|
123
120
|
end
|
|
124
121
|
end
|
|
125
122
|
end
|
data/lib/philosophal/property.rb
CHANGED
|
@@ -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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
else
|
|
34
|
-
Convertor.send(convert_method, value)
|
|
35
|
-
end
|
|
28
|
+
Transform.make(property.transform, converted)
|
|
29
|
+
end
|
|
36
30
|
|
|
37
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Maxime Désécot
|
|
8
8
|
autorequire:
|
|
9
|
-
bindir:
|
|
9
|
+
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
12
|
-
dependencies:
|
|
13
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
78
|
+
rubygems_version: 3.5.22
|
|
65
79
|
signing_key:
|
|
66
80
|
specification_version: 4
|
|
67
|
-
summary:
|
|
81
|
+
summary: Typed, auto-converting properties for Ruby classes
|
|
68
82
|
test_files: []
|
data/.rubocop.yml
DELETED
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
data/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.1.0
|