vcdry 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []