castkit 0.1.2 → 0.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.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec_status +195 -219
  3. data/CHANGELOG.md +42 -0
  4. data/README.md +744 -83
  5. data/castkit.gemspec +1 -0
  6. data/lib/castkit/attribute.rb +6 -24
  7. data/lib/castkit/castkit.rb +61 -10
  8. data/lib/castkit/cli/generate.rb +98 -0
  9. data/lib/castkit/cli/list.rb +200 -0
  10. data/lib/castkit/cli/main.rb +43 -0
  11. data/lib/castkit/cli.rb +24 -0
  12. data/lib/castkit/configuration.rb +116 -46
  13. data/lib/castkit/contract/base.rb +168 -0
  14. data/lib/castkit/contract/data_object.rb +62 -0
  15. data/lib/castkit/contract/result.rb +74 -0
  16. data/lib/castkit/contract/validator.rb +248 -0
  17. data/lib/castkit/contract.rb +67 -0
  18. data/lib/castkit/{data_object_extensions → core}/attribute_types.rb +21 -7
  19. data/lib/castkit/{data_object_extensions → core}/attributes.rb +8 -3
  20. data/lib/castkit/core/config.rb +74 -0
  21. data/lib/castkit/core/registerable.rb +59 -0
  22. data/lib/castkit/data_object.rb +56 -67
  23. data/lib/castkit/error.rb +15 -3
  24. data/lib/castkit/ext/attribute/access.rb +67 -0
  25. data/lib/castkit/ext/attribute/error_handling.rb +63 -0
  26. data/lib/castkit/ext/attribute/options.rb +142 -0
  27. data/lib/castkit/ext/attribute/validation.rb +85 -0
  28. data/lib/castkit/ext/data_object/contract.rb +96 -0
  29. data/lib/castkit/ext/data_object/deserialization.rb +167 -0
  30. data/lib/castkit/ext/data_object/plugins.rb +86 -0
  31. data/lib/castkit/ext/data_object/serialization.rb +61 -0
  32. data/lib/castkit/inflector.rb +47 -0
  33. data/lib/castkit/plugins.rb +82 -0
  34. data/lib/castkit/serializers/base.rb +94 -0
  35. data/lib/castkit/serializers/default_serializer.rb +156 -0
  36. data/lib/castkit/types/base.rb +122 -0
  37. data/lib/castkit/types/boolean.rb +47 -0
  38. data/lib/castkit/types/collection.rb +35 -0
  39. data/lib/castkit/types/date.rb +34 -0
  40. data/lib/castkit/types/date_time.rb +34 -0
  41. data/lib/castkit/types/float.rb +46 -0
  42. data/lib/castkit/types/integer.rb +46 -0
  43. data/lib/castkit/types/string.rb +44 -0
  44. data/lib/castkit/types.rb +15 -0
  45. data/lib/castkit/validators/base.rb +59 -0
  46. data/lib/castkit/validators/boolean_validator.rb +39 -0
  47. data/lib/castkit/validators/collection_validator.rb +29 -0
  48. data/lib/castkit/validators/float_validator.rb +31 -0
  49. data/lib/castkit/validators/integer_validator.rb +31 -0
  50. data/lib/castkit/validators/numeric_validator.rb +2 -2
  51. data/lib/castkit/validators/string_validator.rb +3 -4
  52. data/lib/castkit/version.rb +1 -1
  53. data/lib/castkit.rb +2 -0
  54. data/lib/generators/base.rb +97 -0
  55. data/lib/generators/contract.rb +68 -0
  56. data/lib/generators/data_object.rb +48 -0
  57. data/lib/generators/plugin.rb +25 -0
  58. data/lib/generators/serializer.rb +28 -0
  59. data/lib/generators/templates/contract.rb.tt +24 -0
  60. data/lib/generators/templates/contract_spec.rb.tt +76 -0
  61. data/lib/generators/templates/data_object.rb.tt +15 -0
  62. data/lib/generators/templates/data_object_spec.rb.tt +36 -0
  63. data/lib/generators/templates/plugin.rb.tt +37 -0
  64. data/lib/generators/templates/plugin_spec.rb.tt +18 -0
  65. data/lib/generators/templates/serializer.rb.tt +24 -0
  66. data/lib/generators/templates/serializer_spec.rb.tt +14 -0
  67. data/lib/generators/templates/type.rb.tt +55 -0
  68. data/lib/generators/templates/type_spec.rb.tt +42 -0
  69. data/lib/generators/templates/validator.rb.tt +26 -0
  70. data/lib/generators/templates/validator_spec.rb.tt +23 -0
  71. data/lib/generators/type.rb +29 -0
  72. data/lib/generators/validator.rb +41 -0
  73. metadata +74 -15
  74. data/lib/castkit/attribute_extensions/access.rb +0 -65
  75. data/lib/castkit/attribute_extensions/casting.rb +0 -147
  76. data/lib/castkit/attribute_extensions/error_handling.rb +0 -83
  77. data/lib/castkit/attribute_extensions/options.rb +0 -131
  78. data/lib/castkit/attribute_extensions/serialization.rb +0 -89
  79. data/lib/castkit/attribute_extensions/validation.rb +0 -72
  80. data/lib/castkit/data_object_extensions/config.rb +0 -113
  81. data/lib/castkit/data_object_extensions/deserialization.rb +0 -110
  82. data/lib/castkit/default_serializer.rb +0 -123
  83. data/lib/castkit/serializer.rb +0 -92
  84. data/lib/castkit/validators.rb +0 -4
