amber_component 0.0.3 → 0.0.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 239144479c990df66cda6d83bd062bdcfd0b0affca32597ebd968d490f6817d8
4
- data.tar.gz: 8d8cc882bceabcbff1cf16d0b5881274f55068a30b91acac98c2e108be248c0c
3
+ metadata.gz: 45f702b26ec801eb7f67fa45e14c4fac035ff31d9ad6602418b7faa15c8e6004
4
+ data.tar.gz: 404f922961c15d865144d8f284511f0b93718bc85f93a1fea1c1b013aa6d815f
5
5
  SHA512:
6
- metadata.gz: f06b5e9288c557ae52015f637111ae6a6b90f0402a72b63873cdc1c3aae14d114c921bec304dda68b6ffddc4e4cc98e27d8cac28d84ec2758eb6da1d4176db28
7
- data.tar.gz: 8d950dcb6102a235f38078511705799a38956f94df12f8e5677f5d038c5adc598cf275ef2266c00f1ccfe9a54666793248afb076b982ea6858300692ce817776
6
+ metadata.gz: 17df8b345dade33fb27c5167b9ee7d17788b2092ecbff5aeb2f6fbeb12a6dd91813e7c2aa9c52b42aafea6e5965869630960b64c1d3da5be3365d4e3f8702148
7
+ data.tar.gz: ec46b84d953e9efcc4ae0a36e68f0a733bfd8e76f518eee3cae2c33a655df3be9ec592b09cd8ade8b88707934e55004d03c62c2a48a2152690edd86910b4cd2e
data/.solargraph.yml CHANGED
@@ -9,8 +9,7 @@ exclude:
9
9
  require: []
10
10
  domains: []
11
11
  reporters:
12
- - rubocop
13
- - require_not_found
12
+ - typecheck:typed
14
13
  formatter:
15
14
  rubocop:
16
15
  cops: safe
data/Gemfile CHANGED
@@ -13,7 +13,7 @@ gem 'git'
13
13
  gem 'haml'
14
14
  gem 'rubocop', '~> 1.21'
15
15
  gem 'sassc'
16
- gem 'solargraph', '~> 0.45.0'
16
+ gem 'solargraph'
17
17
  gem 'yard'
18
18
 
19
19
  # Testing dependencies
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- amber_component (0.0.3)
4
+ amber_component (0.0.4)
5
5
  actionview (>= 6)
6
6
  activemodel (>= 6)
7
7
  activesupport (>= 6)
@@ -73,7 +73,7 @@ GEM
73
73
  nokogiri (1.13.8-arm64-darwin)
74
74
  racc (~> 1.4)
75
75
  parallel (1.22.1)
76
- parser (3.1.2.0)
76
+ parser (3.1.2.1)
77
77
  ast (~> 2.4.1)
78
78
  racc (1.6.0)
79
79
  rack (2.2.4)
@@ -98,17 +98,17 @@ GEM
98
98
  reverse_markdown (2.1.1)
99
99
  nokogiri
100
100
  rexml (3.2.5)
101
- rubocop (1.32.0)
101
+ rubocop (1.36.0)
102
102
  json (~> 2.3)
103
103
  parallel (~> 1.10)
104
- parser (>= 3.1.0.0)
104
+ parser (>= 3.1.2.1)
105
105
  rainbow (>= 2.2.2, < 4.0)
106
106
  regexp_parser (>= 1.8, < 3.0)
107
107
  rexml (>= 3.2.5, < 4.0)
108
- rubocop-ast (>= 1.19.1, < 2.0)
108
+ rubocop-ast (>= 1.20.1, < 2.0)
109
109
  ruby-progressbar (~> 1.7)
110
110
  unicode-display_width (>= 1.4.0, < 3.0)
111
- rubocop-ast (1.19.1)
111
+ rubocop-ast (1.21.0)
112
112
  parser (>= 3.1.1.0)
113
113
  ruby-progressbar (1.11.0)
