libis-tools 1.0.5-java
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 +7 -0
- data/.coveralls.yml +2 -0
- data/.gitignore +16 -0
- data/.rspec +2 -0
- data/.travis.yml +40 -0
- data/Gemfile +7 -0
- data/README.md +202 -0
- data/Rakefile +11 -0
- data/bin/libis_tool +5 -0
- data/lib/libis-tools.rb +1 -0
- data/lib/libis/tools.rb +25 -0
- data/lib/libis/tools/assert.rb +52 -0
- data/lib/libis/tools/checksum.rb +106 -0
- data/lib/libis/tools/cli/cli_helper.rb +189 -0
- data/lib/libis/tools/cli/reorg.rb +416 -0
- data/lib/libis/tools/command.rb +133 -0
- data/lib/libis/tools/command_line.rb +23 -0
- data/lib/libis/tools/config.rb +147 -0
- data/lib/libis/tools/config_file.rb +85 -0
- data/lib/libis/tools/csv.rb +38 -0
- data/lib/libis/tools/deep_struct.rb +71 -0
- data/lib/libis/tools/extend/array.rb +16 -0
- data/lib/libis/tools/extend/empty.rb +7 -0
- data/lib/libis/tools/extend/hash.rb +147 -0
- data/lib/libis/tools/extend/kernel.rb +25 -0
- data/lib/libis/tools/extend/ostruct.rb +3 -0
- data/lib/libis/tools/extend/roo.rb +91 -0
- data/lib/libis/tools/extend/string.rb +94 -0
- data/lib/libis/tools/extend/struct.rb +29 -0
- data/lib/libis/tools/extend/symbol.rb +8 -0
- data/lib/libis/tools/logger.rb +130 -0
- data/lib/libis/tools/mets_dnx.rb +61 -0
- data/lib/libis/tools/mets_file.rb +504 -0
- data/lib/libis/tools/mets_objects.rb +547 -0
- data/lib/libis/tools/parameter.rb +372 -0
- data/lib/libis/tools/spreadsheet.rb +196 -0
- data/lib/libis/tools/temp_file.rb +42 -0
- data/lib/libis/tools/thread_safe.rb +31 -0
- data/lib/libis/tools/version.rb +5 -0
- data/lib/libis/tools/xml_document.rb +583 -0
- data/libis-tools.gemspec +55 -0
- data/spec/assert_spec.rb +65 -0
- data/spec/checksum_spec.rb +68 -0
- data/spec/command_spec.rb +90 -0
- data/spec/config_file_spec.rb +83 -0
- data/spec/config_spec.rb +113 -0
- data/spec/csv_spec.rb +159 -0
- data/spec/data/test-headers.csv +2 -0
- data/spec/data/test-headers.tsv +2 -0
- data/spec/data/test-noheaders.csv +1 -0
- data/spec/data/test-noheaders.tsv +1 -0
- data/spec/data/test.data +9 -0
- data/spec/data/test.xlsx +0 -0
- data/spec/data/test.xml +8 -0
- data/spec/data/test.yml +2 -0
- data/spec/data/test_config.yml +15 -0
- data/spec/deep_struct_spec.rb +138 -0
- data/spec/logger_spec.rb +165 -0
- data/spec/mets_file_spec.rb +223 -0
- data/spec/parameter_container_spec.rb +152 -0
- data/spec/parameter_spec.rb +148 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/spreadsheet_spec.rb +1820 -0
- data/spec/temp_file_spec.rb +76 -0
- data/spec/test.xsd +20 -0
- data/spec/thread_safe_spec.rb +64 -0
- data/spec/xmldocument_spec.rb +421 -0
- data/test/test_helper.rb +7 -0
- data/test/webservices/test_ca_item_info.rb +59 -0
- data/test/webservices/test_ca_search.rb +35 -0
- metadata +437 -0
@@ -0,0 +1,133 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
module Libis
|
5
|
+
module Tools
|
6
|
+
|
7
|
+
# This module allows to run an external command safely and returns it's output, error messages and status.
|
8
|
+
# The run method takes any number of arguments that will be used as command-line arguments. The method returns
|
9
|
+
# a Hash with:
|
10
|
+
# * :out => an array with lines that were printed on the external program's standard out.
|
11
|
+
# * :err => an array with lines that were printed on the external program's standard error.
|
12
|
+
# * :status => exit code returned by the external program.
|
13
|
+
# * :timeout => true if the command was terminated due to a timeout.
|
14
|
+
# * :pid => pid of the command (in case <pid>.log files need to be cleaned up)
|
15
|
+
#
|
16
|
+
# Optionally an option hash can be appended to the list of arguments with:
|
17
|
+
# * :stdin_data => values sent to the command's standard input (optional, nothing sent if not present)
|
18
|
+
# * :binmode => if present and true, will set the IO communication to binary data
|
19
|
+
# * :timeout => if specified, SIGTERM signal is sent to the command after the number of seconds
|
20
|
+
# * :signal => Signal sent to the command instead of the default SIGTERM
|
21
|
+
# * :kill_after => if specified, SIGKILL signal is sent aftern the number of seconds if command is still running
|
22
|
+
# after initial signal was sent
|
23
|
+
# * any other options will be handed over to the spawn command (e.g. pgroup)
|
24
|
+
#
|
25
|
+
# Examples:
|
26
|
+
#
|
27
|
+
# require 'libis/tools/command'
|
28
|
+
# result = ::Libis::Tools::Command.run('ls', '-l', File.absolute_path(__FILE__))
|
29
|
+
# p result # => {out: [...], err: [...], status: 0}
|
30
|
+
#
|
31
|
+
# require 'libis/tools/command'
|
32
|
+
# include ::Libis::Tools::Command
|
33
|
+
# result = run('ls', '-l', File.absolute_path(__FILE__))
|
34
|
+
# p result # => {out: [...], err: [...], status: 0}
|
35
|
+
#
|
36
|
+
# Note that the Command class uses Open3#popen3 internally. All arguments supplied to Command#run are passed to
|
37
|
+
# the popen3 call. Unfortunately some older JRuby versions have some known issues with popen3. Please use and
|
38
|
+
# test carefully in JRuby environments.
|
39
|
+
module Command
|
40
|
+
|
41
|
+
# Run an external program and return status, stdout and stderr.
|
42
|
+
#
|
43
|
+
#
|
44
|
+
# @param [Array<String>] cmd command name optionally prepended with env and appended with command-line arguments
|
45
|
+
# @return [Hash] a Hash with:
|
46
|
+
# * :status (Integer) - the exit status of the command
|
47
|
+
# * :out (Array<String>) - the stdout output of the command
|
48
|
+
# * :err (Array<String>)- the stderr output of the command
|
49
|
+
# * :timeout(Boolean) - if true, the command did not return in time
|
50
|
+
# * :pid(Integer) - the command's processID
|
51
|
+
def self.run(*cmd)
|
52
|
+
|
53
|
+
spawn_opts = Hash === cmd.last ? cmd.pop.dup : {}
|
54
|
+
opts = {
|
55
|
+
:stdin_data => spawn_opts.delete(:stdin_data) || '',
|
56
|
+
:binmode => spawn_opts.delete(:binmode) || false,
|
57
|
+
:timeout => spawn_opts.delete(:timeout),
|
58
|
+
:signal => spawn_opts.delete(:signal) || :TERM,
|
59
|
+
:kill_after => spawn_opts.delete(:kill_after),
|
60
|
+
}
|
61
|
+
in_r, in_w = IO.pipe
|
62
|
+
out_r, out_w = IO.pipe
|
63
|
+
err_r, err_w = IO.pipe
|
64
|
+
in_w.sync = true
|
65
|
+
|
66
|
+
if opts[:binmode]
|
67
|
+
in_w.binmode
|
68
|
+
out_r.binmode
|
69
|
+
err_r.binmode
|
70
|
+
end
|
71
|
+
|
72
|
+
spawn_opts[:in] = in_r
|
73
|
+
spawn_opts[:out] = out_w
|
74
|
+
spawn_opts[:err] = err_w
|
75
|
+
|
76
|
+
result = {
|
77
|
+
:pid => nil,
|
78
|
+
:status => nil,
|
79
|
+
:out => [],
|
80
|
+
:err => [],
|
81
|
+
:timeout => false,
|
82
|
+
}
|
83
|
+
|
84
|
+
out_reader = nil
|
85
|
+
err_reader = nil
|
86
|
+
wait_thr = nil
|
87
|
+
|
88
|
+
begin
|
89
|
+
Timeout.timeout(opts[:timeout]) do
|
90
|
+
result[:pid] = spawn(*cmd, spawn_opts)
|
91
|
+
wait_thr = Process.detach(result[:pid])
|
92
|
+
in_r.close
|
93
|
+
out_w.close
|
94
|
+
err_w.close
|
95
|
+
|
96
|
+
out_reader = Thread.new {out_r.read}
|
97
|
+
err_reader = Thread.new {err_r.read}
|
98
|
+
|
99
|
+
in_w.write opts[:stdin_data]
|
100
|
+
in_w.close
|
101
|
+
|
102
|
+
result[:status] = wait_thr.value
|
103
|
+
end
|
104
|
+
|
105
|
+
rescue Timeout::Error
|
106
|
+
result[:timeout] = true
|
107
|
+
pid = spawn_opts[:pgroup] ? -result[:pid] : result[:pid]
|
108
|
+
Process.kill(opts[:signal], pid)
|
109
|
+
if opts[:kill_after]
|
110
|
+
unless wait_thr.join(opts[:kill_after])
|
111
|
+
Process.kill(:KILL, pid)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
rescue StandardError => e
|
116
|
+
result[:err] = [e.class.name, e.message]
|
117
|
+
|
118
|
+
ensure
|
119
|
+
result[:status] = wait_thr.value.exitstatus if wait_thr
|
120
|
+
result[:out] += out_reader.value.split("\n").map(&:chomp) if out_reader
|
121
|
+
result[:err] += err_reader.value.split("\n").map(&:chomp) if err_reader
|
122
|
+
out_r.close unless out_r.closed?
|
123
|
+
err_r.close unless err_r.closed?
|
124
|
+
end
|
125
|
+
|
126
|
+
result
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'tty-prompt'
|
3
|
+
require 'tty-config'
|
4
|
+
|
5
|
+
require 'libis/tools/cli/cli_helper'
|
6
|
+
require 'libis/tools/cli/reorg'
|
7
|
+
|
8
|
+
module Libis
|
9
|
+
module Tools
|
10
|
+
|
11
|
+
class CommandLine < Thor
|
12
|
+
|
13
|
+
include Cli::Helper
|
14
|
+
include Cli::Reorg
|
15
|
+
|
16
|
+
def reorg
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'singleton'
|
3
|
+
require 'yaml'
|
4
|
+
require 'erb'
|
5
|
+
require 'logging'
|
6
|
+
|
7
|
+
require_relative 'config_file'
|
8
|
+
|
9
|
+
module Libis
|
10
|
+
module Tools
|
11
|
+
|
12
|
+
# The Singleton Config class is a convenience class for easy configuration maintenance, loading and saving.
|
13
|
+
# It also initializes a default logger and supports creating extra loggers. The logging infrastructure is based on
|
14
|
+
# the {http://www.rubydoc.info/gems/logging/Logging ::Logging} gem and supports the {::Libis::Tools::Logger} class.
|
15
|
+
#
|
16
|
+
# For the configuration parameters, it supports code defaults, loading configurations from multiple YAML files
|
17
|
+
# containing ERB statements. The Config class behaves like a Hash/OpenStruct/HashWithIndifferentAccess.
|
18
|
+
#
|
19
|
+
# The parameters can be accessed by getter/setter method or using the Hash syntax:
|
20
|
+
#
|
21
|
+
# require 'libis/tools/config'
|
22
|
+
# cfg = ::Libis::Tools::Config
|
23
|
+
# cfg['my_value'] = 10
|
24
|
+
# p cfg.instance.my_value # => 10
|
25
|
+
# cfg.instance.my_text = 'abc'
|
26
|
+
# p cfg[:my_text] # => 'abc'
|
27
|
+
# p cfg.logger.warn('message') # => W, [2015-03-16T12:51:01.180548 #123.456] WARN : message
|
28
|
+
#
|
29
|
+
class Config
|
30
|
+
include Singleton
|
31
|
+
|
32
|
+
class << self
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# For each configuration parameter, the value can be accessed via the class or the Singleton instance.
|
37
|
+
# The class diverts to the instance automatically.
|
38
|
+
def method_missing(name, *args, &block)
|
39
|
+
result = instance.send(name, *args, &block)
|
40
|
+
self === result ? self : result
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
# Instance method that allows to access the configuration parameters by method.
|
46
|
+
def method_missing(name, *args, &block)
|
47
|
+
result = config.send(name, *args, &block)
|
48
|
+
self === config ? self : result
|
49
|
+
end
|
50
|
+
|
51
|
+
# Load configuration parameters from a YAML file or Hash.
|
52
|
+
#
|
53
|
+
# The file paths and Hashes are memorised and loaded again by the {#reload} methods.
|
54
|
+
# @param [String,Hash] file_or_hash
|
55
|
+
def <<(file_or_hash)
|
56
|
+
sync do
|
57
|
+
@config.send('<<', (file_or_hash)) { |data| @sources << data }
|
58
|
+
self
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Load all files and Hashes again.
|
63
|
+
#
|
64
|
+
# Will not reset the configuration parameters. Parameters set directly on the
|
65
|
+
# configuration are kept intact unless they also exist in the files or hashes in which case they will be overwritten.
|
66
|
+
def reload
|
67
|
+
sync do
|
68
|
+
sources = @sources.dup
|
69
|
+
@sources.clear
|
70
|
+
sources.each { |f| self << f }
|
71
|
+
self
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Clear data and load all files and Hashes again.
|
76
|
+
#
|
77
|
+
# All configuration parameters are first deleted which means that any parameters
|
78
|
+
# added directly (not via file or hash) will no longer be available. Parameters set explicitly that also exist in
|
79
|
+
# the files or hashes will be reset to the values in those files and hashes.
|
80
|
+
def reload!
|
81
|
+
sync do
|
82
|
+
@config.clear!
|
83
|
+
reload
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Clear all data.
|
88
|
+
#
|
89
|
+
# Not only all configuration parameters are deleted, but also the memorized list of loaded files
|
90
|
+
# and hashes are cleared and the logger configuration is reset to it's default status.
|
91
|
+
def clear!
|
92
|
+
sync do
|
93
|
+
@config.clear!
|
94
|
+
@sources = Array.new
|
95
|
+
self.logger
|
96
|
+
self
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Gets the default ::Logging formatter.
|
101
|
+
#
|
102
|
+
# This in an instance of a layout that prints in the default message format.
|
103
|
+
#
|
104
|
+
# The default layout prints log lines like this:
|
105
|
+
#
|
106
|
+
# <first char of severity>, [<timestamp> #<process-id>.<thread-id] <severity> : <message>
|
107
|
+
#
|
108
|
+
def get_log_formatter
|
109
|
+
# noinspection RubyResolve
|
110
|
+
::Logging::Layouts::Pattern.new(DEFAULT_LOG_LAYOUT_PARAMETERS)
|
111
|
+
end
|
112
|
+
|
113
|
+
def logger(name = nil, appenders = nil)
|
114
|
+
sync do
|
115
|
+
name ||= 'root'
|
116
|
+
logger = ::Logging.logger[name]
|
117
|
+
if logger.appenders.empty?
|
118
|
+
logger.appenders = appenders || ::Logging.appenders.stdout(layout: get_log_formatter)
|
119
|
+
end
|
120
|
+
logger
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
attr_accessor :config, :sources
|
125
|
+
|
126
|
+
protected
|
127
|
+
|
128
|
+
def initialize(hash = nil, opts = {})
|
129
|
+
@mutex = ReentrantMutex.new
|
130
|
+
@config = ConfigFile.new(hash, opts)
|
131
|
+
self.clear!
|
132
|
+
end
|
133
|
+
|
134
|
+
def sync(&block)
|
135
|
+
@mutex.synchronize(&block)
|
136
|
+
end
|
137
|
+
|
138
|
+
::Logging::init
|
139
|
+
# noinspection RubyResolve
|
140
|
+
DEFAULT_LOG_LAYOUT_PARAMETERS = {
|
141
|
+
pattern: "%.1l, [%d #%p.%t] %5l%X{Application}%X{Subject} : %m\n",
|
142
|
+
date_pattern: '%Y-%m-%dT%H:%M:%S.%L'
|
143
|
+
}
|
144
|
+
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'singleton'
|
3
|
+
require 'yaml'
|
4
|
+
require 'erb'
|
5
|
+
|
6
|
+
require 'libis/tools/deep_struct'
|
7
|
+
|
8
|
+
module Libis
|
9
|
+
module Tools
|
10
|
+
|
11
|
+
# The ConfigFile class is a convenience class for interfacing with YAML configuration files. These files can
|
12
|
+
# contain ERB statements. An initial hash or file can be loaded during initialization. The class supports loading
|
13
|
+
# and saving of files, but note that any ERB statements in the file are lost by performing such a round trip.
|
14
|
+
# The class is derived from the DeepStruct class and therefore supports nested hashes and arrays and supports
|
15
|
+
# the OpenStruct style of accessors.
|
16
|
+
#
|
17
|
+
# The parameters can be accessed by getter/setter method or using the Hash syntax:
|
18
|
+
#
|
19
|
+
# require 'libis/tools/config_file'
|
20
|
+
# cfg_file = ::Libis::Tools::ConfigFile.new
|
21
|
+
# cfg_file << {foo: 'bar'}
|
22
|
+
# cfg_file.my_value = 10
|
23
|
+
# p cfg_file[:my_value] # => 10
|
24
|
+
# cfg_file{:my_text] = 'abc'
|
25
|
+
# p cfg_file['my_text'] # => 'abc'
|
26
|
+
# p cfg_file.to_hash # => { :foo => 'bar', 'my_value' => 10, :my_text => 'abc' }
|
27
|
+
# cfg >> 'my_config.yml'
|
28
|
+
#
|
29
|
+
class ConfigFile < DeepStruct
|
30
|
+
|
31
|
+
# Create a new ConfigFile instance. The optional argument can either be a Hash or a String. The argument is
|
32
|
+
# passed to the {#<<} method after initialization.
|
33
|
+
#
|
34
|
+
# @param [String,Hash] file_or_hash optional String or Hash argument to initialize the data.
|
35
|
+
def initialize(file_or_hash = nil, opt = {})
|
36
|
+
super _file_to_hash(file_or_hash), opt
|
37
|
+
end
|
38
|
+
|
39
|
+
# Load configuration parameters from a YAML file or Hash.
|
40
|
+
#
|
41
|
+
# The YAML file can contain ERB syntax values that will be evaluated at loading time. Instead of a YAML file,
|
42
|
+
# a Hash can be passed.
|
43
|
+
#
|
44
|
+
# Note that the method also yields the hash or absolute path to a given block. This is for data management of
|
45
|
+
# derived classes such as ::Libis::Tools::Config.
|
46
|
+
#
|
47
|
+
# @param [String,Hash] file_or_hash optional String or Hash argument to initialize the data.
|
48
|
+
def <<(file_or_hash, &block)
|
49
|
+
self.merge!(_file_to_hash(file_or_hash, &block))
|
50
|
+
end
|
51
|
+
|
52
|
+
# Save configuration parameters in a YAML file.
|
53
|
+
#
|
54
|
+
# @param [String] file path of the YAML file to save the configuration to.
|
55
|
+
def >>(file)
|
56
|
+
File.open(file, 'w') { |f| f.write to_hash.to_yaml }
|
57
|
+
end
|
58
|
+
|
59
|
+
protected
|
60
|
+
|
61
|
+
def _file_to_hash(file_or_hash)
|
62
|
+
return {} if file_or_hash.nil? || (file_or_hash.respond_to?(:empty?) && file_or_hash.empty?)
|
63
|
+
hash = case file_or_hash
|
64
|
+
when Hash
|
65
|
+
yield file_or_hash if block_given?
|
66
|
+
file_or_hash
|
67
|
+
when String
|
68
|
+
return {} unless File.exist?(file_or_hash)
|
69
|
+
yield File.absolute_path(file_or_hash) if block_given?
|
70
|
+
# noinspection RubyResolve
|
71
|
+
begin
|
72
|
+
YAML.load(ERB.new(open(file_or_hash).read).result)
|
73
|
+
rescue Exception => e
|
74
|
+
raise RuntimeError, "Error loading YAML '#{file_or_hash}': #{e.message}"
|
75
|
+
end
|
76
|
+
else
|
77
|
+
{}
|
78
|
+
end
|
79
|
+
hash = {} unless hash.is_a? Hash
|
80
|
+
hash
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'csv'
|
2
|
+
|
3
|
+
module Libis
|
4
|
+
module Tools
|
5
|
+
module Csv
|
6
|
+
|
7
|
+
# @param [String] file_name
|
8
|
+
# @param [Hash] options
|
9
|
+
# @return [CSV] Open CSV object
|
10
|
+
def self.open(file_name, options = {})
|
11
|
+
options = {
|
12
|
+
mode: 'rb:UTF-8',
|
13
|
+
required: %w'',
|
14
|
+
optional: %w'',
|
15
|
+
col_sep: ',',
|
16
|
+
quote_char: '"'
|
17
|
+
}.merge options
|
18
|
+
mode = options.delete(:mode)
|
19
|
+
required_headers = options.delete(:required)
|
20
|
+
optional_headers = options.delete(:optional)
|
21
|
+
options[:headers] = true
|
22
|
+
options[:return_headers] = true
|
23
|
+
csv = CSV.open(file_name, mode, options)
|
24
|
+
line = csv.shift
|
25
|
+
found_headers = required_headers & line.headers
|
26
|
+
return csv if found_headers.size == required_headers.size
|
27
|
+
raise RuntimeError, "CSV headers not found: #{required_headers - found_headers}" unless found_headers.empty?
|
28
|
+
csv.close
|
29
|
+
options[:headers] = (required_headers + optional_headers)[0...line.size]
|
30
|
+
raise RuntimeError, 'CSV does not contain enough columns' if required_headers.size > line.size
|
31
|
+
options[:return_headers] = true
|
32
|
+
csv = CSV.open(file_name, mode, options)
|
33
|
+
csv.shift
|
34
|
+
csv
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'libis/tools/extend/ostruct'
|
2
|
+
require 'recursive-open-struct'
|
3
|
+
|
4
|
+
module Libis
|
5
|
+
module Tools
|
6
|
+
|
7
|
+
# A class that derives from OpenStruct through the RecursiveOpenStruct.
|
8
|
+
# By wrapping a Hash recursively, it allows for easy access to the content by method names.
|
9
|
+
# A RecursiveOpenStruct is derived from stdlib's OpenStruct, but can be made recursive.
|
10
|
+
# DeepStruct enforces this behaviour and adds a clear! method.
|
11
|
+
class DeepStruct < RecursiveOpenStruct
|
12
|
+
include Enumerable
|
13
|
+
|
14
|
+
# Create a new DeepStruct from a Hash and configure the behaviour.
|
15
|
+
#
|
16
|
+
# @param [Hash] hash the initial data structure.
|
17
|
+
# @param [Hash] opts optional configuration options:
|
18
|
+
# * recurse_over_arrays: also wrap the Hashes that are enbedded in Arrays. Default: true.
|
19
|
+
# * preserver_original_keys: creating a Hash from the wrapper preserves symbols and strings as keys. Default: true.
|
20
|
+
def initialize(hash = {}, opts = {})
|
21
|
+
hash = {} unless hash
|
22
|
+
opts = {} unless opts
|
23
|
+
hash = {default: hash} unless hash.is_a? Hash
|
24
|
+
super(hash, {recurse_over_arrays: true, preserve_original_keys: true}.merge(opts))
|
25
|
+
end
|
26
|
+
|
27
|
+
def merge(hash)
|
28
|
+
return self unless hash.respond_to?(:to_hash)
|
29
|
+
hash.to_hash.inject(self.dup) do |ds, (key, value)|
|
30
|
+
ds[key] = DeepDup.new(
|
31
|
+
recurse_over_arrays: @recurse_over_arrays,
|
32
|
+
preserve_original_keys: @preserve_original_keys
|
33
|
+
).call(value)
|
34
|
+
ds
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def merge!(hash)
|
39
|
+
return self unless hash.respond_to?(:to_hash)
|
40
|
+
hash.to_hash.inject(self) do |ds, (key, value)|
|
41
|
+
ds[key] = DeepDup.new(
|
42
|
+
recurse_over_arrays: @recurse_over_arrays,
|
43
|
+
preserve_original_keys: @preserve_original_keys
|
44
|
+
).call(value)
|
45
|
+
ds
|
46
|
+
end
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
def key?(key)
|
51
|
+
self.respond_to?(key)
|
52
|
+
end
|
53
|
+
alias_method :has_key?, :key?
|
54
|
+
|
55
|
+
def keys
|
56
|
+
@table.keys
|
57
|
+
end
|
58
|
+
|
59
|
+
def each(&block)
|
60
|
+
self.each_pair &block
|
61
|
+
end
|
62
|
+
|
63
|
+
# Delete all data fields
|
64
|
+
def clear!
|
65
|
+
@table.keys.each { |key| delete_field(key) }
|
66
|
+
@sub_elements = {}
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|