philosophal 1.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.
data/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # Philosophal
2
+
3
+ If you don't want to manage your parameters' conversation before valuing your objects' instance variables, **Philosophal** is the gem for you ;-)
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ gem install philosophal
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ````ruby
14
+ require 'philosophal'
15
+
16
+ class Person
17
+ extend Philosophal::Properties
18
+
19
+ cprop :first_name, String, transform: :downcase
20
+ cprop :last_name, String, transform: :upcase
21
+ cprop :age, Integer
22
+ end
23
+
24
+ maxime = Person.new
25
+ maxime.first_name = "Maxime"
26
+ maxime.last_name = "Désécot"
27
+ maxime.age = "40"
28
+ puts maxime.inspect
29
+ #<Person:0x00007da7088eed88 @first_name="maxime", @last_name="DÉSÉCOT", @age=40>
30
+ ````
data/Rakefile ADDED
@@ -0,0 +1,12 @@
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 ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philosophal
4
+ class Convertor
5
+ METHOD_TYPE_MAP = {
6
+ Symbol => :convert_to_symbol,
7
+ String => :convert_to_string,
8
+ Integer => :convert_to_integer,
9
+ Float => :convert_to_float,
10
+ Array => :convert_to_array,
11
+ Hash => :convert_to_hash,
12
+ Time => :convert_to_time,
13
+ Date => :convert_to_date,
14
+ DateTime => :convert_to_date_time,
15
+ Philosophal::Types::AnyType::Instance => :convert_to_any,
16
+ Philosophal::Types::BooleanType::Instance => :convert_to_boolean,
17
+ Philosophal::Types::ArrayOfType => :convert_to_array_of,
18
+ Philosophal::Types::HashOfType => :convert_to_hash_of,
19
+ Pathname => :convert_to_pathname
20
+ }.freeze
21
+
22
+ class << self
23
+ def convert_method_for(type)
24
+ if type.respond_to?(:subtype)
25
+ [METHOD_TYPE_MAP[type.class], type.subtype]
26
+ else
27
+ METHOD_TYPE_MAP[type]
28
+ end
29
+ end
30
+
31
+ def convert_to_symbol(obj)
32
+ raise Philosophal::TypeError unless obj.respond_to?(:to_sym)
33
+
34
+ obj.to_sym
35
+ end
36
+
37
+ def convert_to_string(obj)
38
+ raise Philosophal::TypeError unless obj.respond_to?(:to_s)
39
+
40
+ obj.to_s
41
+ end
42
+
43
+ def convert_to_integer(obj)
44
+ raise Philosophal::TypeError unless obj.respond_to?(:to_i)
45
+
46
+ obj.to_i
47
+ end
48
+
49
+ def convert_to_float(obj)
50
+ raise Philosophal::TypeError unless obj.respond_to?(:to_f)
51
+
52
+ obj.to_f
53
+ end
54
+
55
+ def convert_to_array(obj)
56
+ if obj.respond_to?(:to_a)
57
+ obj.to_a
58
+ else
59
+ [obj]
60
+ end
61
+ end
62
+
63
+ def convert_to_hash(obj)
64
+ raise Philosophal::TypeError unless obj.respond_to?(:to_h)
65
+
66
+ obj.to_h
67
+ end
68
+
69
+ def convert_to_time(obj)
70
+ return Time.zone.local(*obj) if obj.is_a?(Array)
71
+ return Time.zone.parse(obj) if obj.is_a?(String)
72
+ return Time.zone.at(obj) if obj.is_a?(Numeric)
73
+
74
+ raise Philosophal::TypeError
75
+ end
76
+
77
+ def convert_to_date(obj)
78
+ return obj.to_date if obj.respond_to?(:to_date)
79
+ return Date.new(*obj) if obj.is_a?(Array)
80
+ return Date.parse(obj) if obj.is_a?(String)
81
+ return Time.zone.at(obj).to_date if obj.is_a?(Numeric)
82
+
83
+ raise Philosophal::TypeError
84
+ end
85
+
86
+ def convert_to_date_time(obj)
87
+ return obj.to_datetime if obj.respond_to?(:to_datetime)
88
+ return DateTime.new(*obj) if obj.is_a?(Array)
89
+ return DateTime.parse(obj) if obj.is_a?(String)
90
+ return Time.zone.at(obj).to_datetime if obj.is_a?(Numeric)
91
+
92
+ raise Philosophal::TypeError
93
+ end
94
+
95
+ def convert_to_any(obj)
96
+ obj
97
+ end
98
+
99
+ def convert_to_boolean(obj)
100
+ if obj.is_a?(Numeric)
101
+ return true if obj == 1
102
+ return false if obj.zero?
103
+
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'
108
+
109
+ end
110
+ raise Philosophal::TypeError
111
+ end
112
+
113
+ def convert_to_pathname(obj)
114
+ raise Philosophal::TypeError unless obj.respond_to?(:to_s)
115
+
116
+ Pathname.new(obj)
117
+ end
118
+
119
+ def convert_to_array_of(obj, subtype)
120
+ subtype_convert_method = convert_method_for(subtype)
121
+ raise Philosophal::TypeError unless subtype_convert_method
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
128
+ end
129
+
130
+ def convert_to_hash_of(obj, subtype)
131
+ key_convert_method = convert_method_for(subtype[:key_type])
132
+ raise Philosophal::TypeError unless key_convert_method
133
+
134
+ value_convert_method = convert_method_for(subtype[:value_type])
135
+ raise Philosophal::TypeError unless value_convert_method
136
+
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) }
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philosophal
4
+ class ArgumentError < ArgumentError
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philosophal
4
+ class TypeError < TypeError
5
+ end
6
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philosophal
4
+ module Properties
5
+ class Schema
6
+ include Enumerable
7
+
8
+ def initialize(properties_index: {})
9
+ @properties_index = properties_index
10
+ @mutex = Mutex.new
11
+ end
12
+
13
+ attr_reader :properties_index
14
+
15
+ def [](key)
16
+ @properties_index[key]
17
+ end
18
+
19
+ def keys
20
+ @properties_index.keys
21
+ end
22
+
23
+ def <<(value)
24
+ @mutex.synchronize do
25
+ @properties_index[value.name] = value
26
+ end
27
+
28
+ self
29
+ end
30
+
31
+ def immutables
32
+ @properties_index.select { |_, v| v.immutable }
33
+ end
34
+
35
+ def mutables
36
+ @properties_index.reject { |_, v| v.immutable }
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philosophal
4
+ module Properties
5
+ autoload :Schema, 'philosophal/properties/schema'
6
+
7
+ include Philosophal::Types
8
+
9
+ def cprop(name, type, default: nil, transform: nil, immutable: false)
10
+ default.freeze if default && !(default.is_a?(Proc) || default.frozen?)
11
+
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
+ property = __philosophal_property_class__.new(name:, type:, default:, transform:, immutable:)
21
+
22
+ philosophal_properties << property
23
+ __define_philosophal_methods__(property)
24
+ include(__philosophal_extension__)
25
+ end
26
+
27
+ def philosophal_properties
28
+ return @philosophal_properties if defined?(@philosophal_properties)
29
+
30
+ @philosophal_properties = if defined?(superclass) && superclass.is_a?(Philosophal::Properties)
31
+ superclass.philosophal_properties.dup
32
+ else
33
+ Philosophal::Properties::Schema.new
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def __philosophal_property_class__
40
+ Philosophal::Property
41
+ end
42
+
43
+ def __define_philosophal_methods__(new_property)
44
+ code = __generate_philosophal_methods__(new_property)
45
+ __philosophal_extension__.module_eval(code)
46
+ end
47
+
48
+ def __philosophal_extension__
49
+ if defined?(@__philosophal_extension__)
50
+ @__philosophal_extension__
51
+ else
52
+ @__philosophal_extension__ = Module.new do
53
+ def cprop?(property_name, klass)
54
+ property = self.class.philosophal_properties.properties_index[property_name.to_sym]
55
+ return false unless property
56
+
57
+ klass = klass.class unless klass.is_a?(Class)
58
+ property.type == klass
59
+ end
60
+
61
+ def philosophal_inspect(light = false)
62
+ keys_map = philosophal_inspect_map
63
+
64
+ if light
65
+ keys_map.reject! do |_, v|
66
+ v.nil? || (v.respond_to?(:empty?) && v.empty?)
67
+ end
68
+ end
69
+
70
+ keys_str = keys_map.map { |k, v| [k, v.inspect].join(': ') }.join(', ')
71
+ "#{self.class}:#{format('0x00%x', (object_id << 1))} #{keys_str}"
72
+ end
73
+ alias cprop_inspect philosophal_inspect
74
+
75
+ def philosophal_inspect_map
76
+ self.class.philosophal_properties.properties_index.values.to_h do |property|
77
+ [
78
+ property.name,
79
+ send(property.name)
80
+ ]
81
+ end
82
+ end
83
+ alias cprop_inspect_map philosophal_inspect_map
84
+ end
85
+ end
86
+ end
87
+
88
+ def __generate_philosophal_methods__(new_property, buffer = +'')
89
+ buffer << "# frozen_string_philosophal: true\n"
90
+ if new_property.immutable
91
+ new_property.generate_immutable_writer_method(buffer)
92
+ else
93
+ new_property.generate_writer_method(buffer)
94
+ end
95
+
96
+ new_property.generate_reader_method(buffer)
97
+ new_property.generate_boolean_method(buffer) if new_property.type == Philosophal::Types::BooleanType::Instance
98
+
99
+ # puts buffer
100
+
101
+ buffer
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philosophal
4
+ class Property
5
+ def initialize(name:, type:, default:, transform:, immutable:)
6
+ @name = name
7
+ @type = type
8
+ @default = default
9
+ @transform = transform
10
+ @immutable = immutable
11
+ end
12
+
13
+ attr_reader :name, :type, :default, :transform, :immutable
14
+
15
+ def default?
16
+ @default != nil
17
+ end
18
+
19
+ def default_value
20
+ case @default
21
+ when Proc then @default.call
22
+ else @default
23
+ end
24
+ end
25
+
26
+ def transform?
27
+ @transform != nil
28
+ end
29
+
30
+ def check_conversion(value)
31
+ Philosophal.convert(self, value)
32
+ end
33
+
34
+ def generate_writer_method(buffer = +'')
35
+ buffer <<
36
+ ' def ' << @name.name << "=(value)\n " \
37
+ '@' << @name.name << ' = self.class.philosophal_properties[:' <<
38
+ @name.name <<
39
+ "].check_conversion(value)\n" \
40
+ "rescue Philosophal::TypeError => error\n" \
41
+ "error.set_backtrace(caller(1))\n raise\n" \
42
+ "end\n"
43
+ end
44
+
45
+ def generate_immutable_writer_method(buffer = +'')
46
+ buffer <<
47
+ ' def ' << @name.name << "=(value)\n " \
48
+ 'raise FrozenError, "can\'t modify frozen variable @' <<
49
+ @name.name << '" if defined?(@' << @name.name << ")\n" \
50
+ '@' << @name.name << ' = self.class.philosophal_properties[:' <<
51
+ @name.name <<
52
+ "].check_conversion(value).freeze\n" \
53
+ "rescue Philosophal::TypeError => error\n" \
54
+ "error.set_backtrace(caller(1))\n raise\n" \
55
+ "end\n"
56
+ end
57
+
58
+ def generate_reader_method(buffer = +'')
59
+ buffer <<
60
+ ' def ' <<
61
+ @name.name <<
62
+ "\n " \
63
+ '@' << @name.name << ' || self.class.philosophal_properties[:' <<
64
+ @name.name <<
65
+ "].default_value\n" \
66
+ "end\n"
67
+ end
68
+
69
+ def generate_boolean_method(buffer = +'')
70
+ buffer <<
71
+ ' def ' <<
72
+ @name.name <<
73
+ "?\n " \
74
+ '!!@' << @name.name <<
75
+ "\nend\n"
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philosophal
4
+ class Transform
5
+ def self.make(method, value)
6
+ case method
7
+ when Proc
8
+ method.call(value)
9
+ else
10
+ value.send(method)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philosophal
4
+ module Types
5
+ class AnyType
6
+ Instance = new.freeze
7
+
8
+ def ===(_value)
9
+ true
10
+ end
11
+
12
+ freeze
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philosophal
4
+ module Types
5
+ class ArrayOfType
6
+ def self.instance(subtype)
7
+ memorization.fetch(subtype, new(subtype).freeze)
8
+ end
9
+
10
+ def self.memorization
11
+ return @memorization if defined?(@memorization)
12
+
13
+ @memorization = {}
14
+ end
15
+
16
+ attr_reader :subtype
17
+
18
+ def initialize(subtype)
19
+ @subtype = subtype
20
+ self.class.memorization[subtype] = self
21
+ end
22
+
23
+ def ===(array)
24
+ array.is_a?(Array) && !array.find { |entry| !entry.is_a?(@subtype) }
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philosophal
4
+ module Types
5
+ class BooleanType
6
+ Instance = new.freeze
7
+
8
+ TRUE_FALSE_SET = Set[true, false]
9
+
10
+ def ===(value)
11
+ TRUE_FALSE_SET.include?(value)
12
+ end
13
+
14
+ freeze
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philosophal
4
+ module Types
5
+ class HashOfType
6
+ def self.instance(key_type, value_type)
7
+ subtype = { key_type:, value_type: }
8
+ memorization.fetch(Marshal.dump(subtype), new(subtype).freeze)
9
+ end
10
+
11
+ def self.memorization
12
+ return @memorization if defined?(@memorization)
13
+
14
+ @memorization = {}
15
+ end
16
+
17
+ attr_reader :subtype
18
+
19
+ def initialize(subtype)
20
+ @subtype = subtype.freeze
21
+ self.class.memorization[Marshal.dump(subtype)] = self
22
+ end
23
+
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]) }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philosophal
4
+ module Types
5
+ autoload :AnyType, 'philosophal/types/any_type'
6
+ autoload :BooleanType, 'philosophal/types/boolean_type'
7
+ autoload :ArrayOfType, 'philosophal/types/array_of_type'
8
+ autoload :HashOfType, 'philosophal/types/hash_of_type'
9
+
10
+ def _Any
11
+ AnyType::Instance
12
+ end
13
+
14
+ def _Boolean
15
+ BooleanType::Instance
16
+ end
17
+
18
+ def _ArrayOf(subtype)
19
+ ArrayOfType.instance(subtype)
20
+ end
21
+
22
+ def _HashOf(key_type, value_type)
23
+ HashOfType.instance(key_type, value_type)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require 'pathname'
5
+ require 'set'
6
+ require 'time'
7
+
8
+ module Philosophal
9
+ autoload :Convertor, 'philosophal/convertor'
10
+ autoload :Transform, 'philosophal/transform'
11
+
12
+ autoload :Properties, 'philosophal/properties'
13
+ autoload :Property, 'philosophal/property'
14
+
15
+ autoload :Types, 'philosophal/types'
16
+
17
+ autoload :TypeError, 'philosophal/errors/type_error'
18
+ autoload :ArgumentError, 'philosophal/errors/argument_error'
19
+
20
+ 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
30
+
31
+ converted = if subtype
32
+ Convertor.send(convert_method, value, subtype)
33
+ else
34
+ Convertor.send(convert_method, value)
35
+ end
36
+
37
+ return converted unless property.transform?
38
+
39
+ Transform.make(property.transform, converted)
40
+ end
41
+ end
42
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: philosophal
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Maxime Désécot
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-03-24 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Auto convert value on setter method call
14
+ email:
15
+ - maxime.desecot@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".rubocop.yml"
21
+ - ".ruby-version"
22
+ - ".tool-versions"
23
+ - CHANGELOG.md
24
+ - LICENSE
25
+ - README.md
26
+ - Rakefile
27
+ - VERSION
28
+ - lib/philosophal.rb
29
+ - lib/philosophal/convertor.rb
30
+ - lib/philosophal/errors/argument_error.rb
31
+ - lib/philosophal/errors/type_error.rb
32
+ - lib/philosophal/properties.rb
33
+ - lib/philosophal/properties/schema.rb
34
+ - lib/philosophal/property.rb
35
+ - lib/philosophal/transform.rb
36
+ - lib/philosophal/types.rb
37
+ - lib/philosophal/types/any_type.rb
38
+ - lib/philosophal/types/array_of_type.rb
39
+ - lib/philosophal/types/boolean_type.rb
40
+ - lib/philosophal/types/hash_of_type.rb
41
+ homepage: https://github.com/RaoH37/philosophal
42
+ licenses:
43
+ - GPL-3.0-only
44
+ metadata:
45
+ homepage_uri: https://github.com/RaoH37/philosophal
46
+ source_code_uri: https://github.com/RaoH37/philosophal
47
+ changelog_uri: https://github.com/RaoH37/philosophal/blob/main/CHANGELOG.md
48
+ rubygems_mfa_required: 'true'
49
+ post_install_message:
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '3.1'
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubygems_version: 3.3.27
65
+ signing_key:
66
+ specification_version: 4
67
+ summary: Philosophal setter convertor
68
+ test_files: []