philosophal 1.2.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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -1
  3. data/README.md +39 -0
  4. data/lib/philosophal/convertor.rb +9 -12
  5. data/lib/philosophal/loaders/json_loader.rb +4 -9
  6. data/lib/philosophal/properties/instance_methods.rb +76 -0
  7. data/lib/philosophal/properties/schema.rb +48 -0
  8. data/lib/philosophal/properties.rb +66 -88
  9. data/lib/philosophal/property.rb +162 -53
  10. data/lib/philosophal/transform.rb +8 -6
  11. data/lib/philosophal/types/array_of_type.rb +4 -3
  12. data/lib/philosophal/types/boolean_type.rb +1 -1
  13. data/lib/philosophal/types/hash_of_type.rb +8 -6
  14. data/lib/philosophal.rb +15 -17
  15. data/sig/manifest.yaml +5 -0
  16. data/sig/philosophal/convertor.rbs +35 -0
  17. data/sig/philosophal/errors/argument_error.rbs +4 -0
  18. data/sig/philosophal/errors/type_error.rbs +4 -0
  19. data/sig/philosophal/loaders/errors.rbs +6 -0
  20. data/sig/philosophal/loaders/json_loader.rbs +29 -0
  21. data/sig/philosophal/loaders.rbs +4 -0
  22. data/sig/philosophal/properties/instance_methods.rbs +26 -0
  23. data/sig/philosophal/properties/schema.rbs +47 -0
  24. data/sig/philosophal/properties.rbs +59 -0
  25. data/sig/philosophal/property.rbs +98 -0
  26. data/sig/philosophal/transform.rbs +7 -0
  27. data/sig/philosophal/types/any_type.rbs +9 -0
  28. data/sig/philosophal/types/array_of_type.rbs +23 -0
  29. data/sig/philosophal/types/boolean_type.rbs +11 -0
  30. data/sig/philosophal/types/hash_of_type.rbs +25 -0
  31. data/sig/philosophal/types.rbs +11 -0
  32. data/sig/philosophal.rbs +12 -0
  33. metadata +43 -14
  34. data/.rubocop.yml +0 -12
  35. data/.ruby-version +0 -1
  36. data/.tool-versions +0 -1
  37. data/Rakefile +0 -12
  38. data/VERSION +0 -1
@@ -2,7 +2,8 @@
2
2
 
3
3
  module Philosophal
4
4
  class Property
5
- def initialize(name:, type:, default:, transform:, immutable:, description:, json:)
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
@@ -10,9 +11,11 @@ module Philosophal
10
11
  @immutable = immutable
11
12
  @description = description
12
13
  @json = json
14
+ @override = override
15
+ @track_changes = track_changes
13
16
  end
14
17
 
15
- attr_reader :name, :type, :default, :transform, :immutable, :description, :json
18
+ attr_reader :name, :type, :default, :transform, :immutable, :description, :json, :override, :track_changes
16
19
 
17
20
  def json_name
18
21
  return @json if @json.is_a?(Symbol)
@@ -35,61 +38,167 @@ module Philosophal
35
38
  @transform != nil
36
39
  end
37
40
 
41
+ def override?
42
+ @override
43
+ end
44
+
45
+ def track_changes?
46
+ @track_changes
47
+ end
48
+
38
49
  def check_conversion(value)
39
50
  Philosophal.convert(self, value)
40
51
  end
41
52
 
