castkit 0.2.0 → 0.3.1

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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -1
  3. data/README.md +297 -13
  4. data/castkit.gemspec +3 -0
  5. data/lib/castkit/attribute.rb +82 -59
  6. data/lib/castkit/attributes/definition.rb +64 -0
  7. data/lib/castkit/attributes/options.rb +214 -0
  8. data/lib/castkit/castkit.rb +18 -5
  9. data/lib/castkit/cli/generate.rb +112 -0
  10. data/lib/castkit/cli/list.rb +200 -0
  11. data/lib/castkit/cli/main.rb +43 -0
  12. data/lib/castkit/cli.rb +24 -0
  13. data/lib/castkit/configuration.rb +31 -8
  14. data/lib/castkit/contract/{generic.rb → base.rb} +5 -17
  15. data/lib/castkit/contract/result.rb +2 -2
  16. data/lib/castkit/contract/validator.rb +5 -1
  17. data/lib/castkit/contract.rb +5 -5
  18. data/lib/castkit/core/attributes.rb +87 -44
  19. data/lib/castkit/data_object.rb +11 -30
  20. data/lib/castkit/{ext → dsl}/attribute/access.rb +1 -1
  21. data/lib/castkit/{ext → dsl}/attribute/error_handling.rb +1 -1
  22. data/lib/castkit/{ext → dsl}/attribute/options.rb +1 -1
  23. data/lib/castkit/{ext → dsl}/attribute/validation.rb +3 -3
  24. data/lib/castkit/dsl/attribute.rb +47 -0
  25. data/lib/castkit/{ext → dsl}/data_object/contract.rb +2 -2
  26. data/lib/castkit/{ext → dsl}/data_object/deserialization.rb +6 -2
  27. data/lib/castkit/dsl/data_object/plugins.rb +86 -0
  28. data/lib/castkit/{ext → dsl}/data_object/serialization.rb +1 -1
  29. data/lib/castkit/dsl/data_object.rb +61 -0
  30. data/lib/castkit/inflector.rb +1 -1
  31. data/lib/castkit/plugins.rb +82 -0
  32. data/lib/castkit/serializers/base.rb +94 -0
  33. data/lib/castkit/serializers/default_serializer.rb +156 -0
  34. data/lib/castkit/types/{generic.rb → base.rb} +30 -10
  35. data/lib/castkit/types/boolean.rb +14 -10
  36. data/lib/castkit/types/collection.rb +13 -2
  37. data/lib/castkit/types/date.rb +2 -2
  38. data/lib/castkit/types/date_time.rb +2 -2
  39. data/lib/castkit/types/float.rb +5 -5
  40. data/lib/castkit/types/integer.rb +5 -5
  41. data/lib/castkit/types/string.rb +2 -2
  42. data/lib/castkit/types.rb +1 -1
  43. data/lib/castkit/validators/base.rb +59 -0
  44. data/lib/castkit/validators/boolean_validator.rb +39 -0
  45. data/lib/castkit/validators/collection_validator.rb +29 -0
  46. data/lib/castkit/validators/float_validator.rb +31 -0
  47. data/lib/castkit/validators/integer_validator.rb +31 -0
  48. data/lib/castkit/validators/numeric_validator.rb +2 -2
  49. data/lib/castkit/validators/string_validator.rb +3 -4
  50. data/lib/castkit/version.rb +1 -1
  51. data/lib/castkit.rb +1 -4
  52. data/lib/generators/attribute.rb +39 -0
  53. data/lib/generators/base.rb +97 -0
  54. data/lib/generators/contract.rb +68 -0
  55. data/lib/generators/data_object.rb +48 -0
  56. data/lib/generators/plugin.rb +25 -0
  57. data/lib/generators/serializer.rb +28 -0
  58. data/lib/generators/templates/attribute.rb.tt +21 -0
  59. data/lib/generators/templates/attribute_spec.rb.tt +41 -0
  60. data/lib/generators/templates/contract.rb.tt +26 -0
  61. data/lib/generators/templates/contract_spec.rb.tt +76 -0
  62. data/lib/generators/templates/data_object.rb.tt +17 -0
  63. data/lib/generators/templates/data_object_spec.rb.tt +36 -0
  64. data/lib/generators/templates/plugin.rb.tt +37 -0
  65. data/lib/generators/templates/plugin_spec.rb.tt +18 -0
  66. data/lib/generators/templates/serializer.rb.tt +24 -0
  67. data/lib/generators/templates/serializer_spec.rb.tt +14 -0
  68. data/lib/generators/templates/type.rb.tt +57 -0
  69. data/lib/generators/templates/type_spec.rb.tt +42 -0
  70. data/lib/generators/templates/validator.rb.tt +26 -0
  71. data/lib/generators/templates/validator_spec.rb.tt +23 -0
  72. data/lib/generators/type.rb +29 -0
  73. data/lib/generators/validator.rb +41 -0
  74. metadata +92 -16
  75. data/.rspec_status +0 -196
  76. data/lib/castkit/core/registerable.rb +0 -59
  77. data/lib/castkit/default_serializer.rb +0 -154
  78. data/lib/castkit/serializer.rb +0 -92
  79. data/lib/castkit/validators/base_validator.rb +0 -39
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "options"
4
+
5
+ module Castkit
6
+ module Attributes
7
+ # Provides a class-based DSL for defining reusable attribute definitions.
8
+ #
9
+ # Extend this class in a subclass of `Castkit::Attributes::Base` to define
10
+ # shared attribute settings that can be reused across multiple DataObjects.
11
+ #
12
+ # @example Defining a reusable attribute
13
+ # class UuidDefinition < Castkit::Attributes::Base
14
+ # type :string
15
+ # required true
16
+ # format /\A[0-9a-f\-]{36}\z/
17
+ # end
18
+ #
19
+ # attribute :id, UuidDefinition.definition
20
+ #
21
+ class Definition
22
+ extend Castkit::Attributes::Options
23
+
24
+ class << self
25
+ # @return [Hash] the internal definition hash, containing the type and options
26
+ def definition
27
+ @definition ||= {
28
+ type: nil,
29
+ options: Castkit::Attributes::Options::DEFAULTS.dup
30
+ }
31
+ end
32
+
33
+ # @return [Hash] the attribute options defined on this class
34
+ def options
35
+ definition[:options]
36
+ end
37
+
38
+ # Defines the attribute's type and configuration using a DSL block.
39
+ #
40
+ # @param type [Symbol, Class<Castkit::DataObject>] the attribute type (e.g., :string, :integer)
41
+ # @param options [Hash] additional options to merge after the block (e.g., default:, access:)
42
+ # @yield DSL block used to set options like `required`, `format`, `readonly`, etc.
43
+ # @return [Array<(Symbol, Hash)>] a tuple of the final type and options hash
44
+ #
45
+ # @example
46
+ # define :string, default: "none" do
47
+ # required true
48
+ # access [:read]
49
+ # end
50
+ def define(type, **options, &block)
51
+ @__castkit_attribute_dsl = true
52
+
53
+ definition[:type] = type
54
+ instance_eval(&block)
55
+ definition[:options] = definition[:options].merge(options)
56
+
57
+ definition
58
+ ensure
59
+ @__castkit_attribute_dsl = false
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castkit
4
+ module Attributes
5
+ # Provides a DSL for configuring attribute options within an attribute definition.
6
+ #
7
+ # This module is designed to be extended by class-level definition objects such as
8
+ # `Castkit::Attributes::Definition`, and is used to build reusable sets of options
9
+ # for attributes declared within `Castkit::DataObject` classes.
10
+ #
11
+ # @example
12
+ # class OptionalString < Castkit::Attributes::Definition
13
+ # type :string
14
+ # required false
15
+ # ignore_blank true
16
+ # end
17
+ module Options
18
+ # Valid access modes for an attribute.
19
+ #
20
+ # @return [Array<Symbol>]
21
+ ACCESS_MODES = %i[read write].freeze
22
+
23
+ # Default configuration for attribute options.
24
+ #
25
+ # @return [Hash{Symbol => Object}]
26
+ DEFAULTS = {
27
+ required: true,
28
+ ignore_nil: false,
29
+ ignore_blank: false,
30
+ ignore: false,
31
+ composite: false,
32
+ transient: false,
33
+ unwrapped: false,
34
+ prefix: nil,
35
+ access: ACCESS_MODES,
36
+ force_type: !Castkit.configuration.enforce_typing
37
+ }.freeze
38
+
39
+ # Sets or retrieves the attribute type.
40
+ #
41
+ # @param value [Symbol, nil] The type to assign (e.g., :string), or nil to fetch.
42
+ # @return [Symbol]
43
+ def type(value = nil)
44
+ value.nil? ? definition[:type] : (definition[:type] = value.to_sym)
45
+ end
46
+
47
+ # Sets the element type for array attributes.
48
+ #
49
+ # @param value [Symbol, Class] the type of elements in the array
50
+ # @return [void]
51
+ def of(value)
52
+ return unless @type == :array
53
+
54
+ set_option(:of, value)
55
+ end
56
+
57
+ # Sets the default value or proc for the attribute.
58
+ #
59
+ # @param value [Object, Proc] the default value or lambda
60
+ # @return [void]
61
+ def default(value = nil)
62
+ set_option(:default, value)
63
+ end
64
+
65
+ # Enables or disables forced typecasting, or sets a custom flag.
66
+ #
67
+ # @param value [Boolean, nil] the forced type flag
68
+ # @return [void]
69
+ def force_type(value = nil)
70
+ set_option(:force_type, value || !Castkit.configuration.enforce_typing)
71
+ end
72
+
73
+ # Marks the attribute as required or optional.
74
+ #
75
+ # @param value [Boolean]
76
+ # @return [void]
77
+ def required(value = nil)
78
+ set_option(:required, value || true)
79
+ end
80
+
81
+ # Marks the attribute to be ignored entirely.
82
+ #
83
+ # @param value [Boolean]
84
+ # @return [void]
85
+ def ignore(value = nil)
86
+ set_option(:ignore, value || true)
87
+ end
88
+
89
+ # Ignores `nil` values during serialization or persistence.
90
+ #
91
+ # @param value [Boolean]
92
+ # @return [void]
93
+ def ignore_nil(value = nil)
94
+ set_option(:ignore_nil, value || true)
95
+ end
96
+
97
+ # Ignores blank values (`""`, `[]`, `{}`) during serialization.
98
+ #
99
+ # @param value [Boolean]
100
+ # @return [void]
101
+ def ignore_blank(value = nil)
102
+ set_option(:ignore_blank, value || true)
103
+ end
104
+
105
+ # Adds a prefix for unwrapped attribute keys.
106
+ #
107
+ # @param value [String, Symbol, nil]
108
+ # @return [void]
109
+ def prefix(value = nil)
110
+ set_option(:prefix, value)
111
+ end
112
+
113
+ # Marks the attribute as unwrapped (inline merging of nested fields).
114
+ #
115
+ # @param value [Boolean]
116
+ # @return [void]
117
+ def unwrapped(value = nil)
118
+ set_option(:unwrapped, value || true)
119
+ end
120
+
121
+ # Sets access modes for the attribute.
122
+ #
123
+ # @param value [Array<Symbol>, Symbol] valid values: `:read`, `:write`, or both
124
+ # @return [void]
125
+ def access(value = nil)
126
+ value = validate_access_modes!(value)
127
+ set_option(:access, value)
128
+ end
129
+
130
+ # Shortcut to make the attribute readonly (`access: [:read]`).
131
+ #
132
+ # @param value [Boolean]
133
+ # @return [void]
134
+ def readonly(value = nil)
135
+ value = value || true ? [:read] : ACCESS_MODES
136
+ set_option(:access, value)
137
+ end
138
+
139
+ # Marks the attribute as a composite (e.g., nested `DataObject`).
140
+ #
141
+ # @param value [Boolean]
142
+ # @return [void]
143
+ def composite(value = nil)
144
+ set_option(:composite, value || true)
145
+ end
146
+
147
+ # Marks the attribute as transient (not included in persistence or serialization).
148
+ #
149
+ # @param value [Boolean]
150
+ # @return [void]
151
+ def transient(value = nil)
152
+ set_option(:transient, value || true)
153
+ end
154
+
155
+ # Sets a format constraint (e.g., regex validation).
156
+ #
157
+ # @param value [Regexp]
158
+ # @return [void]
159
+ def format(value)
160
+ set_option(:format, value)
161
+ end
162
+
163
+ # Attaches a custom validator callable for this attribute.
164
+ #
165
+ # @param value [Proc, #call]
166
+ # @return [void]
167
+ def validator(value)
168
+ set_option(:validator, value)
169
+ end
170
+
171
+ private
172
+
173
+ # Converts class or symbol into a normalized type symbol.
174
+ #
175
+ # @param type [Class, Symbol]
176
+ # @return [Symbol]
177
+ # @raise [Castkit::AttributeError] if type cannot be resolved
178
+ def process_type(type)
179
+ case type
180
+ when Class
181
+ return :boolean if [TrueClass, FalseClass].include?(type)
182
+
183
+ type.name.downcase.to_sym
184
+ when Symbol
185
+ type
186
+ else
187
+ raise Castkit::AttributeError.new("Unknown type: #{type.inspect}", context: to_h)
188
+ end
189
+ end
190
+
191
+ # Sets an option key-value pair in the current definition.
192
+ #
193
+ # @param option [Symbol]
194
+ # @param value [Object, nil]
195
+ # @return [Object, nil]
196
+ def set_option(option, value)
197
+ value.nil? ? definition[:options][option] : (definition[:options][option] = value)
198
+ end
199
+
200
+ # Validates and normalizes access mode array.
201
+ #
202
+ # @param value [Array<Symbol>, Symbol, nil]
203
+ # @return [Array<Symbol>]
204
+ # @raise [Castkit::AttributeError] if invalid modes are present
205
+ def validate_access_modes!(value)
206
+ value_array = Array(value || ACCESS_MODES).compact
207
+ unknown_modes = value_array - ACCESS_MODES
208
+ return value_array if unknown_modes.empty?
209
+
210
+ raise Castkit::AttributeError.new("Unknown access flags: #{unknown_modes.inspect}", context: to_h)
211
+ end
212
+ end
213
+ end
214
+ end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "configuration"
4
- require_relative "inflector"
3
+ require_relative "core/attribute_types"
5
4
 
