mimi-core 0.2.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6d247ba31bb61a81362ff2cb8d260d0a1b718bb9
4
- data.tar.gz: 332bd926feab1cdfd99e49e24c6a3b713c2ce254
3
+ metadata.gz: 4386bcac3a69eeb1f5a0422c445ba509e735ea4a
4
+ data.tar.gz: 6c583235d2e0069d255c0598b3e2cce4f4631bcb
5
5
  SHA512:
6
- metadata.gz: fcbb60ca621ead3705f57f73216d6952461cebbd592a5891a755ebe0d09be9f2fd0ab88ec437671f289e2e1bf8eade0f994888744df9c2aa78e2f5084a00252d
7
- data.tar.gz: 10df7f41b424a2fec0e460a2f242612ab7d23e508fbfe22428ca2f71cd4c5cb9fb480e81a05ac0807398266e90069b0a9ccd4bcb84d71ef7d36a823f63c53ec8
6
+ metadata.gz: d234685fd746173702527047e71aaa0c0d237da588e84b1de2f0fa23ecd74cca832569b91729af0e5968e53807d06d6eafb6077c73ac73d454cdfaa9ea041e92
7
+ data.tar.gz: db3ece3e5d363f942ebd3d98ff92b2c286fc198f74261378dcee0d1b67e38d4dd3f88fbbb443e3f2c648623dc85c2eefd679f6fc59a94f16f65bd8cf4e35533a
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --markup markdown
data/README.md CHANGED
@@ -1,8 +1,205 @@
1
1
  # mimi-core
2
2
 
3
- Core module for *mimi*, microframework for microservices.
3
+ Core module of the [Mimi framework](Mimi).
4
4
 