114
114
  ruby2_keywords (0.0.5)
@@ -124,7 +124,7 @@ GEM
124
124
  simplecov (~> 0.19)
125
125
  simplecov-html (0.12.3)
126
126
  simplecov_json_formatter (0.1.4)
127
- solargraph (0.45.0)
127
+ solargraph (0.46.0)
128
128
  backport (~> 1.2)
129
129
  benchmark
130
130
  bundler (>= 1.17.2)
@@ -144,7 +144,7 @@ GEM
144
144
  tilt (2.0.11)
145
145
  tzinfo (2.0.5)
146
146
  concurrent-ruby (~> 1.0)
147
- unicode-display_width (2.2.0)
147
+ unicode-display_width (2.3.0)
148
148
  webrick (1.7.0)
149
149
  yard (0.9.28)
150
150
  webrick (~> 1.7.0)
@@ -168,7 +168,7 @@ DEPENDENCIES
168
168
  shoulda-context (~> 2.0)
169
169
  simplecov
170
170
  simplecov-cobertura
171
- solargraph (~> 0.45.0)
171
+ solargraph
172
172
  yard
173
173
 
174
174
  BUNDLED WITH
@@ -48,6 +48,8 @@ module ::AmberComponent
48
48
  extend Assets::ClassMethods
49
49
  include Rendering::InstanceMethods
50
50
  extend Rendering::ClassMethods
51
+ include Props::InstanceMethods
52
+ extend Props::ClassMethods
51
53
 
52
54
  class << self
53
55
  include ::Memery
@@ -67,37 +69,51 @@ module ::AmberComponent
67
69
  # @return [void]
68
70
  def inherited(subclass)
69
71
  super
70
- # @type [Module]
71
- method_body = proc do |**kwargs, &block|
72
+ method_body = lambda do |**kwargs, &block|
72
73
  subclass.render(**kwargs, &block)
73
74
  end
74
75
  parent_module = subclass.module_parent
75
76
 
76
77
  if parent_module.equal?(::Object)
77
78
  method_name = subclass.name
78
- define_helper_method(subclass, Helpers::ComponentHelper, method_name, method_body)
79
79
  define_helper_method(subclass, Helpers::ComponentHelper, method_name.underscore, method_body)
80
80
  return
81
81
  end
82
82
 
83
83
  method_name = subclass.const_name
84
- define_helper_method(subclass, parent_module.singleton_class, method_name, method_body)
85
84
  define_helper_method(subclass, parent_module.singleton_class, method_name.underscore, method_body)
86
85
  end
87
86
 
88
- # @param component [Class]
87
+ # Gets or defines an anonymous module that
88
+ # will store all dynamically generated helper methods
89
+ # for the received module/class.
90
+ #
89
91
  # @param mod [Module, Class]
92
+ # @return [Module]
93
+ def helper_module(mod)
94
+ ivar_name = :@__amber_component_helper_module
95
+ mod.instance_variable_get(ivar_name)&.then { return _1 }
96
+
97
+ helper_mod = mod.instance_variable_set(ivar_name, ::Module.new)
98
+ mod.include helper_mod
99
+ helper_mod
100
+ end
101
+
102
+ # Defines an instance method on the given `mod` Module/Class.
103
+ #
104
+ # @param component [Class]
105
+ # @param target_mod [Module, Class]
90
106
  # @param method_name [String, Symbol]
91
107
  # @param body [Proc]
92
- def define_helper_method(component, mod, method_name, body)
93
- mod.define_method(method_name, &body)
108
+ def define_helper_method(component, target_mod, method_name, body)
109
+ helper_module(target_mod).define_method(method_name, &body)
94
110
 
95
111
  return if ::ENV['RAILS_ENV'] == 'production'
96
112
 
97
- ::Warning.warn <<~WARN if mod.instance_methods.include?(method_name)
113
+ ::Warning.warn <<~WARN if target_mod.instance_methods.include?(method_name)
98
114
  #{caller(0, 1).first}: warning:
99
- `#{component}` shadows the name of an already existing `#{mod}` method `#{method_name}`.
100
- Consider renaming this component, because the original method will be overwritten.
115
+ `#{component}` shadows the name of an already existing `#{target_mod}` method `#{method_name}`.
116
+ Consider renaming this component, because the original method will be overridden.
101
117
  WARN
102
118
  end
103
119
  end
@@ -105,9 +121,12 @@ module ::AmberComponent
105
121
  define_model_callbacks :initialize, :render
106
122
 
107
123
  # @param kwargs [Hash{Symbol => Object}]
124
+ # @raise [AmberComponent::MissingPropsError] when required props are missing
108
125
  def initialize(**kwargs)
109
126
  run_callbacks :initialize do
110
- bind_variables(kwargs)
127
+ next if bind_props(kwargs)
128
+
129
+ bind_instance_variables(kwargs)
111
130
  end
112
131
  end
113
132
 
@@ -115,7 +134,7 @@ module ::AmberComponent
115
134
 
116
135
  # @param kwargs [Hash{Symbol => Object}]
117
136
  # @return [void]
118
- def bind_variables(kwargs)
137
+ def bind_instance_variables(kwargs)
119
138
  kwargs.each do |key, value|
120
139
  instance_variable_set("@#{key}", value)
121
140
  end
@@ -7,6 +7,7 @@ module ::AmberComponent
7
7
  # Contains methods for quickly rendering
8
8
  # components defined under the root namespace `Object`.
9
9
  module ComponentHelper
10
+ @__amber_component_helper_module = self
10
11
  end
11
12
  end