@@ -1,113 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Castkit
4
- module DataObjectExtensions
5
- # Provides per-class configuration for a Castkit::DataObject,
6
- # including root key handling, strict mode, and unknown key behavior.
7
- module Config
8
- # Automatically extends class-level methods when included.
9
- #
10
- # @param base [Class]
11
- def self.included(base)
12
- base.extend(ClassMethods)
13
- end
14
-
15
- # Class-level configuration methods.
16
- module ClassMethods
17
- # Sets or retrieves the root key to wrap the object under during (de)serialization.
18
- #
19
- # @param value [String, Symbol, nil] optional root key
20
- # @return [Symbol, nil]
21
- def root(value = nil)
22
- @root = value.to_s.strip.to_sym if value
23
- @root
24
- end
25
-
26
- # Sets or retrieves whether to skip `nil` values in output.
27
- #
28
- # @param value [Boolean, nil]
29
- # @return [Boolean, nil]
30
- def ignore_nil(value = nil)
31
- value.nil? ? @ignore_nil : (@ignore_nil = value)
32
- end
33
-
34
- # Sets or retrieves whether to skip blank values (`[]`, `{}`, `""`, etc.) in output.
35
- #
36
- # Defaults to true unless explicitly set to false.
37
- #
38
- # @param value [Boolean, nil]
39
- # @return [Boolean]
40
- def ignore_blank(value = nil)
41
- @ignore_blank = value.nil? || value
42
- end
43
-
44
- # Sets or retrieves strict mode behavior.
45
- #
46
- # In strict mode, unknown keys during deserialization raise errors.
47
- #
48
- # @param value [Boolean, nil]
49
- # @return [Boolean]
50
- def strict(value = nil)
51
- if value.nil?
52
- @strict.nil? || @strict
53
- else
54
- @strict = value
55
- end
56
- end
57
-
58
- # Enables or disables ignoring unknown keys during deserialization.
59
- #
60
- # This is the inverse of `strict`.
61
- #
62
- # @param value [Boolean]
63
- # @return [void]
64
- def ignore_unknown(value = nil)
65
- @strict = !value
66
- end
67
-
68
- # Sets or retrieves whether to emit warnings when unknown keys are encountered.
69
- #
70
- # @param value [Boolean, nil]
71
- # @return [Boolean, nil]
72
- def warn_on_unknown(value = nil)
73
- value.nil? ? @warn_on_unknown : (@warn_on_unknown = value)
74
- end
75
-
76
- # Sets or retrieves whether to allow unknown keys when they are encountered.
77
- #
78
- # @param value [Boolean, nil]
79
- # @return [Boolean, nil]
80
- def allow_unknown(value = nil)
81
- value.nil? ? @allow_unknown : (@allow_unknown = value)
82
- end
83
-
84
- # Returns a relaxed version of the current class with strict mode off.
85
- #
86
- # Useful for tolerant parsing scenarios.
87
- #
88
- # @param warn_on_unknown [Boolean]
89
- # @return [Class] a subclass with relaxed rules
90
- def relaxed(warn_on_unknown: true)
91
- klass = Class.new(self)
92
- klass.strict(false)
93
- klass.warn_on_unknown(warn_on_unknown)
94
- klass
95
- end
96
- end
97
-
98
- # Returns the root key for this instance.
99
- #
100
- # @return [Symbol]
101
- def root_key
102
- self.class.root.to_s.strip.to_sym
103
- end
104
-
105
- # Whether a root key is configured for this instance.
106
- #
107
- # @return [Boolean]
108
- def root_key_set?
109
- !self.class.root.to_s.strip.empty?
110
- end
111
- end
112
- end
113
- end
@@ -1,110 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Castkit
4
- module DataObjectExtensions
5
- # Adds deserialization support for Castkit::DataObject instances.
6
- #
7
- # Handles attribute loading, alias resolution, and unwrapped field extraction.
8
- module Deserialization
9
- # Hooks in class methods like `.from_hash` when included.
10
- #
11
- # @param base [Class]
12
- def self.included(base)
13
- base.extend(ClassMethods)
14
- end
15
-
16
- # Class-level deserialization helpers.
17
- module ClassMethods
18
- # Builds a new instance from a Hash, symbolizing keys as needed.
19
- #
20
- # @param hash [Hash]
21
- # @return [Castkit::DataObject]
22
- def from_hash(hash)
23
- hash = hash.transform_keys { |k| k.respond_to?(:to_sym) ? k.to_sym : k }
24
- new(hash)
25
- end
26
-
27
- # @!method from_h(hash)
28
- # Alias for {.from_hash}
29
- #
30
- # @!method creator(hash)
31
- # Alias for {.from_hash}
32
- alias from_h from_hash
33
- alias creator from_hash
34
- end
35
-
36
- private
37
-
38
- # Loads attribute values from the given hash.
39
- #
40
- # Respects access control (e.g., `writeable?`) and uses `.load` for casting/validation.
41
- #
42
- # @param data [Hash]
43
- # @return [void]
44
- def deserialize_attributes!(data)
45
- self.class.attributes.each do |field, attribute|
46
- next if attribute.skip_deserialization?
47
-
48
- value = fetch_attribute_key(data, attribute)
49
- value = attribute.load(value, context: field)
50
-
51
- instance_variable_set("@#{field}", value)
52
- end
53
- end
54
-
55
- # Fetches the best matching value from the hash using attribute key and aliases.
56
- #
57
- # @param data [Hash]
58
- # @param attribute [Castkit::Attribute]
59
- # @return [Object]
60
- def fetch_attribute_key(data, attribute)
61
- attribute.key_path(with_aliases: true).each do |path|
62
- value = path.reduce(data) { |memo, key| memo.is_a?(Hash) ? memo[key] : nil }
63
- return value unless value.nil?
64
- end
65
-
66
- nil
67
- end
68
-
69
- # Extracts prefixed fields for unwrapped attributes and groups them under the original field key.
70
- #
71
- # @param data [Hash]
72
- # @return [Hash] modified input hash with unwrapped values nested under their base field
73
- def unwrap_prefixed_fields!(data)
74
- self.class.attributes.each_value do |attribute|
75
- next unless attribute.unwrapped?
76
-
77
- unwrapped, keys_to_remove = unwrap_prefixed_values(data, attribute)
78
- next if unwrapped.empty?
79
-
80
- data[attribute.field] = unwrapped
81
- keys_to_remove.each { |k| data.delete(k) }
82
- end
83
-
84
- data
85
- end
86
-
87
- # Returns the prefixed key-value pairs for a given unwrapped attribute.
88
- #
89
- # @param data [Hash]
90
- # @param attribute [Castkit::Attribute]
91
- # @return [Array<Hash, Array<Symbol>>] extracted key-value pairs and keys to delete
92
- def unwrap_prefixed_values(data, attribute)
93
- prefix = attribute.prefix.to_s
94
- unwrapped_data = {}
95
- keys_to_remove = []
96
-
97
- data.each do |k, v|
98
- k_str = k.to_s
99
- next unless k_str.start_with?(prefix)
100
-
101
- stripped = k_str.sub(prefix, "").to_sym
102
- unwrapped_data[stripped] = v
103
- keys_to_remove << k
104
- end
105
-
106
- [unwrapped_data, keys_to_remove]
107
- end
108
- end
109
- end
110
- end
@@ -1,123 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "serializer"
4
-
5
- module Castkit
6
- # Default serializer for Castkit::DataObject instances.
7
- #
8
- # Serializes attributes based on access rules, nil/blank filtering, and nested structure.
9
- class DefaultSerializer < Castkit::Serializer
10
- SKIP_ATTRIBUTE = :__castkit_skip_attribute
11
-
12
- # @return [Hash{Symbol => Castkit::Attribute}] attributes to serialize
13
- attr_reader :attributes
14
-
15
- # @return [Hash{Symbol => Object}] attributes that are not predefined or known to the object
16
- attr_reader :unknown_attributes
17
-
18
- # @return [Hash] serialization options (root key, ignore_nil, allow_unknown, etc.)
19
- attr_reader :options
20
-
21
- # Returns the serialized object as a Hash.
22
- #
23
- # Includes root wrapping if configured. If `allow_unknown` is set to true, unknown attributes
24
- # are merged into the final result.
25
- #
26
- # @return [Hash] The serialized object, potentially wrapped with a root key
27
- def call
28
- result = serialize_attributes
29
- result.merge!(unknown_attributes) if options[:allow_unknown]
30
-
31
- options[:root] ? { options[:root].to_sym => result } : result
32
- end
33
-
34
- private
35
-
36
- # Initializes the serializer with the target object and context.
37
- #
38
- # @param raw [Castkit::DataObject] the object to serialize
39
- # @param visited [Set, nil] used to detect circular references (default is nil)
40
- def initialize(raw, visited: nil)
41
- super
42
-
43
- # Setting up attributes, unknown attributes, and options based on the class-level configuration
44
- @attributes = raw.class.attributes
45
- @unknown_attributes = raw.unknown_attributes
46
- @options = {
47
- root: raw.class.root,
48
- ignore_nil: raw.class.ignore_nil || false,
49
- allow_unknown: raw.class.allow_unknown || false
50
- }
51
- end
52
-
53
- # Iterates over attributes and serializes each into a result hash.
54
- #
55
- # @return [Hash] The serialized attributes as a hash
56
- def serialize_attributes
57
- attributes.each_with_object({}) do |(_, attribute), hash|
58
- next if attribute.skip_serialization?
59
-
60
- # Serializing each attribute
61
- serialized_value = serialize_attribute(attribute)
62
- next if serialized_value == SKIP_ATTRIBUTE
63
-
64
- # Assign the serialized value to the correct key in the hash
65
- assign_attribute_key!(hash, attribute, serialized_value)
66
- end
67
- end
68
-
69
- # Process and serialize a given attribute.
70
- #
71
- # This handles value extraction, skipping when nil values are encountered, and ensuring
72
- # attributes are serialized according to their rules.
73
- #
74
- # @param attribute [Castkit::Attribute] The attribute instance to serialize
75
- # @return [Object, nil] The serialized value or SKIP_ATTRIBUTE if the value should be skipped
76
- def serialize_attribute(attribute)
77
- # Fetch the value of the attribute from the object
78
- value = obj.public_send(attribute.field)
79
-
80
- # Skip serialization if value is nil and ignore_nil is set to true
81
- return SKIP_ATTRIBUTE if value.nil? && (attribute.ignore_nil? || options[:ignore_nil])
82
-
83
- # Serialize the value using the attribute's dump method
84
- serialized_value = attribute.dump(value, visited: visited)
85
-
86
- # Skip if value is blank and ignore_blank is set to true
87
- return SKIP_ATTRIBUTE if blank?(serialized_value) && (attribute.ignore_blank? || options[:ignore_blank])
88
-
89
- serialized_value
90
- end
91
-
92
- # Assigns a serialized value into the hash using nested key paths.
93
- #
94
- # This ensures attributes with nested key paths (like `address.city`) are placed into nested hashes.
95
- #
96
- # @param hash [Hash] The resulting hash to populate with the serialized values
97
- # @param attribute [Castkit::Attribute] The attribute being serialized
98
- # @param value [Object] The serialized value
99
- # @return [void] Updates the hash in-place
100
- def assign_attribute_key!(hash, attribute, value)
101
- key_path = attribute.key_path
102
- last = key_path.pop
103
- current = hash
104
-
105
- # Traverse the key path and create nested hashes as needed
106
- key_path.each do |key|
107
- current[key] ||= {}
108
- current = current[key]
109
- end
110
-
111
- # Assign the final value to the last key in the path
112
- current[last] = value
113
- end
114
-
115
- # Determines if a value is blank (nil, empty array, empty hash, empty string, etc.)
116
- #
117
- # @param value [Object, nil] The value to check
118
- # @return [Boolean] true if the value is blank, false otherwise
119
- def blank?(value)
120
- value.nil? || (value.respond_to?(:empty?) && value.empty?)
121
- end
122
- end
123
- end
@@ -1,92 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "set"
4
-
5
- module Castkit
6
- # Abstract base class for defining custom serializers for Castkit::DataObject instances.
7
- #
8
- # Handles circular reference detection and provides a consistent `call` API.
9
- #
10
- # Subclasses must implement an instance method `#call` that returns a hash-like representation.
11
- #
12
- # @example Usage
13
- # class CustomSerializer < Castkit::Serializer
14
- # private
15
- #
16
- # def call
17
- # { type: obj.class.name, data: obj.to_h }
18
- # end
19
- # end
20
- #
21
- # CustomSerializer.call(user_dto)
22
- class Serializer
23
- class << self
24
- # Entrypoint for serializing an object.
25
- #
26
- # @param obj [Castkit::DataObject] the object to serialize
27
- # @param visited [Set, nil] used to track visited object IDs
28
- # @return [Object] result of custom serialization
29
- def call(obj, visited: nil)
30
- new(obj, visited: visited).send(:serialize)
31
- end
32
- end
33
-
34
- # @return [Castkit::DataObject] the object being serialized
35
- attr_reader :obj
36
-
37
- protected
38
-
39
- # Fallback to the default serializer.
40
- #
41
- # @return [Hash]
42
- def serialize_with_default
43
- Castkit::DefaultSerializer.call(obj, visited: visited)
44
- end
45
-
46
- private
47
-
48
- # @return [Set<Integer>] a set of visited object IDs to detect circular references
49
- attr_reader :visited
50
-
51
- # Initializes the serializer instance.
52
- #
53
- # @param obj [Castkit::DataObject]
54
- # @param visited [Set, nil]
55
- def initialize(obj, visited: nil)
56
- @obj = obj
57
- @visited = visited || Set.new
58
- end
59
-
60
- # Subclasses must override this method to implement serialization logic.
61
- #
62
- # @raise [NotImplementedError]
63
- # @return [Object]
64
- def call
65
- raise NotImplementedError, "#{self.class.name} must implement `#call`"
66
- end
67
-
68
- # Wraps the actual serialization logic with circular reference detection.
69
- #
70
- # @return [Object]
71
- # @raise [Castkit::SerializationError] if a circular reference is detected
72
- def serialize
73
- check_circular_reference!
74
- visited << obj.object_id
75
-
76
- result = call
77
- visited.delete(obj.object_id)
78
-
79
- result
80
- end
81
-
82
- # Raises if the object has already been visited (circular reference).
83
- #
84
- # @raise [Castkit::SerializationError]
85
- # @return [void]
86
- def check_circular_reference!
87
- return unless visited.include?(obj.object_id)
88
-
89
- raise Castkit::SerializationError, "Circular reference detected for #{obj.class}"
90
- end
91
- end
92
- end
@@ -1,4 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "validators/numeric_validator"
4
- require_relative "validators/string_validator"