6
5
  # Castkit is a lightweight, type-safe data object system for Ruby.
7
6
  #
@@ -20,12 +19,15 @@ require_relative "inflector"
20
19
  # @see Castkit::Contract
21
20
  # @see Castkit::DataObject
22
21
  module Castkit
23
- # Namespace used for registering DataObjects generated from contracts.
22
+ # Namespace used for registering generated DataObjects.
24
23
  module DataObjects; end
25
24
 
26
- # Namespace used for registering contracts generated from DataObjects.
25
+ # Namespace used for registering generated contracts.
27
26
  module Contracts; end
28
27
 
28
+ # Namespace used for registering generated plugins.
29
+ module Plugins; end
30
+
29
31
  class << self
30
32
  # Yields the global configuration object for customization.
31
33
  #
@@ -60,7 +62,10 @@ module Castkit
60
62
  # @param obj [Object] the object to test
61
63
  # @return [Boolean] true if obj is a Castkit::DataObject class
62
64
  def dataobject?(obj)
63
- obj.is_a?(Class) && obj.ancestors.include?(Castkit::DataObject)
65
+ obj.is_a?(Class) && (
66
+ obj <= Castkit::DataObject ||
67
+ obj.ancestors.include?(Castkit::DSL::DataObject)
68
+ )
64
69
  end
