vcdry 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 185f278d99a71c67953d1453a9b2c62ee2afac0c914d3a4c8793c5677396b232
4
+ data.tar.gz: 84290611068e8991a71164cdd11ca4d2e318ce364975905b1ece0c75e5a2dada
5
+ SHA512:
6
+ metadata.gz: 95dd05bd5d0654374b46a99d43cb0029e9302a3b38a0d60e4278b64a2e0edb2e5dce528a91a0c732ab494fafa9e2cf287977960f31cc4d4411ec19947f80eb7b
7
+ data.tar.gz: c47f92e3d43e7b96f7901b37a4633d6a8e9a96a787e8fc9e2ad0d4d922d76bcfe532eb623059305b3c7c348f629e22bf580baafaba39cba3737927bf45a2b7c1
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [1.0.0] - 2023-04-16
4
+
5
+ - Initial release
data/README.md ADDED
@@ -0,0 +1,305 @@
1
+ # VCDry
2
+
3
+ [![Test](https://github.com/wamonroe/vcdry/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/wamonroe/vcdry/actions/workflows/test.yml)
4
+
5
+ Simple DSL designed with [View Components](https://viewcomponent.org) in mind to
6
+ make defining and setting keyword arguments and instance variables easier.
7
+
8
+ Before:
9
+
10
+ ```ruby
11
+ class HeadingComponent < ApplicationComponent
12
+ def initialize(text:, tag: "h1", **options)
13
+ @text = text
14
+ @tag = tag
15
+ @options = options
16
+ end
17
+ end
18
+ ```
19
+
20
+ After:
21
+
22
+ ```ruby
23
+ class HeadingComponent < ApplicationComponent
24
+ include VCDry::DSL
25
+
26
+ keyword :text
27
+ keyword :tag, default: "h1"
28
+ other_keywords :options
29
+ end
30
+ ```
31
+
32
+ ## Table of Contents
33
+
34
+ - [VCDry](#vcdry)
35
+ - [Table of Contents](#table-of-contents)
36
+ - [Installation](#installation)
37
+ - [General Usage](#general-usage)
38
+ - [Overview](#overview)
39
+ - [keyword](#keyword)
40
+ - [other_keywords](#other_keywords)
41
+ - [strict_keywords](#strict_keywords)
42
+ - [remove_keyword](#remove_keyword)
43
+ - [Types](#types)
44
+ - [Development](#development)
45
+ - [Contributing](#contributing)
46
+ - [License](#license)
47
+
48
+ ## Installation
49
+
50
+ Add this line to your application's Gemfile:
51
+
52
+ ```ruby
53
+ gem "vcdry"
54
+ ```
55
+
56
+ And then execute:
57
+
58
+ ```sh
59
+ bundle install
60
+ ```
61
+
62
+ ## General Usage
63
+
64
+ ### Overview
65
+
66
+ Include `VCDry::DSL` in your component.
67
+
68
+ ```ruby
69
+ class ApplicationComponent < ViewComponent::Base
70
+ include VCDry::DSL
71
+ end
72
+ ```
73
+
74
+ Use the `keyword` method to define a keyword.
75
+
76
+ ```ruby
77
+ class MyComponent < ApplicationComponent
78
+ keyword :name
79
+ end
80
+ ```
81
+
82
+ By default, specifying an unknown keyword results in an error.
83
+
84
+ ```ruby
85
+ MyComponent.new(class: "mt-1")
86
+ # unknown keyword: :class (VCDry::UnknownArgumentError)
87
+ ```
88
+
89
+ To disable this behavior, specify `strick_keywords false`.
90
+
91
+ ```ruby
92
+ class MyComponent < ApplicationComponent
93
+ strict_keywords false
94
+ end
95
+ ```
96
+
97
+ Or, to save those unknown keywords into a variable, use `other_keywords`.
98
+
99
+ ```ruby
100
+ class MyComponent < ApplicationComponent
101
+ other_keywords :options
102
+ end
103
+ ```
104
+
105
+ Keywords specified on a parent component are inherited by a child component.
106
+
107
+ ```ruby
108
+ class ApplicationComponent < ViewComponent::Base
109
+ include VCDry::DSL
110
+
111
+ other_keywords :options
112
+ end
113
+
114
+ class ParentComponent < ApplicationComponent
115
+ keyword :name
116
+ end
117
+
118
+ class ChildComponent < ParentComponent
119
+ keyword :age
120
+ end
121
+
122
+ ChildComponent.new(name: "Child", age: 7, class: "mt-1").instance_variables
123
+ # => [:@name, :@age, :@options]
124
+ ```
125
+
126
+ ### keyword
127
+
128
+ Define a keyword variable to read and store to an instance variable when
129
+ instantiating a component.
130
+
131
+ ```ruby
132
+ class MyComponent
133
+ keyword :name
134
+ end
135
+ ```
136
+
137
+ Specify a type to typecast the value specified.
138
+
139
+ ```ruby
140
+ class MyComponent
141
+ keyword :name, :string
142
+ end
143
+ ```
144
+
145
+ You can use any of the built-in types or create your own as defined in
146
+ [Types](#types). Additionally you can specify a proc to define
147
+ your own one off type for a component.
148
+
149
+ ```ruby
150
+ class MyComponent
151
+ keyword :name, ->(value) { "custom #{value}" }
152
+ end
153
+ ```
154
+
155
+ By default, keywords are required. To make a keyword optional, specify a default
156
+ value using the `:default` or pass `optional: true`.
157
+
158
+ ```ruby
159
+ class MyComponent
160
+ keyword :padding, :integer, optional: true
161
+ keyword :size, :symbol, default: :md
162
+ end
163
+ ```
164
+
165
+ When specifying a default, you can also pass the value as a proc to resolve the
166
+ default value.
167
+
168
+ ```ruby
169
+ class MyComponent
170
+ keyword :options, :hash, default: -> { Hash.new }
171
+ end
172
+ ```
173
+
174
+ You can instruct a keyword to only accept a predefined set of values by using
175
+ the `:values` option.
176
+
177
+ ```ruby
178
+ class MyComponent
179
+ keyword :size, :symbol, values: [:sm, :md, :lg]
180
+ end
181
+ ```
182
+
183
+ You can also instruct a keyword to accept an array of values by passing the
184
+ `array: true` option.
185
+
186
+ ```ruby
187
+ class MyComponent
188
+ keyword :author_ids, :string, array: true
189
+ end
190
+ ```
191
+
192
+ A child component can override the declaration of a keyword from a parent
193
+ component.
194
+
195
+ ```ruby
196
+ class MyOtherComponent < MyComponent
197
+ keyword :author_ids, :integer, array: true
198
+ end
199
+ ```
200
+
201
+ ### other_keywords
202
+
203
+ To gather all keywords not explicitly defined by the `keyword` method, use the
204
+ `other_keywords` method.
205
+
206
+ ```ruby
207
+ class MyComponent
208
+ keyword :name
209
+ other_keywords :options
210
+ end
211
+ ```
212
+
213
+ In the example above, the `:name` keyword would be stored in the variable
214
+ `@name` while all other keywords specified would be stored in the variable
215
+ `@options`.
216
+
217
+ If you have a custom type defined that acts similarly to a `Hash` (like the
218
+ [TagOptions::Hash](https://github.com/wamonroe/tag_options) gem), you can pass
219
+ that in to the `other_keywords` declaration.
220
+
221
+ ```ruby
222
+ class MyComponent
223
+ other_keywords :options, :tag_options
224
+ end
225
+ ```
226
+
227
+ **Note**: You must register a custom type using `VCDry::Types.add_type` as
228
+ detailed in [Types](#types).
229
+
230
+ ### strict_keywords
231
+
232
+ By default, if you specify a unknown keyword when instantiating a component an
233
+ error will be raised. To silently discard the additional keywords, use the
234
+ `strict_keywords false` declaration.
235
+
236
+ ```ruby
237
+ class MyComponent
238
+ keyword :name
239
+ strict_keywords false
240
+ end
241
+ ```
242
+
243
+ To turn it back on for a component that inherits from parent component that
244
+ turned off strict keywords, use the `strict_keywords true` declaration.
245
+
246
+ ```ruby
247
+ class MyOtherComponent < MyComponent
248
+ strict_keywords true
249
+ end
250
+ ```
251
+
252
+ ### remove_keyword
253
+
254
+ To remove a keyword specified on a component that inherits from parent
255
+ component, use the `remove_keyword` method.
256
+
257
+ ```ruby
258
+ class MyOtherComponent < MyComponent
259
+ remove_keyword :name
260
+ end
261
+ ```
262
+
263
+ ## Types
264
+
265
+ The following types are built-in to `vcdry`.
266
+
267
+ - boolean
268
+ - datetime
269
+ - hash
270
+ - integer
271
+ - string
272
+ - symbol
273
+
274
+ Additionally you can define custom, or override built-in, types by using
275
+ `VCDry::Types.add_type`.
276
+
277
+ ```ruby
278
+ # config/initializers/vcdry_types.rb
279
+ VCDry::Types.add_type(:boolean, ->(value) { ActiveRecord::Type::Boolean.new.cast(value) })
280
+ VCDry::Types.add_type(:custom_hash, ->(value) { CustomHash.new(value) })
281
+ ```
282
+
283
+ ## Development
284
+
285
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
286
+ `bin/rspec` to run the tests. You can also run:
287
+
288
+ - `bin/console` for an interactive prompt that will allow you to experiment
289
+ - `bin/rubocop` to run RuboCop to check the code style and formatting
290
+
291
+ To build this gem on your local machine, run `bundle exec rake build`. To
292
+ release a new version, update the version number in `version.rb`, and then run
293
+ `bundle exec rake release`, which will create a git tag for the version, push
294
+ git commits and the created tag, and push the `.gem` file to
295
+ [rubygems.org](https://rubygems.org).
296
+
297
+ ## Contributing
298
+
299
+ Bug reports and pull requests are welcome on GitHub at
300
+ https://github.com/wamonroe/vcdry.
301
+
302
+ ## License
303
+
304
+ The gem is available as open source under the terms of the [MIT
305
+ License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/test_*.rb"]
10
+ end
11
+
12
+ require "standard/rake"
13
+
14
+ task default: %i[test standard]
@@ -0,0 +1,83 @@
1
+ require "active_support/core_ext/enumerable"
2
+
3
+ require_relative "error"
4
+ require_relative "types"
5
+
6
+ module VCDry
7
+ class Config
8
+ NOT_DEFINED = Object.new.freeze
9
+
10
+ attr_reader :name
11
+
12
+ def initialize(name, type = nil, **options)
13
+ @name = name.to_sym
14
+ @type = (type.nil? || type.respond_to?(:call)) ? type : VCDry::Types[type]
15
+ @options = options
16
+ end
17
+
18
+ def array?
19
+ !!@options[:array]
20
+ end
21
+
22
+ def default?
23
+ @options.key?(:default)
24
+ end
25
+
26
+ def dup
27
+ self.class.new(@name, @type, **@options.dup)
28
+ end
29
+
30
+ def enum?
31
+ @options.key?(:values)
32
+ end
33
+
34
+ def enum_values
35
+ @enum_values ||= Array(@options[:values]).map do |value|
36
+ @type.nil? ? value : @type.call(value)
37
+ end
38
+ end
39
+
40
+ def default
41
+ value = @options.fetch(:default, NOT_DEFINED)
42
+ value = value.call if value.respond_to?(:call)
43
+ value
44
+ end
45
+
46
+ def instance_variable
47
+ "@#{name}"
48
+ end
49
+
50
+ def optional?
51
+ default? || !!@options[:optional]
52
+ end
53
+
54
+ def required?
55
+ !optional?
56
+ end
57
+
58
+ def type_cast(value)
59
+ array? ? type_cast_array(value) : type_cast_value(value)
60
+ end
61
+
62
+ private
63
+
64
+ def type_cast_array(values)
65
+ return if values == NOT_DEFINED
66
+
67
+ Array(values).map do |value|
68
+ type_cast_value(value)
69
+ end
70
+ end
71
+
72
+ def type_cast_value(value)
73
+ return if value == NOT_DEFINED
74
+ return value if @type.nil?
75
+
76
+ value = @type.call(value)
77
+ if enum? && enum_values.exclude?(value)
78
+ raise InvalidEnumValueError.new(name, enum_values)
79
+ end
80
+ value
81
+ end
82
+ end
83
+ end
data/lib/vcdry/dsl.rb ADDED
@@ -0,0 +1,71 @@
1
+ require "active_support/concern"
2
+ require "active_support/core_ext/hash"
3
+ require "active_support/core_ext/module/delegation"
4
+
5
+ require_relative "error"
6
+ require_relative "registry"
7
+
8
+ module VCDry
9
+ module DSL
10
+ extend ActiveSupport::Concern
11
+
12
+ def initialize(**kwargs)
13
+ kwargs = kwargs.symbolize_keys
14
+ vcdry_parse_keywords(**kwargs)
15
+ vcdry_parse_unknown_keywords(**kwargs)
16
+ end
17
+
18
+ def vcdry_parse_keywords(**kwargs)
19
+ self.class.vcdry.keyword_configs.each do |config|
20
+ if config.required? && !kwargs.key?(config.name)
21
+ raise MissingRequiredKeywordError.new(config.name)
22
+ end
23
+
24
+ value = kwargs.fetch(config.name, config.default)
25
+ instance_variable_set(config.instance_variable, config.type_cast(value))
26
+ end
27
+ end
28
+
29
+ def vcdry_parse_unknown_keywords(**kwargs)
30
+ unknown_kwargs = kwargs.except(*self.class.vcdry.keywords)
31
+ raise UnknownArgumentError.new(*unknown_kwargs.keys) if self.class.vcdry.strict? && unknown_kwargs.present?
32
+ return unless self.class.vcdry.gather_unknown_keywords?
33
+
34
+ config = self.class.vcdry.other_keywords_config
35
+ instance_variable_set(config.instance_variable, config.type_cast(unknown_kwargs))
36
+ end
37
+
38
+ class_methods do
39
+ delegate :other_keywords, :remove_keyword, :strict_keywords, to: :vcdry
40
+
41
+ def inherited(subclass)
42
+ subclass.instance_variable_set(:@vcdry, @vcdry&.dup)
43
+ end
44
+
45
+ def keyword(name, type = nil, **options)
46
+ name = name.to_sym
47
+ config = vcdry.keyword(name, type, options)
48
+ vcutils_define_helper_methods(config)
49
+ end
50
+
51
+ def vcdry
52
+ @vcdry ||= Registry.new
53
+ end
54
+
55
+ def vcutils_define_helper_methods(config)
56
+ if config.enum?
57
+ config.enum_values.each do |value|
58
+ define_method "#{config.name}_#{value}?" do
59
+ instance_variable_get(config.instance_variable) == value
60
+ end
61
+ end
62
+ end
63
+ if config.optional?
64
+ define_method "#{config.name}?" do
65
+ instance_variable_get(config.instance_variable).present?
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,33 @@
1
+ module VCDry
2
+ class Error < StandardError; end
3
+
4
+ class InvalidEnumValueError < Error
5
+ def initialize(name, values)
6
+ super("value for #{name} must be one of '#{values.join("', '")}'")
7
+ end
8
+ end
9
+
10
+ class MissingRequiredKeywordError < Error
11
+ def initialize(name)
12
+ super("missing required keyword: :#{name}")
13
+ end
14
+ end
15
+
16
+ class ReservedNameError < Error
17
+ def initialize(name)
18
+ super("'#{name}' is used to gather unknown keywords")
19
+ end
20
+ end
21
+
22
+ class UnknownArgumentError < Error
23
+ def initialize(*names)
24
+ super("unknown keyword: :#{names.join(" :")}")
25
+ end
26
+ end
27
+
28
+ class UnknownTypeError < Error
29
+ def initialize(type)
30
+ super("unknown type: #{type}")
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,60 @@
1
+ require_relative "config"
2
+ require_relative "error"
3
+
4
+ module VCDry
5
+ class Registry
6
+ attr_reader :other_keywords_config
7
+
8
+ def initialize
9
+ @keywords = {}
10
+ @other_keywords_config = nil
11
+ @strict = true
12
+ end
13
+
14
+ def dup
15
+ object = self.class.new
16
+ keywords = {}
17
+ @keywords.each do |name, config|
18
+ keywords[name] = config.dup
19
+ end
20
+ object.instance_variable_set(:@keywords, keywords)
21
+ object.instance_variable_set(:@other_keywords_config, @other_keywords_config&.dup)
22
+ object.instance_variable_set(:@strict, @strict)
23
+ object
24
+ end
25
+
26
+ def gather_unknown_keywords?
27
+ !@other_keywords_config.nil?
28
+ end
29
+
30
+ def keyword(name, type, options = {})
31
+ raise ReservedNameError.new(name) if @other_keywords_config&.name == name.to_sym
32
+
33
+ @keywords[name] = Config.new(name, type, **options)
34
+ end
35
+
36
+ def keyword_configs
37
+ @keywords.values
38
+ end
39
+
40
+ def keywords
41
+ @keywords.keys
42
+ end
43
+
44
+ def other_keywords(name, type: :hash)
45
+ @other_keywords_config = Config.new(name, type)
46
+ end
47
+
48
+ def strict?
49
+ !gather_unknown_keywords? && @strict
50
+ end
51
+
52
+ def strict_keywords(value)
53
+ @strict = !!value
54
+ end
55
+
56
+ def remove_keyword(name)
57
+ @keywords.delete(name)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,40 @@
1
+ require "active_support/core_ext/module/delegation"
2
+
3
+ module VCDry
4
+ module Types
5
+ class << self
6
+ delegate :add_type, :remove_type, :[], to: :registry
7
+
8
+ private
9
+
10
+ def registry
11
+ @registry ||= TypeRegistry.new
12
+ end
13
+ end
14
+
15
+ class TypeRegistry
16
+ def initialize
17
+ @types = {
18
+ boolean: ->(value) { !!value },
19
+ datetime: ->(value) { value.to_datetime },
20
+ hash: ->(value) { value.to_h },
21
+ integer: ->(value) { value.to_i },
22
+ string: ->(value) { value.to_s },
23
+ symbol: ->(value) { value.to_s.to_sym }
24
+ }
25
+ end
26
+
27
+ def add_type(name, method)
28
+ raise TypeError, "method must respond to #call" unless method.respond_to?(:call)
29
+
30
+ @types[name] = method
31
+ end
32
+
33
+ def [](name)
34
+ @types.fetch(name)
35
+ rescue KeyError
36
+ raise UnknownTypeError.new(name)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ module VCDry
2
+ VERSION = "1.0.0"
3
+ end
data/lib/vcdry.rb ADDED
@@ -0,0 +1,9 @@
1
+ require_relative "vcdry/dsl"
2
+ require_relative "vcdry/error"
3
+ require_relative "vcdry/config"
4
+ require_relative "vcdry/registry"
5
+ require_relative "vcdry/types"
6
+ require_relative "vcdry/version"
7
+
8
+ module VCDry
9
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vcdry
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Alex Monroe
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-04-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6.1'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '8.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '6.1'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '8.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: view_component
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '2.35'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '3.0'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '2.35'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '3.0'
53
+ - !ruby/object:Gem::Dependency
54
+ name: sqlite3
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ type: :development
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ description: DSL for declaring keyword parameters on ViewComponents.
68
+ email:
69
+ - alex@monroepost.com
70
+ executables: []
71
+ extensions: []
72
+ extra_rdoc_files: []
73
+ files:
74
+ - CHANGELOG.md
75
+ - README.md
76
+ - Rakefile
77
+ - lib/vcdry.rb
78
+ - lib/vcdry/config.rb
79
+ - lib/vcdry/dsl.rb
80
+ - lib/vcdry/error.rb
81
+ - lib/vcdry/registry.rb
82
+ - lib/vcdry/types.rb
83
+ - lib/vcdry/version.rb
84
+ homepage: https://github.com/wamonroe/vcdry
85
+ licenses:
86
+ - MIT
87
+ metadata:
88
+ homepage_uri: https://github.com/wamonroe/vcdry
89
+ source_code_uri: https://github.com/wamonroe/vcdry
90
+ changelog_uri: https://github.com/wamonroe/vcdry/blob/main/CHANGELOG.md
91
+ post_install_message:
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: 3.0.0
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ requirements: []
106
+ rubygems_version: 3.4.6
107
+ signing_key:
108
+ specification_version: 4
109
+ summary: DSL for declaring keyword parameters on ViewComponents.
110
+ test_files: []