5
- [in development]
5
+ `mimi-core` defines the top-level Mimi namespace, provides a base for Mimi modules
6
+ and offers some core extentions.
7
+
8
+
9
+ ## Mimi namespace
10
+
11
+ At the top of hierarchy of the Mimi components there is a `Mimi` module that binds all components of the
12
+ framework together. It allows Mimi modules to be used (included in the stack and configured), started
13
+ and stopped.
14
+
15
+ ```ruby
16
+ require 'mimi/core'
17
+
18
+ Mimi # => Module
19
+ ```
20
+
21
+ ### Mimi.use(mod)
22
+
23
+ Configures the given module and appends it to the list of **used modules**.
24
+
25
+ ### Mimi.start
26
+
27
+ Starts the used modules in the order of appending.
28
+
29
+ ### Mimi.stop
30
+
31
+ Stops the used modules in reverse order.
32
+
33
+ ### Example
34
+
35
+ ```ruby
36
+ require 'mimi/db'
37
+
38
+ Mimi.use Mimi::DB, db_adapter: 'sqlite', db_database: 'tmp/dev.db'
39
+ Mimi.start
40
+
41
+ Mimi.app_root_path # => <Pathname> -- guesses current application's root
42
+ ```
43
+
44
+ ## Mimi::Core::Module
45
+
46
+ `Mimi::Core::Module` is a base for any Mimi module.
47
+
48
+ Mimi modules are pluggable components that you can include in your applications.
49
+ They provide some useful functionality, e.g. database abstraction layer, logging etc.
50
+
51
+ Any Mimi module provides a standard list of methods to interact with it:
52
+
53
+ ```
54
+ .configure(opts)
55
+ .start
56
+ .stop
57
+ ```
58
+
59
+ A Mimi module is typically packaged as a gem and it can declare a `module_root_path`.
60
+ Under this path the module may export some files, related to the module's offered functionality.
61
+ For example `Mimi::DB` exposes some rake tasks to perform database clearing, migrations etc.
62
+
63
+
64
+ ### Creating a new module
65
+
66
+ A Mimi component module can be created by including `Mimi::Core::Module` in a module or class
67
+ of yours:
68
+
69
+ ```ruby
70
+ # my_module.rb
71
+
72
+ require 'mimi-core'
73
+
74
+ module MyModule
75
+ include Mimi::Core::Module
76
+ end
77
+
78
+ Mimi.use MyModule, param: 1 # invokes MyModule.configure(param: 1)
79
+ Mimi.start # invokes MyModule.start
80
+ ```
81
+
82
+
83
+ ### Configuring a module
84
+
85
+ A Mimi module implements a `.manifest`, `.configure` and `.options` class methods.
86
+
87
+ The `.configure` class method accepts a Hash of configurable parameter values and sets the options Hash
88
+ available via `.options` class method. Which parameters can be set and whether there are any default
89
+ values for the parameters is defined by the module *manifest*.
90
+
91
+ ```ruby
92
+ Mimi.configure(param: 1)
93
+ Mimi.options[:param] # => 1
94
+ ```
95
+
96
+ ### Module manifest
97
+
98
+ The original `.configure` implementation in `Mimi::Core::Module` takes into consideration the
99
+ module *manifest*.
100
+
101
+ Module manifest is a set of formal definitions of configurable parameters.
102
+
103
+ The manifest declares which configurable parameters are accepted by the module,
104
+ which types of values are accepted, which are default values and so on.
105
+
106
+ When designing your own module, you should redefine `.manifest` method to contain
107
+ your module's actual manifest. The `.manifest` class method should return a Hash representation of the module manifest,
108
+ where keys define the configurable parameter names, and values are Hash-es with the configurable parameter
109
+ properties:
110
+
111
+
112
+ ```ruby
113
+ # my_module.rb
114
+
115
+ require 'mimi-core'
116
+
117
+ module MyModule
118
+ include Mimi::Core::Module
119
+
120
+ def self.manifest
121
+ {
122
+ param1: {} # minimal definition of a configurable parameter
123
+ }
124
+ end
125
+ end
126
+
127
+ Mimi.configure(param: 1)
128
+ Mimi.options[:param] # => 1
129
+ ```
130
+
131
+ Recognised configurable parameter properties are:
132
+
133
+ * `:desc` -- (default: `nil`) human readable description
134
+ * `:type` -- (default: `:string`) accepted value type
135
+ * `:default` -- (default: `nil`) default value for configurable parameter
136
+ * `:const` -- (default: `false`) makes the configurable parameter constant
137
+ * `:hidden` -- (default: `false`) omits the configurable parameter from a combined manifest
138
+
139
+
140
+ Example:
141
+
142
+ ```ruby
143
+ def self.manifest
144
+ {
145
+ param1: {
146
+ desc: 'My configurable param1',
147
+ type: :integer,
148
+ default: 1
149
+ },
150
+ }
151
+ end
152
+ ```
153
+
154
+ @see Mimi::Core::Manifest
155
+
156
+ ## Useful extensions
157
+
158
+ `mimi-core` gem contains some useful extensions which are included automatically, once the gem
159
+ is *require*-d:
160
+
161
+ * Mimi::Core::InheritableProperty
162
+ * Array and Hash extensions
163
+
164
+ ### Mimi::Core::InheritableProperty
165
+
166
+ Makes `.inheritable_property` method available to the class.
167
+
168
+ Example:
169
+
170
+ ```ruby
171
+ class A
172
+ include Mimi::Core::InheritableProperty
173
+
174
+ inheritable_property :my_var
175
+ end
176
+ ```
177
+ @see Mimi::Core::InheritableProperty::ClassMethods#inheritable_property
178
+
179
+
180
+ ### Array and Hash extensions
181
+
182
+ `Array` extensions:
183
+
184
+ ```
185
+ Array#only(*values) # select array elements only if listed in 'values'
186
+ Array#only!(*values) # as above, modify array in-place
187
+ Array#except(*values) # reject array elements listed in 'values'
188
+ Array#except!(*values) # as above, modify array in-place
189
+ ```
190
+
191
+ `Hash` extensions:
192
+
193
+ ```
194
+ Hash#only(*keys) # keep only those key-value pairs of the Hash which keys are listed in 'keys'
195
+ Hash#only!(*keys) # as above, modify Hash in-place
196
+ Hash#except(*keys) # reject key-value pairs of the Hash which keys are listed in 'keys'
197
+ Hash#except!(*keys) # as above, modify Hash in-place
198
+ Hash#deep_merge(other) # deep-merges another Hash
199
+ Hash#deep_dup # deep-duplicates a Hash, taking care of nested Hashes and Arrays
200
+ Hash#deep_symbolize_keys
201
+ Hash#deep_stringify_keys
202
+ ```
6
203
 
7
204
  ## License
8
205
 
@@ -1,5 +1,5 @@
1
- require 'active_support'
2
- require 'active_support/core_ext'
1
+ # frozen_string_literal: true
2
+
3
3
  require 'hashie'
4
4
 
5
5
  class Hash
@@ -9,6 +9,10 @@ class Hash
9
9
  end
10
10
 
11
11
  def only!(*keys)
12
+ if keys.size == 1 && keys.first.is_a?(Array)
13
+ raise ArgumentError, 'Hash#only!() expects keys as list of arguments,' \
14
+ ' not an Array as first argument'
15
+ end
12
16
  select! { |k, _| keys.include?(k) }
13
17
  self
14
18
  end
@@ -20,6 +24,10 @@ class Hash
20
24
  end
21
25
 
22
26
  def except!(*keys)
27
+ if keys.size == 1 && keys.first.is_a?(Array)
28
+ raise ArgumentError, 'Hash#except!() expects keys as list of arguments,' \
29
+ ' not an Array as first argument'
30
+ end
23
31
  reject! { |k, _| keys.include?(k) }
24
32
  self
25
33
  end