65
70
 
66
71
  # Returns a type caster lambda for the given type.
@@ -102,3 +107,11 @@ module Castkit
102
107
  end
103
108
  end
104
109
  end
110
+
111
+ require_relative "configuration"
112
+ require_relative "plugins"
113
+ require_relative "inflector"
114
+ require_relative "version"
115
+ require_relative "attribute"
116
+ require_relative "contract"
117
+ require_relative "data_object"
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require_relative "../../generators/attribute"
5
+ require_relative "../../generators/contract"
6
+ require_relative "../../generators/data_object"
7
+ require_relative "../../generators/plugin"
8
+ require_relative "../../generators/serializer"
9
+ require_relative "../../generators/type"
10
+ require_relative "../../generators/validator"
11
+
12
+ module Castkit
13
+ module CLI
14
+ # Thor CLI class for generating Castkit components.
15
+ #
16
+ # Provides `castkit generate` commands for each major Castkit component, including types,
17
+ # data objects, contracts, validators, serializers, and plugins.
18
+ #
19
+ # All generators support the `--no-spec` flag to skip spec file creation.
20
+ class Generate < Thor
21
+ desc "contract NAME", "Generates a new Castkit contract"
22
+ method_option :spec, type: :boolean, default: true
23
+ # Generates a new contract class.
24
+ #
25
+ # @param name [String] the class name for the contract
26
+ # @param fields [Array<String>] optional attribute definitions
27
+ # @return [void]
28
+ def contract(name, *fields)
29
+ args = [Castkit::Inflector.pascalize(name), fields]
30
+ args << "--no-spec" unless options[:spec]
31
+ Castkit::Generators::Contract.start(args)
32
+ end
33
+
34
+ desc "attribute NAME", "Generates a new Castkit attribute"
35
+ method_option :spec, type: :boolean, default: true
36
+ # Generates a new attribute definition class.
37
+ #
38
+ # @param name [String] the class name for the attribute
39
+ # @param fields [Array<String>] optional attribute options
40
+ # @return [void]
41
+ def attribute(name, *fields)
42
+ args = [Castkit::Inflector.pascalize(name), fields]
43
+ args << "--no-spec" unless options[:spec]
44
+ Castkit::Generators::Attribute.start(args)
45
+ end
46
+
47
+ desc "dataobject NAME", "Generates a new Castkit DataObject"
48
+ method_option :spec, type: :boolean, default: true
49
+ # Generates a new DataObject class.
50
+ #
51
+ # @param name [String] the class name for the data object
52
+ # @param fields [Array<String>] optional attribute definitions
53
+ # @return [void]
54
+ def dataobject(name, *fields)
55
+ args = [Castkit::Inflector.pascalize(name), fields]
56
+ args << "--no-spec" unless options[:spec]
57
+ Castkit::Generators::DataObject.start(args)
58
+ end
59
+
60
+ desc "plugin NAME", "Generates a new Castkit plugin"
61
+ method_option :spec, type: :boolean, default: true
62
+ # Generates a new plugin module.
63
+ #
64
+ # @param name [String] the module name for the plugin
65
+ # @param fields [Array<String>] optional stub fields
66
+ # @return [void]
67
+ def plugin(name, *fields)
68
+ args = [Castkit::Inflector.pascalize(name), fields]
69
+ args << "--no-spec" unless options[:spec]
70
+ Castkit::Generators::Plugin.start(args)
71
+ end
72
+
73
+ desc "serializer NAME", "Generates a new Castkit serializer"
74
+ method_option :spec, type: :boolean, default: true
75
+ # Generates a new custom serializer class.
76
+ #
77
+ # @param name [String] the class name for the serializer
78
+ # @param fields [Array<String>] optional stub fields
79
+ # @return [void]
80
+ def serializer(name, *fields)
81
+ args = [Castkit::Inflector.pascalize(name), fields]
82
+ args << "--no-spec" unless options[:spec]
83
+ Castkit::Generators::Serializer.start(args)
84
+ end
85
+
86
+ desc "type NAME", "Generates a new Castkit type"
87
+ method_option :spec, type: :boolean, default: true
88
+ # Generates a new custom type.
89
+ #
90
+ # @param name [String] the class name for the type
91
+ # @return [void]
92
+ def type(name)
93
+ args = [Castkit::Inflector.pascalize(name)]
94
+ args << "--no-spec" unless options[:spec]
95
+ Castkit::Generators::Type.start(args)
96
+ end
97
+
98
+ desc "validator NAME", "Generates a new Castkit validator"
99
+ method_option :spec, type: :boolean, default: true
100
+ # Generates a new validator class.
101
+ #
102
+ # @param name [String] the class name for the validator
103
+ # @param fields [Array<String>] optional stub fields
104
+ # @return [void]
105
+ def validator(name, *fields)
106
+ args = [Castkit::Inflector.pascalize(name), fields]
107
+ args << "--no-spec" unless options[:spec]
108
+ Castkit::Generators::Validator.start(args)
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "castkit"
5
+ require_relative "../../generators/contract"
6
+ require_relative "../../generators/data_object"
7
+ require_relative "../../generators/plugin"
8
+ require_relative "../../generators/serializer"
9
+ require_relative "../../generators/type"
10
+ require_relative "../../generators/validator"
11
+
12
+ module Castkit
13
+ module CLI
14
+ # CLI commands for listing internal Castkit registry components.
15
+ #
16
+ # Supports listing:
17
+ # - Registered types (`castkit list types`)
18
+ # - Available validators (`castkit list validators`)
19
+ #
20
+ # @example Show all available types
21
+ # $ castkit list types
22
+ #
23
+ # @example Show all defined validators
24
+ # $ castkit list validators
25
+ class List < Thor
26
+ desc "types", "Lists registered Castkit types"
27
+ # Lists registered Castkit types, grouped into native and custom-defined.
28
+ #
29
+ # @return [void]
30
+ def types
31
+ all_keys = Castkit.configuration.types
32
+ default_keys = Castkit::Configuration::DEFAULT_TYPES.keys
33
+
34
+ native_types(all_keys, default_keys)
35
+ custom_types(all_keys, default_keys)
36
+ end
37
+
38
+ desc "contracts", "Lists all generated Castkit contracts"
39
+ # Lists all Castkit contract classes defined in the file system or registered under the Castkit namespace.
40
+ #
41
+ # @return [void]
42
+ def contracts
43
+ list_files("contracts")
44
+ end
45
+
46
+ desc "dataobjects", "Lists all generated Castkit DataObjects"
47
+ # Lists all Castkit DataObjects classes defined in the file system or registered under the Castkit namespace.
48
+ #
49
+ # @return [void]
50
+ def dataobjects
51
+ list_files("data_objects")
52
+ end
53
+
54
+ desc "serializers", "Lists all generated Castkit serializers"
55
+ # Lists all Castkit serializers classes defined in the file system or registered under the Castkit namespace.
56
+ #
57
+ # @return [void]
58
+ def serializers
59
+ list_files("serializers")
60
+ end
61
+
62
+ desc "validators", "Lists all generated Castkit validators"
63
+ # Lists all Castkit validator classes defined in the file system or registered under the Castkit namespace.
64
+ #
65
+ # @return [void]
66
+ def validators
67
+ list_files("validators")
68
+ end
69
+
70
+ private
71
+
72
+ # Prints all native types and their aliases.
73
+ #
74
+ # @param all_types [Hash<Symbol, Object>] all registered types
75
+ # @param default_keys [Array<Symbol>] predefined native type keys
76
+ # @return [void]
77
+ def native_types(all_types, default_keys)
78
+ alias_map = reverse_grouped(Castkit::Configuration::TYPE_ALIASES)
79
+ native = all_types.slice(*default_keys)
80
+
81
+ say "Native Types:", :green
82
+ native.each do |name, type|
83
+ aliases = alias_map[name] || []
84
+ list_type(type.class, [name, *aliases].map(&:to_sym))
85
+ end
86
+ end
87
+
88
+ # Prints all custom (non-native, non-alias) registered types.
89
+ #
90
+ # @param all_types [Hash<Symbol, Object>]
91
+ # @param default_keys [Array<Symbol>]
92
+ # @return [void]
93
+ def custom_types(all_types, default_keys)
94
+ alias_keys = Castkit::Configuration::TYPE_ALIASES.keys.map(&:to_sym)
95
+ custom = all_types.except(*default_keys).reject { |k, _| alias_keys.include?(k) }
96
+
97
+ say "\nCustom Types:", :green
98
+ return no_custom_types if custom.empty?
99
+
100
+ grouped_custom_types(custom)
101
+ end
102
+
103
+ # Outputs a fallback message if no custom types exist.
104
+ #
105
+ # @return [void]
106
+ def no_custom_types
107
+ say " No registered types, register with " \
108
+ "#{set_color("Castkit.configure { |c| c.register_type(:type, Type) }", :yellow)}"
109
+ end
110
+
111
+ # Groups and prints custom types by their class.
112
+ #
113
+ # @param types [Hash<Symbol, Object>]
114
+ # @return [void]
115
+ def grouped_custom_types(types)
116
+ types.group_by { |_, inst| inst.class }.each do |klass, group|
117
+ list_type(klass, group.map(&:first).map(&:to_sym))
118
+ end
119
+ end
120
+
121
+ # Reverses a hash of alias => type into type => [aliases].
122
+ #
123
+ # @param hash [Hash]
124
+ # @return [Hash{Symbol => Array<Symbol>}]
125
+ def reverse_grouped(hash)
126
+ hash.each_with_object(Hash.new { |h, k| h[k] = [] }) do |(k, v), acc|
127
+ acc[v] << k
128
+ end
129
+ end
130
+
131
+ # Prints a type or class with all its symbol aliases.
132
+ #
133
+ # @param klass [Class]
134
+ # @param keys [Array<Symbol>]
135
+ # @return [void]
136
+ def list_type(klass, keys)
137
+ types = keys.uniq.sort.map { |k| set_color(":#{k}", :yellow) }.join(", ")
138
+ say " #{(klass.name || "<AnonymousType>").ljust(34)} - #{types}"
139
+ end
140
+
141
+ # Lists class references for a component (e.g. validators), distinguishing by source (file or custom).
142
+ #
143
+ # @param component [String] base namespace (e.g. "validators")
144
+ # @return [void]
145
+ def list_files(component)
146
+ path = "lib/castkit/#{component}"
147
+ all_classes, file_classes = component_classes(component, path)
148
+ return say "No registered #{Castkit::Inflector.pascalize(component)} found." if all_classes.empty?
149
+
150
+ max_width = all_classes.map(&:length).max + 5
151
+ say "Castkit #{Castkit::Inflector.pascalize(component)}", :green
152
+
153
+ all_classes.each do |klass|
154
+ tag = file_classes.include?(klass) ? set_color("[Castkit]", :yellow) : set_color("[Custom]", :green)
155
+ say " #{klass.ljust(max_width)} #{tag}"
156
+ end
157
+ end
158
+
159
+ # Gathers all registered and defined constants for a component.
160
+ #
161
+ # @param component [String]
162
+ # @param path [String]
163
+ # @return [Array<[Array<String>, Set<String>]>]
164
+ def component_classes(component, path)
165
+ namespace = Castkit.const_get(Castkit::Inflector.pascalize(component))
166
+ file_classes = file_classes(namespace, path)
167
+ defined_classes = defined_classes(namespace)
168
+
169
+ all_classes = (file_classes + defined_classes).to_a.sort
170
+ [all_classes, file_classes]
171
+ end
172
+
173
+ # Converts file names into class names for a given component.
174
+ #
175
+ # @param namespace [Module]
176
+ # @param path [String]
177
+ # @return [Set<String>]
178
+ def file_classes(namespace, path)
179
+ classes = Dir.glob("#{path}/*.rb")
180
+ .map { |f| File.basename(f, ".rb") }
181
+ .reject { |f| f.to_s == "base" }
182
+ .map { |base| "#{namespace}::#{Castkit::Inflector.pascalize(base)}" }
183
+
184
+ classes.to_set
185
+ end
186
+
187
+ # Lists actual constants under a namespace, filtering out missing definitions.
188
+ #
189
+ # @param namespace [Module]
190
+ # @return [Set<String>]
191
+ def defined_classes(namespace)
192
+ namespace.constants
193
+ .reject { |const| const.to_s == "Base" }
194
+ .map { |const| "#{namespace}::#{const}" }
195
+ .select { |klass| Object.const_defined?(klass) }
196
+ .to_set
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "generate"
4
+ require_relative "list"
5
+
6
+ module Castkit
7
+ module CLI
8
+ # Main CLI entry point for Castkit.
9
+ #
10
+ # Provides top-level commands for printing the gem version and generating Castkit components.
11
+ #
12
+ # @example Print the version
13
+ # $ castkit version
14
+ #
15
+ # @example Generate a DataObject
16
+ # $ castkit generate dataobject User name:string age:integer
17
+ class Main < Thor
18
+ desc "version", "Prints the version"
19
+ # Outputs the current Castkit version.
20
+ #
21
+ # @return [void]
22
+ def version
23
+ puts Castkit::VERSION
24
+ end
25
+
26
+ desc "generate TYPE NAME", "Generate a Castkit component"
27
+ # Dispatches to the `castkit generate` subcommands.
28
+ #
29
+ # Supports generating components like `type`, `dataobject`, `contract`, etc.
30
+ #
31
+ # @return [void]
32
+ subcommand "generate", Castkit::CLI::Generate
33
+
34
+ desc "list COMPONENT", "List registered Castkit components"
35
+ # Dispatches to the `castkit list` subcommands.
36
+ #
37
+ # Supports listing components like `type`, `dataobject`, `contract`, etc.
38
+ #
39
+ # @return [void]
40
+ subcommand "list", Castkit::CLI::List
41
+ end
42
+ end
43
+ end