42
- def generate_writer_method(buffer = +'')
43
- buffer <<
44
- ' def ' << @name.name << "=(value)\n " \
45
- '@' << @name.name << ' = self.class.philosophal_properties[:' <<
46
- @name.name <<
47
- "].check_conversion(value)\n" \
48
- "rescue Philosophal::TypeError => error\n" \
49
- "error.set_backtrace(caller(1))\n raise\n" \
50
- "end\n"
51
- end
52
-
53
- def generate_immutable_writer_method(buffer = +'')
54
- buffer <<
55
- ' def ' << @name.name << "=(value)\n " \
56
- 'raise FrozenError, "can\'t modify frozen variable @' <<
57
- @name.name << '" if defined?(@' << @name.name << ")\n" \
58
- '@' << @name.name << ' = self.class.philosophal_properties[:' <<
59
- @name.name <<
60
- "].check_conversion(value).freeze\n" \
61
- "rescue Philosophal::TypeError => error\n" \
62
- "error.set_backtrace(caller(1))\n raise\n" \
63
- "end\n"
64
- end
65
-
66
- def generate_reader_method(buffer = +'')
67
- buffer <<
68
- ' def ' <<
69
- @name.name <<
70
- "\n " \
71
- '@' << @name.name << ' || self.class.philosophal_properties[:' <<
72
- @name.name <<
73
- "].default_value\n" \
74
- "end\n"
75
- end
76
-
77
- def generate_boolean_method(buffer = +'')
78
- buffer <<
79
- ' def ' <<
80
- @name.name <<
81
- "?\n " \
82
- '!!@' << @name.name <<
83
- "\nend\n"
84
- end
85
-
86
- def generate_description_method(buffer = +'')
87
- buffer <<
88
- ' def ' <<
89
- @name.name <<
90
- "_description\n " \
91
- '%(' << @description << ')' \
92
- "\nend\n"
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
+
60
+ def method_generators
61
+ generators = {
62
+ writer_method_name => immutable ? :generate_immutable_writer_method : :generate_writer_method,
63
+ reader_method_name => default? ? :generate_reader_method : :generate_plain_reader_method
64
+ }
65
+ generators[boolean_method_name] = :generate_boolean_method if type == Philosophal::Types::BooleanType::Instance
66
+ generators[description_method_name] = :generate_description_method if description
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}"
82
+ end
83
+
84
+ def writer_method_name
85
+ return @writer_method_name if defined? @writer_method_name
86
+
87
+ @writer_method_name = :"#{@name.name}="
88
+ end
89
+
90
+ def generate_writer_method(buffer)
91
+ buffer << <<~RUBY
92
+ def #{writer_method_name.name}(value)
93
+ @#{@name.name} = #{constant_name}.check_conversion(value)
94
+ rescue Philosophal::TypeError => error
95
+ error.set_backtrace(caller(1))
96
+ raise
97
+ end
98
+ RUBY
99
+ end
100
+
101
+ def generate_immutable_writer_method(buffer)
102
+ buffer << <<~RUBY
103
+ def #{writer_method_name.name}(value)
104
+ raise FrozenError, "can't modify frozen variable @#{@name.name}" if defined?(@#{@name.name})
105
+ @#{@name.name} = #{constant_name}.check_conversion(value).freeze
106
+ rescue Philosophal::TypeError => error
107
+ error.set_backtrace(caller(1))
108
+ raise
109
+ end
110
+ RUBY
111
+ end
112
+
113
+ def reader_method_name
114
+ @name
115
+ end
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
+
130
+ def generate_reader_method(buffer)
131
+ buffer << <<~RUBY
132
+ def #{reader_method_name.name}
133
+ @#{@name.name}.nil? ? #{constant_name}.default_value : @#{@name.name}
134
+ end
135
+ RUBY
136
+ end
137
+
138
+ def boolean_method_name
139
+ return @boolean_method_name if defined? @boolean_method_name
140
+
141
+ @boolean_method_name = :"#{@name.name}?"
142
+ end
143
+
144
+ def generate_boolean_method(buffer)
145
+ buffer << <<~RUBY
146
+ def #{boolean_method_name.name}
147
+ !!#{reader_method_name.name}
148
+ end
149
+ RUBY
150
+ end
151
+
152
+ def description_method_name
153
+ return @description_method_name if defined? @description_method_name
154
+
155
+ @description_method_name = :"#{@name.name}_description"
156
+ end
157
+
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,
195
+ # so it is never evaluated as Ruby.
196
+ def generate_description_method(buffer)
197
+ buffer << <<~RUBY
198
+ def #{description_method_name.name}
199
+ #{constant_name}.description
200
+ end
201
+ RUBY
93
202
  end
94
203
  end
95
204
  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.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,15 +13,16 @@ 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)
24
- array.is_a?(Array) && !array.find { |entry| !entry.is_a?(@subtype) }
25
+ array.is_a?(Array) && array.all?(@subtype)
25
26
  end
26
27
  end
27
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
@@ -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,17 +13,20 @@ 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)
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
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'date'
4
4
  require 'pathname'
5
- require 'set'
6
5
  require 'time'
7
6
  require 'json'
8
7
 
@@ -20,25 +19,24 @@ module Philosophal
20
19
  autoload :ArgumentError, 'philosophal/errors/argument_error'
21
20
 
22
21
  def self.convert(property, value)
23
- if property.type === value
24
- if property.transform?
25
- Transform.make(property.transform, value)
26
- else
27
- value
28
- end
29
- else
30
- convert_method, subtype = Convertor.convert_method_for(property.type)
31
- 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, value) # rubocop:disable Style/CaseEquality
26
+ return converted unless property.transform?
32
27
 
33
- converted = if subtype
34
- Convertor.send(convert_method, value, subtype)
35
- else
36
- Convertor.send(convert_method, value)
37
- end
28
+ Transform.make(property.transform, converted)
29
+ end
38
30
 
39
- return converted unless property.transform?
31
+ def self.convert_to_type(property, value)
32
+ convert_method, subtype = property.convert_method
33
+ raise Philosophal::TypeError unless convert_method
40
34
 
41
- Transform.make(property.transform, converted)
35
+ if subtype
36
+ Convertor.send(convert_method, value, subtype)
37
+ else
38
+ Convertor.send(convert_method, value)
42
39
  end
43
40
  end
41
+ private_class_method :convert_to_type
44
42
  end
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