12
13
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ::AmberComponent
4
+ # Internal class which represents a property
5
+ # on a component class.
6
+ class PropDefinition
7
+ # @return [Symbol]
8
+ attr_reader :name
9
+ # @return [Class, nil]
10
+ attr_reader :type
11
+ # @return [Boolean]
12
+ attr_reader :required
13
+ # @return [Object, Proc, nil]
14
+ attr_reader :default
15
+ # @return [Boolean]
16
+ attr_reader :allow_nil
17
+
18
+ # @param name [Symbol]
19
+ # @param type [Class, nil]
20
+ # @param required [Boolean]
21
+ # @param default [Object, Proc, nil]
22
+ # @param allow_nil [Boolean]
23
+ def initialize(name:, type: nil, required: false, default: nil, allow_nil: false)
24
+ @name = name
25
+ @type = type
26
+ @required = required
27
+ @default = default
28
+ @allow_nil = allow_nil
29
+ end
30
+
31
+ alias required? required
32
+ alias allow_nil? allow_nil
33
+
34
+ # @return [Boolean]
35
+ def type?
36
+ !@type.nil?
37
+ end
38
+
39
+ # @return [Boolean]
40
+ def default?
41
+ !@default.nil?
42
+ end
43
+
44
+ # Evaluate the default value if it's a `Proc`
45
+ # and return the result.
46
+ #
47
+ # @return [Object]
48
+ def default!
49
+ return @default.call if @default.is_a?(::Proc)
50
+
51
+ @default
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'prop_definition'
4
+
5
+ module ::AmberComponent
6
+ # Provides a DSL for defining component
7
+ # properties.
8
+ module Props
9
+ # Class methods for component properties.
10
+ module ClassMethods
11
+ # @return [Hash{Symbol => AmberComponent::Prop}]
12
+ attr_reader :prop_definitions
13
+
14
+ # @param names [Array<Symbol>]
15
+ # @param type [Class, nil]
16
+ # @param required [Boolean]
17
+ # @param default [Object, Proc, nil]
18
+ # @param allow_nil [Boolean]
19
+ def prop(*names, type: nil, required: false, default: nil, allow_nil: false)
20
+ @prop_definitions ||= {}
21
+ include(@prop_methods_module = ::Module.new) if @prop_methods_module.nil?
22
+
23
+ names.each do |name|
24
+ @prop_definitions[name] = prop_def = PropDefinition.new(
25
+ name: name,
26
+ type: type,
27
+ required: required,
28
+ default: default,
29
+ allow_nil: allow_nil
30
+ )
31
+ raise IncorrectPropTypeError, <<~MSG unless type.nil? || type.is_a?(::Class)
32
+ `type` should be a class but received `#{type.inspect}` (`#{type.class}`)
33
+ MSG
34
+
35
+ @prop_methods_module.attr_reader name
36
+ next @prop_methods_module.attr_writer(name) unless prop_def.type?
37
+
38
+ @prop_methods_module.class_eval( # rubocop:disable Style/DocumentDynamicEvalDefinition
39
+ # def phone=(val)
40
+ # raise IncorrectPropTypeError, <<~MSG unless val.nil? || val.is_a?(String)
41
+ # #{self.class} received `#{val.class}` instead of `String` for `phone` prop
42
+ # MSG
43
+ #
44
+ # @phone = val
45
+ # end
46
+ <<~RUB, __FILE__, __LINE__ + 1
47
+ def #{name}=(val)
48
+ raise IncorrectPropTypeError, <<~MSG unless #{allow_nil ? 'val.nil? ||' : nil} val.is_a?(#{prop_def.type})
49
+ \#{self.class} received `\#{val.class}` instead of `#{prop_def.type}` for `#{name}` prop
50
+ MSG
51
+
52
+ @#{name} = val
53
+ end
54
+ RUB
55
+ ) # rubocop:disable Layout/HeredocArgumentClosingParenthesis
56
+ end
57
+ end
58
+
59
+ # @return [Array<Symbol>, nil]
60
+ def prop_names
61
+ @prop_definitions.keys
62
+ end
63
+
64
+ # @return [Array<Symbol>, nil]
65
+ def required_prop_names
66
+ @prop_definitions&.filter_map do |name, prop_def|
67
+ next unless prop_def.required
68
+
69
+ name
70
+ end
71
+ end
72
+ end
73
+
74
+ # Instance methods for component properties.
75
+ module InstanceMethods
76
+ private
77
+
78
+ # @param props [Hash{Symbol => Object}]
79
+ def initialize(**kwargs)
80
+ bind_props(kwargs)
81
+ end
82
+
83
+ # @param props [Hash{Symbol => Object}]
84
+ # @return [Boolean] `false` when there are no props defined on the class
85
+ # and `true` otherwise
86
+ # @raise [AmberComponent::MissingPropsError] when required props are missing
87
+ # @raise [AmberComponent::IncorrectPropTypeError]
88
+ def bind_props(props)
89
+ return false if self.class.prop_definitions.nil?
90
+
91
+ self.class.prop_definitions.each do |name, prop_def|
92
+ setter_name = :"#{name}="
93
+ public_send(setter_name, prop_def.default!) if prop_def.default?
94
+
95
+ prop_present = props.include? name
96
+
97
+ raise MissingPropsError, <<~MSG if prop_def.required? && !prop_present
98
+ `#{self.class}` has a missing required prop: `#{name.inspect}`
99
+ MSG
100
+
101
+ next unless prop_present
102
+
103
+ value = props[name]
104
+ public_send(setter_name, value)
105
+ end
106
+
107
+ true
108
+ end
109
+ end
110
+ end
111
+ end
@@ -9,9 +9,9 @@ module ::AmberComponent
9
9
  def wrap(val)
10
10
  return val if val.is_a?(self)
11
11
 