@@ -28,6 +36,69 @@ class Hash
28
36
  unless instance_methods(false).include?(:deep_merge)
29
37
  include Hashie::Extensions::DeepMerge
30
38
  end
39
+
40
+ unless instance_methods(false).include?(:deep_dup)
41
+ def deep_dup
42
+ map do |k, v|
43
+ v = v.respond_to?(:deep_dup) ? v.deep_dup : v.dup
44
+ [k, v]
45
+ end.to_h
46
+ end
47
+ end
48
+
49
+ unless instance_methods(false).include?(:symbolize_keys)
50
+ def symbolize_keys
51
+ map do |k, v|
52
+ k = k.respond_to?(:to_sym) ? k.to_sym : k
53
+ [k, v]
54
+ end.to_h
55
+ end
56
+
57
+ def symbolize_keys!
58
+ replace(symbolize_keys)
59
+ end
60
+ end
61
+
62
+ unless instance_methods(false).include?(:deep_symbolize_keys)
63
+ def deep_symbolize_keys
64
+ map do |k, v|
65
+ k = k.respond_to?(:to_sym) ? k.to_sym : k
66
+ v = v.respond_to?(:deep_symbolize_keys) ? v.deep_symbolize_keys : v
67
+ [k, v]
68
+ end.to_h
69
+ end
70
+
71
+ def deep_symbolize_keys!
72
+ replace(deep_symbolize_keys)
73
+ end
74
+ end
75
+
76
+ unless instance_methods(false).include?(:stringify_keys)
77
+ def stringify_keys
78
+ map do |k, v|
79
+ k = k.respond_to?(:to_s) ? k.to_s : k
80
+ [k, v]
81
+ end.to_h
82
+ end
83
+
84
+ def stringify_keys!
85
+ replace(stringify_keys)
86
+ end
87
+ end
88
+
89
+ unless instance_methods(false).include?(:deep_stringify_keys)
90
+ def deep_stringify_keys
91
+ map do |k, v|
92
+ k = k.respond_to?(:to_s) ? k.to_s : k
93
+ v = v.respond_to?(:deep_stringify_keys) ? v.deep_stringify_keys : v
94
+ [k, v]
95
+ end.to_h
96
+ end
97
+
98
+ def deep_stringify_keys!
99
+ replace(deep_stringify_keys)
100
+ end
101
+ end
31
102
  end
32
103
 
33
104
  class Array
@@ -37,6 +108,10 @@ class Array
37
108
  end
38
109
 
39
110
  def only!(*keys)
111
+ if keys.size == 1 && keys.first.is_a?(Array)
112
+ raise ArgumentError, 'Array#only!() expects keys as list of arguments,' \
113
+ ' not an Array as first argument'
114
+ end
40
115
  select! { |k| keys.include?(k) }
41
116
  self
42
117
  end
@@ -48,8 +123,20 @@ class Array
48
123
  end
49
124
 
50
125
  def except!(*keys)
126
+ if keys.size == 1 && keys.first.is_a?(Array)
127
+ raise ArgumentError, 'Array#except!() expects keys as list of arguments,' \
128
+ ' not an Array as first argument'
129
+ end
51
130
  reject! { |k| keys.include?(k) }
52
131
  self
53
132
  end
54
133
  end
134
+
135
+ unless instance_methods(false).include?(:deep_dup)
136
+ def deep_dup
137
+ map do |v|
138
+ v.respond_to?(:deep_dup) ? v.deep_dup : v.dup
139
+ end
140
+ end
141
+ end
55
142
  end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mimi
