amber_component 0.0.3 → 0.0.4

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