12
- unless val.respond_to?(:[])
13
- raise InvalidType, "`TypedContent` should be a `Hash` or `#{self}` but was `#{val.class}` (#{val.inspect})"
14
- end
12
+ raise InvalidTypeError, <<~MSG unless val.respond_to?(:[])
13
+ `TypedContent` should be a `Hash` or `#{self}` but was `#{val.class}` (#{val.inspect})
14
+ MSG
15
15
 
16
16
  new(type: val[:type], content: val[:content])
17
17
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ::AmberComponent
4
- VERSION = '0.0.3'
4
+ VERSION = '0.0.4'
5
5
  end
@@ -55,7 +55,7 @@ module ::AmberComponent
55
55
  # @return [String, nil]
56
56
  def view_file_name
57
57
  files = asset_file_names(VIEW_FILE_REGEXP)
58
- raise MultipleViews, "More than one view file for `#{name}` found!" if files.length > 1
58
+ raise MultipleViewsError, "More than one view file for `#{name}` found!" if files.length > 1
59
59
 
60
60
  files.first
61
61
  end
@@ -81,7 +81,7 @@ module ::AmberComponent
81
81
  view_content = view_from_inline unless view_from_inline.empty?
82
82
 
83
83
  if view_content.nil? || view_content.empty?
84
- raise ViewFileNotFound, "View for `#{self.class}` could not be found!"
84
+ raise ViewFileNotFoundError, "View for `#{self.class}` could not be found!"
85
85
  end
86
86
 
87
87
  view_content
@@ -108,13 +108,13 @@ module ::AmberComponent
108
108
  content = content.to_s
109
109
 
110
110
  if content.empty?
111
- raise EmptyView, <<~ERR.squish
111
+ raise EmptyViewError, <<~ERR.squish
112
112
  Custom view for `#{self.class}` from view method cannot be empty!
113
113
  ERR
114
114
  end
115
115
 
116
116
  unless ALLOWED_VIEW_TYPES.include? type
117
- raise UnknownViewType, <<~ERR.squish
117
+ raise UnknownViewTypeError, <<~ERR.squish
118
118
  Unknown view type for `#{self.class}` from view method!
119
119
  Check return value of param type in `view :[type] do`
120
120
  ERR
@@ -5,16 +5,14 @@ require 'active_support/core_ext'
5
5
 
6
6
  module ::AmberComponent
7
7
  class Error < ::StandardError; end
8
- class ViewFileNotFound < Error; end
9
- class InvalidType < Error; end
8
+ class MissingPropsError < Error; end
9
+ class IncorrectPropTypeError < Error; end
10
+ class ViewFileNotFoundError < Error; end
11
+ class InvalidTypeError < Error; end
10
12
 
11
- class EmptyView < Error; end
12
- class UnknownViewType < Error; end
13
- class MultipleViews < Error; end
14
-
15
- class EmptyStyle < Error; end
16
- class UnknownStyleType < Error; end
17
- class MultipleStyles < Error; end
13
+ class EmptyViewError < Error; end
14
+ class UnknownViewTypeError < Error; end
15
+ class MultipleViewsError < Error; end
18
16
  end
19
17
 
20
18
  require_relative 'amber_component/version'
@@ -24,5 +22,6 @@ require_relative 'amber_component/template_handler'
24
22
  require_relative 'amber_component/views'
25
23
  require_relative 'amber_component/assets'
26
24
  require_relative 'amber_component/rendering'
25
+ require_relative 'amber_component/props'
27
26
  require_relative 'amber_component/base'
28
27
  require_relative 'amber_component/railtie' if defined?(::Rails::Railtie)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: amber_component
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ruby-Amber
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2022-09-12 00:00:00.000000000 Z
13
+ date: 2022-09-25 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: actionview
@@ -143,6 +143,8 @@ files:
143
143
  - lib/amber_component/helpers/class_helper.rb
144
144
  - lib/amber_component/helpers/component_helper.rb
145
145
  - lib/amber_component/helpers/css_helper.rb
146
+ - lib/amber_component/prop_definition.rb
147
+ - lib/amber_component/props.rb
146
148
  - lib/amber_component/railtie.rb
147
149
  - lib/amber_component/rendering.rb
148
150
  - lib/amber_component/template_handler.rb