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 +4 -4
- data/.yardopts +1 -0
- data/README.md +199 -2
- data/lib/mimi/core/core_ext.rb +89 -2
- data/lib/mimi/core/inheritable_property.rb +173 -0
- data/lib/mimi/core/manifest.rb +538 -0
- data/lib/mimi/core/module.rb +80 -16
- data/lib/mimi/core/rake.rb +46 -0
- data/lib/mimi/core/version.rb +1 -1
- data/lib/mimi/core.rb +32 -2
- data/mimi-core.gemspec +1 -2
- metadata +8 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4386bcac3a69eeb1f5a0422c445ba509e735ea4a
|
4
|
+
data.tar.gz: 6c583235d2e0069d255c0598b3e2cce4f4631bcb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
3
|
+
Core module of the [Mimi framework](Mimi).
|
4
4
|
|
5
|
-
|
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
|
|
data/lib/mimi/core/core_ext.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
|
2
|
-
|
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
|