4
+ module Core
5
+ #
6
+ # Makes `.inheritable_property` method available to the class.
7
+ #
8
+ # Example:
9
+ #
10
+ # ```
11
+ # class A
12
+ # include Mimi::Core::InheritableProperty
13
+ #
14
+ # inheritable_property :my_var
15
+ # end
16
+ # ```
17
+ # @see Mimi::Core::InheritableProperty::ClassMethods#inheritable_property
18
+ #
19
+ module InheritableProperty
20
+ def self.included(base)
21
+ base.send :extend, ClassMethods
22
+ end
23
+
24
+ module ClassMethods
25
+ #
26
+ # Declares an inheritable property.
27
+ #
28
+ # The inheritable property is a special class variable, that is accessible
29
+ # in descendant classes, but each of the descendant classes can alter only its local value.
30
+ #
31
+ # Once a property is declared, a class method with the name of the property
32
+ # becomes available.
33
+ #
34
+ # ```
35
+ # class A
36
+ # include Mimi::Core::InheritableProperty
37
+ #
38
+ # inheritable_property :var1, default: 1
39
+ # end
40
+ #
41
+ # class B < A
42
+ # end
43
+ #
44
+ # A.var1 # => 1
45
+ # B.var1 # => 1
46
+ # ```
47
+ #
48
+ # A class method with the name of the property accepts one optional argument -- a new value
49
+ # for the property. If the argument is omitted, current inherited value is returned.
50
+ #
51
+ # If the argument is present, it sets a new property value for this class
52
+ # and its subclasses, but not for the parent class.
53
+ #
54
+ # ```
55
+ # class A
56
+ # include Mimi::Core::InheritableProperty
57
+ #
58
+ # inheritable_property :var1, default: 1
59
+ # end
60
+ #
61
+ # class B < A
62
+ # var1 123 # sets new value for B.var1
63
+ # end
64
+ #
65
+ # class C < B
66
+ # end
67
+ #
68
+ # A.var1 # => 1, the default value, unchanged
69
+ # B.var1 # => 123
70
+ # C.var1 # => 123
71
+ # ```
72
+ #
73
+ # Working with Hash values
74
+ # ===
75
+ # An `inherited_property` can be declared having the type `:hash`, then the inherited
76
+ # values are deep-merged in subclasses.
77
+ #
78
+ # ```
79
+ # class A
80
+ # include Mimi::Core::InheritableProperty
81
+ #
82
+ # inheritable_property :var1, default: { a: 1 }
83
+ # end
84
+ #
85
+ # class B < A
86
+ # var1 b: 2
87
+ # end
88
+ #
89
+ # class C < B
90
+ # end
91
+ #
92
+ # A.var1 # => { a: 1 }
93
+ # B.var1 # => { a: 1, b: 2 }
94
+ # C.var1 # => { a: 1, b: 2 }
95
+ # ```
96
+ #
97
+ # Proc as default value
98
+ # ===
99
+ # A Proc can be specified instead of literal default value, in which case it will
100
+ # be evaluated when the inherited property value is queried. The passed block will
101
+ # be evaluated in the context of the current class.
102
+ #
103
+ # ```
104
+ # class A
105
+ # include Mimi::Core::InheritableProperty
106
+ #
107
+ # inheritable_property :var1, default: -> { self.name }
108
+ # end
109
+ #
110
+ # class B < A
111
+ # end
112
+ #
113
+ # class C < B
114
+ # end
115
+ #
116
+ # A.var1 # => "A"
117
+ # B.var1 # => "B"
118
+ # ```
119
+ #
120
+ # @param name [Symbol] a name for the new inheritable property
121
+ # @param opts [Hash,nil] optional parameters for the inheritable property
122
+ # @option opts [Object,Proc] :default specifies the literal or Proc as the default value
123
+ # for the property
124
+ # @option opts [:hash,nil] :type instructs that the property is a Hash or a simple value
125
+ # (inherited values are deep-merged for Hash'es)
126
+ #
127
+ def inheritable_property(name, opts = {})
128
+ unless name.is_a?(Symbol)
129
+ raise ArgumentError, 'Symbol is expected as inheritable_property name'
130
+ end
131
+ var_name = :"@#{name}"
132
+ ip_get_name = :"inheritable_property_get_#{name}"
133
+ ip_set_name = :"inheritable_property_set_#{name}"
134
+ opts_default = opts[:default] && opts[:default].dup
135
+
136
+ define_singleton_method(ip_get_name) do |caller|
137
+ if opts[:type] == :hash
138
+ # combine this class' value with a superclass' value
139
+ self_value = instance_variable_get(var_name) || {}
140
+ super_value =
141
+ (superclass.respond_to?(ip_get_name) && superclass.send(ip_get_name, caller)) ||
142
+ (opts_default.is_a?(Proc) ? caller.instance_exec(&opts_default) : opts_default)
143
+ super_value.deep_merge(self_value)
144
+ else
145
+ instance_variable_get(var_name) ||
146
+ (superclass.respond_to?(ip_get_name) && superclass.send(ip_get_name, caller)) ||
147
+ (opts_default.is_a?(Proc) ? caller.instance_exec(&opts_default) : opts_default)
148
+ end
149
+ end
150
+
151
+ define_singleton_method(ip_set_name) do |arg|
152
+ if opts[:type] == :hash && !arg.is_a?(Hash) && !arg.nil?
153
+ raise ArgumentError, "Hash or nil is expected as a value for property #{self}.#{name}"
154
+ end
155
+ instance_variable_set(var_name, arg)
156
+ end
157
+
158
+ define_singleton_method(name) do |*args|
159
+ if args.empty?
160
+ send(:"inheritable_property_get_#{name}", self)
161
+ else
162
+ send(:"inheritable_property_set_#{name}", args.first)
163
+ end
164
+ end
165
+
166
+ define_singleton_method("#{name}!".to_sym) do
167
+ send(name) || (raise "Property #{self}.#{name} is not set")
168
+ end
169
+ end
170
+ end # module ClassMethods
171
+ end # module InheritableProperty
172
+ end # module Core
173
+ end # module Mimi