docker-template 0.2.0 → 0.3.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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +30 -4
  3. data/LICENSE +1 -1
  4. data/README.md +79 -14
  5. data/Rakefile +115 -38
  6. data/bin/docker-template +24 -10
  7. data/comp/bin +9 -0
  8. data/comp/list +83 -0
  9. data/comp/list.pak +0 -0
  10. data/lib/docker/template.rb +47 -61
  11. data/lib/docker/template/builder.rb +302 -0
  12. data/lib/docker/template/cache.rb +71 -0
  13. data/lib/docker/template/cli.rb +125 -0
  14. data/lib/docker/template/error.rb +120 -11
  15. data/lib/docker/template/logger.rb +128 -0
  16. data/lib/docker/template/metadata.rb +566 -103
  17. data/lib/docker/template/normal.rb +46 -0
  18. data/lib/docker/template/notify.rb +44 -0
  19. data/lib/docker/template/parser.rb +48 -38
  20. data/lib/docker/template/repo.rb +131 -97
  21. data/lib/docker/template/rootfs.rb +51 -41
  22. data/lib/docker/template/scratch.rb +96 -66
  23. data/lib/docker/template/version.rb +4 -2
  24. data/lib/erb/context.rb +29 -0
  25. data/shas.yml +11 -0
  26. data/templates/rootfs.erb +5 -0
  27. data/templates/rootfs/alpine.erb +71 -0
  28. data/templates/rootfs/ubuntu.erb +76 -0
  29. data/{lib/docker/template/templates → templates}/scratch.erb +0 -1
  30. metadata +64 -50
  31. data/lib/docker/template/alias.rb +0 -28
  32. data/lib/docker/template/ansi.rb +0 -85
  33. data/lib/docker/template/auth.rb +0 -25
  34. data/lib/docker/template/common.rb +0 -130
  35. data/lib/docker/template/config.rb +0 -80
  36. data/lib/docker/template/error/bad_exit_status.rb +0 -17
  37. data/lib/docker/template/error/bad_repo_name.rb +0 -15
  38. data/lib/docker/template/error/invalid_repo_type.rb +0 -16
  39. data/lib/docker/template/error/invalid_targz_file.rb +0 -15
  40. data/lib/docker/template/error/no_rootfs_copy_dir.rb +0 -15
  41. data/lib/docker/template/error/no_rootfs_mkimg.rb +0 -15
  42. data/lib/docker/template/error/no_setup_context_found.rb +0 -15
  43. data/lib/docker/template/error/not_implemented.rb +0 -15
  44. data/lib/docker/template/error/repo_not_found.rb +0 -16
  45. data/lib/docker/template/interface.rb +0 -118
  46. data/lib/docker/template/patches.rb +0 -9
  47. data/lib/docker/template/patches/array.rb +0 -11
  48. data/lib/docker/template/patches/hash.rb +0 -71
  49. data/lib/docker/template/patches/object.rb +0 -9
  50. data/lib/docker/template/patches/pathname.rb +0 -46
  51. data/lib/docker/template/patches/string.rb +0 -9
  52. data/lib/docker/template/routable.rb +0 -28
  53. data/lib/docker/template/simple.rb +0 -49
  54. data/lib/docker/template/stream.rb +0 -63
  55. data/lib/docker/template/templates/rootfs.erb +0 -8
  56. data/lib/docker/template/util.rb +0 -54
  57. data/lib/docker/template/util/copy.rb +0 -77
  58. data/lib/docker/template/util/data.rb +0 -26
@@ -0,0 +1,125 @@
1
+ # ----------------------------------------------------------------------------
2
+ # Frozen-string-literal: true
3
+ # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License
4
+ # Encoding: utf-8
5
+ # ----------------------------------------------------------------------------
6
+
7
+ require "thor"
8
+
9
+ module Docker
10
+ module Template
11
+ class CLI < Thor
12
+
13
+ # ----------------------------------------------------------------------
14
+ # docker-template build [repos [opts]]
15
+ # ----------------------------------------------------------------------
16
+
17
+ desc "build [REPOS [OPTS]]", "Build all (or some) of your repositories"
18
+ option :cache_only, :type => :boolean, :desc => "Only cache your repositories, don't build."
19
+ option :clean_only, :type => :boolean, :desc => "Only clean your repositories, don't build."
20
+ option :push_only, :type => :boolean, :desc => "Only push your repositories, don't build."
21
+ option :profile, :type => :boolean, :desc => "Profile Memory."
22
+ option :tty, :type => :boolean, :desc => "Enable TTY Output."
23
+ option :push, :type => :boolean, :desc => "Push Repo After Building."
24
+ option :cache, :type => :boolean, :desc => "Cache your repositories to cache."
25
+ option :mocking, :type => :boolean, :desc => "Disable Certain Actions."
26
+ option :clean, :type => :boolean, :desc => "Cleanup your caches."
27
+
28
+ # ----------------------------------------------------------------------
29
+
30
+ def build(*args)
31
+ repos = nil; with_profiling do
32
+ repos = Parser.new(args, options).parse.tap { |o| o.map( \
33
+ &:build) }.uniq(&:name).map(&:clean)
34
+ end
35
+
36
+ rescue Docker::Template::Error::StandardError => e
37
+ $stderr.puts Simple::Ansi.red(e.message)
38
+ exit e.respond_to?(:status) ? e.status : 1
39
+ end
40
+
41
+ # ----------------------------------------------------------------------
42
+ # docker-template list [options]
43
+ # ----------------------------------------------------------------------
44
+
45
+ desc "list [OPTS]", "List all possible builds."
46
+ option :grep, :type => :boolean, :desc => "Make --only a Regexp search."
47
+ option :only, :type => :string, :desc => "Only a specific repo."
48
+
49
+ # ----------------------------------------------------------------------
50
+
51
+ def list
52
+ Parser.new([], {}).parse.each do |repo|
53
+ repo_s = repo_s = repo.to_s.gsub(/^[^\/]+\//, "")
54
+ next unless (only.is_a?(Regexp) && repo_s =~ only) \
55
+ || (only && repo_s == only) || !only
56
+
57
+ $stderr.print repo.to_s
58
+ $stderr.print " -> ", repo.aliased.to_s, "\n" if repo.alias?
59
+ $stderr.puts unless repo.alias?
60
+ end
61
+ rescue Docker::Template::Error::StandardError => e
62
+ $stderr.puts Simple::Ansi.red(e.message)
63
+ exit e.respond_to?(:status) \
64
+ ? e.status : 1
65
+ end
66
+
67
+ # ----------------------------------------------------------------------
68
+
69
+ no_tasks do
70
+ def only
71
+ return @only ||= begin
72
+ if !options.grep?
73
+ then options[
74
+ :only
75
+ ]
76
+
77
+ elsif options.only?
78
+ Regexp.new(options[
79
+ :only
80
+ ])
81
+ end
82
+ end
83
+ end
84
+
85
+ # --------------------------------------------------------------------
86
+ # When a user wishes to profile their builds to see memory being used.
87
+ # rubocop:disable Lint/RescueException
88
+ # --------------------------------------------------------------------
89
+
90
+ def with_profiling
91
+ if options.profile?
92
+ begin
93
+ require "memory_profiler"
94
+ MemoryProfiler.report(:top => 10_240) { yield }.pretty_print({\
95
+ :to_file => "mem.txt"
96
+ })
97
+ rescue LoadError
98
+ $stderr.puts "The gem 'memory_profiler' wasn't found."
99
+ $stderr.puts "You can install it with `gem install memory_profiler'"
100
+ abort "Hope you install it so you can report back."
101
+ end
102
+
103
+ else
104
+ yield
105
+ end
106
+
107
+ rescue Excon::Errors::SocketError
108
+ $stderr.puts "Unable to connect to your Docker Instance."
109
+ $stderr.puts "Are you absolutely sure that you have the Docker installed?"
110
+ abort "Unable to build your images."
111
+
112
+ rescue Exception
113
+ raise unless $ERROR_POSITION
114
+ $ERROR_POSITION.delete_if do |source|
115
+ source =~ %r!#{Regexp.escape(
116
+ __FILE__
117
+ )}!o
118
+ end
119
+
120
+ raise
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -1,20 +1,129 @@
1
+ # ----------------------------------------------------------------------------
1
2
  # Frozen-string-literal: true
2
- # Copyright: 2015 Jordon Bedwell - Apache v2.0 License
3
+ # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License
3
4
  # Encoding: utf-8
5
+ # ----------------------------------------------------------------------------
4
6
 
5
7
  module Docker
6
8
  module Template
7
9
  module Error
8
- const_set :StandardError, Class.new(StandardError)
9
- autoload :BadRepoName, "docker/template/error/bad_repo_name"
10
- autoload :BadExitStatus, "docker/template/error/bad_exit_status"
11
- autoload :InvalidTargzFile, "docker/template/error/invalid_targz_file"
12
- autoload :NoSetupContextFound, "docker/template/error/no_setup_context_found"
13
- autoload :NoRootfsCopyDir, "docker/template/error/no_rootfs_copy_dir"
14
- autoload :InvalidRepoType, "docker/template/error/invalid_repo_type"
15
- autoload :NoRootfsMkimg, "docker/template/error/no_rootfs_mkimg"
16
- autoload :NotImplemented, "docker/template/error/not_implemented"
17
- autoload :RepoNotFound, "docker/template/error/repo_not_found"
10
+ StandardError = Class.new(
11
+ StandardError
12
+ )
13
+
14
+ # ----------------------------------------------------------------------
15
+
16
+ class PlaceHolderError < StandardError
17
+ def initialize(error)
18
+ super "PLACEHOLDER ERROR: %s" % (
19
+ error
20
+ )
21
+ end
22
+ end
23
+
24
+ # ----------------------------------------------------------------------
25
+
26
+ class BadExitStatus < StandardError
27
+ attr_reader :status
28
+
29
+ def initialize(status)
30
+ super "Got bad exit status #{
31
+ @status = status
32
+ }"
33
+ end
34
+ end
35
+
36
+ # ----------------------------------------------------------------------
37
+
38
+ class BadRepoName < StandardError
39
+ def initialize(name)
40
+ super "Only a-z0-9_- are allowed. Invalid repo name: #{
41
+ name
42
+ }"
43
+ end
44
+ end
45
+
46
+ # ----------------------------------------------------------------------
47
+
48
+ class InvalidRepoType < StandardError
49
+ def initialize(type)
50
+ build_types = Template.config.build_types.join(", ")
51
+ super "Uknown repo type given '#{type}' not in '#{
52
+ build_types
53
+ }'"
54
+ end
55
+ end
56
+
57
+ # ----------------------------------------------------------------------
58
+
59
+ class InvalidTargzFile < StandardError
60
+ def initialize(tar_gz)
61
+ super "No data was given to the tar.gz file '#{
62
+ tar_gz.basename
63
+ }'"
64
+ end
65
+ end
66
+
67
+ # ----------------------------------------------------------------------
68
+
69
+ class InvalidYAMLFile < StandardError
70
+ def initialize(file)
71
+ super "The yaml data provided by #{file} is invalid and not a hash."
72
+ end
73
+ end
74
+
75
+ # ----------------------------------------------------------------------
76
+
77
+ class NoHookExists < StandardError
78
+ def initialize(base, point)
79
+ super "Unknown hook base '#{base}' or hook point '#{
80
+ point
81
+ }'"
82
+ end
83
+ end
84
+
85
+ # ----------------------------------------------------------------------
86
+
87
+ class NoRootMetadata < StandardError
88
+ def initialize
89
+ super "Metadata without the root flag must provide the root_metadata."
90
+ end
91
+ end
92
+
93
+ # ----------------------------------------------------------------------
94
+
95
+ class NoRootfsMkimg < StandardError
96
+ def initialize
97
+ super "Unable to find rootfs.rb in your repo folder."
98
+ end
99
+ end
100
+
101
+ # ----------------------------------------------------------------------
102
+
103
+ class NoSetupContext < StandardError
104
+ def initialize
105
+ super "No #setup_context method exists."
106
+ end
107
+ end
108
+
109
+ # ----------------------------------------------------------------------
110
+
111
+ class NotImplemented < StandardError
112
+ def initialize
113
+ super "The feature is not implemented yet, sorry about that."
114
+ end
115
+ end
116
+
117
+ # ----------------------------------------------------------------------
118
+
119
+ class RepoNotFound < StandardError
120
+ def initialize(repo = nil)
121
+ ending = repo ? "the repo '#{repo}'" : "your repo folder"
122
+ super "Unable to find #{
123
+ ending
124
+ }"
125
+ end
126
+ end
18
127
  end
19
128
  end
20
129
  end
@@ -0,0 +1,128 @@
1
+ # ----------------------------------------------------------------------------
2
+ # Frozen-string-literal: true
3
+ # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License
4
+ # Encoding: utf-8
5
+ # ----------------------------------------------------------------------------
6
+
7
+ module Docker
8
+ module Template
9
+ class Logger
10
+ def initialize(builder = nil)
11
+ @lines = { 0 => 0 }
12
+ @builder = \
13
+ builder
14
+ end
15
+
16
+ # ----------------------------------------------------------------------
17
+
18
+ def increment
19
+ @lines.update({
20
+ @lines.size => @lines.size
21
+ })
22
+ end
23
+
24
+ # ----------------------------------------------------------------------
25
+ # A simple TTY stream that just prints out the data that it is given.
26
+ # This is the logger that most will use for most of their building.
27
+ # ----------------------------------------------------------------------
28
+
29
+ def tty(stream)
30
+ $stdout.print stream
31
+ end
32
+
33
+ # ----------------------------------------------------------------------
34
+ # A simple logger that accepts a multi-type stream.
35
+ # ----------------------------------------------------------------------
36
+
37
+ def simple(type, str)
38
+ type == :stderr ? $stderr.print(str) : $stdout.print(str)
39
+ end
40
+
41
+ # ----------------------------------------------------------------------
42
+ # A more complex streamer designed for the actual output of the Docker.
43
+ # ----------------------------------------------------------------------
44
+
45
+ def api(part, *_)
46
+ stream = JSON.parse(part)
47
+ retried ||= false
48
+
49
+ return progress_bar(stream) if stream.any_key?("progress", "progressDetail")
50
+ return output(stream["status"] || stream["stream"]) if stream.any_key?("status", "stream")
51
+ return progress_error(stream) if stream.any_key?("errorDetail", "error")
52
+
53
+ warn Simple::Ansi.red("Unhandled Stream.")
54
+ $stdout.puts(
55
+ part
56
+ )
57
+
58
+ # Addresses a Docker 1.9 bug.
59
+ rescue JSON::ParserError => e
60
+ if !retried
61
+ retried = true
62
+ part = "#{part}\" }"
63
+ retry
64
+ else
65
+ raise e
66
+ end
67
+ end
68
+
69
+ # ----------------------------------------------------------------------
70
+
71
+ def output(msg)
72
+ unless filter_matches?(msg)
73
+ $stdout.puts msg
74
+ increment
75
+ end
76
+ end
77
+
78
+ # ----------------------------------------------------------------------
79
+
80
+ def progress_error(stream)
81
+ abort Object::Simple::Ansi.red(
82
+ stream["errorDetail"]["message"]
83
+ )
84
+ end
85
+
86
+ # ----------------------------------------------------------------------
87
+
88
+ private
89
+ def progress_bar(stream)
90
+ id = stream["id"]
91
+
92
+ return unless id
93
+ before, diff = progress_diff(id)
94
+ $stderr.print before if before
95
+ str = stream["progress"] || stream["status"]
96
+ str = "#{id}: #{str}\r"
97
+
98
+ $stderr.print(Object::Simple::Ansi.jump(
99
+ str, diff
100
+ ))
101
+ end
102
+
103
+ # ----------------------------------------------------------------------
104
+
105
+ private
106
+ def progress_diff(id)
107
+ if @lines.key?(id)
108
+ return nil, @lines.size - @lines[id]
109
+ end
110
+
111
+ @lines[id] = @lines.size
112
+ before = "\n" unless @lines.one?
113
+ return before, 0
114
+ end
115
+
116
+ # ----------------------------------------------------------------------
117
+
118
+ private
119
+ def filter_matches?(msg)
120
+ return false unless @builder
121
+
122
+ @builder.repo.metadata["log_filters"].any? do |filter|
123
+ filter.is_a?(Regexp) && msg =~ filter || msg == filter
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -1,164 +1,627 @@
1
+ # ----------------------------------------------------------------------------
1
2
  # Frozen-string-literal: true
2
- # Copyright: 2015 Jordon Bedwell - Apache v2.0 License
3
+ # Copyright: 2015 - 2016 Jordon Bedwell - Apache v2.0 License
3
4
  # Encoding: utf-8
5
+ # ----------------------------------------------------------------------------
6
+
7
+ require "active_support/inflector"
8
+ require "active_support/core_ext/hash/indifferent_access"
9
+ require "yaml"
4
10
 
5
11
  module Docker
6
12
  module Template
7
13
  class Metadata
8
- extend Forwardable, Routable
9
-
10
- # Provides aliases for the root element so you can do something like:
11
- # * data["release"].fallback
12
-
13
- ALIASES = {
14
- "entry" => "entries",
15
- "release" => "releases",
16
- "version" => "versions",
17
- "script" => "scripts",
18
- "image" => "images"
19
- }
20
-
21
- def_delegator :@metadata, :keys
22
- def_delegator :@metadata, :size
23
- def_delegator :@metadata, :to_enum
24
- def_delegator :@metadata, :has_key?
25
- def_delegator :@metadata, :inspect
26
- def_delegator :@metadata, :delete
27
- def_delegator :@metadata, :each
28
- def_delegator :@metadata, :to_h
29
- def_delegator :@metadata, :key?
30
- route_to_ivar :is_root, :@is_root, bool: true
31
- route_to_hash :for_all, :self, :all
32
-
33
- def initialize(metadata, root_metadata = metadata)
34
- @is_root = metadata == root_metadata
35
- @root_metadata = root_metadata || {}
36
- @metadata = metadata || {}
37
-
38
- return unless is_root?
39
- @root_metadata = @metadata
40
- @base = Template.config
14
+ attr_reader :data
15
+ extend Forwardable::Extended
16
+
17
+ # ----------------------------------------------------------------------
18
+ # rubocop:disable Style/MultilineBlockLayout
19
+ # ----------------------------------------------------------------------
20
+
21
+ OPTS_FILE = "opts.yml"
22
+ [Pathutil.allowed[:yaml][:classes], Array.allowed[:keys], \
23
+ Hash.allowed[:vals]].each do |v| v.push(
24
+ self, HashWithIndifferentAccess, Regexp
25
+ )
41
26
  end
42
27
 
28
+ # ----------------------------------------------------------------------
29
+ # rubocop:enable Style/MultilineBlockLayout
30
+ # ----------------------------------------------------------------------
31
+
32
+ DEFAULTS = HashWithIndifferentAccess.new({
33
+ "log_filters" => [],
34
+ "push" => false,
35
+ "cache" => false,
36
+ "type" => "normal",
37
+ "user" => "random_user",
38
+ "local_prefix" => "local",
39
+ "rootfs_base_img" => "envygeeks/ubuntu",
40
+ "maintainer" => "Random User <random.user@example.com>",
41
+ "name" => Template.root.basename.to_s,
42
+ "rootfs_template" => "alpine",
43
+ "cache_dir" => "cache",
44
+ "repos_dir" => "repos",
45
+ "copy_dir" => "copy",
46
+ "tag" => "latest",
47
+ "clean" => true,
48
+ "tty" => false,
49
+ "tags" => {}
50
+ }).freeze
51
+
52
+ # ----------------------------------------------------------------------
53
+ # @param data [Hash, self.class] - the main data.
54
+ # @param root [Hash, self.class] - the root data.
55
+ # Create a new instance of `self.class`.
43
56
  #
57
+ # @example ```
58
+ # self.class.new({
59
+ # :hello => :world
60
+ # })
61
+ # ```
62
+ # ----------------------------------------------------------------------
63
+
64
+ def initialize(overrides, root: nil)
65
+ if root.is_a?(self.class)
66
+ then root = root.to_h({
67
+ :raw => true
68
+ })
69
+ end
70
+
71
+ if overrides.is_a?(self.class)
72
+ then overrides = overrides.to_h({
73
+ :raw => true
74
+ })
75
+ end
44
76
 
45
- def as_gem_version
46
- "#{self["repo"]}@#{self["version"].fallback}"
77
+ if root.nil?
78
+ overrides = overrides.stringify
79
+ gdata = Template.root.join(OPTS_FILE).read_yaml
80
+ @data = DEFAULTS.deep_merge(gdata.stringify).deep_merge(overrides)
81
+ tdata = Template.root.join(@data[:repos_dir], @data[:name], OPTS_FILE).read_yaml
82
+ @data = @data.deep_merge(tdata.stringify).deep_merge(overrides)
83
+ @data = @data.stringify.with_indifferent_access
84
+
85
+ else
86
+ @data = overrides.stringify.with_indifferent_access
87
+ @root_data = root.stringify \
88
+ .with_indifferent_access
89
+ end
47
90
  end
48
91
 
49
- #
92
+ # ----------------------------------------------------------------------
93
+
94
+ def _shas
95
+ return @_shas ||= begin
96
+ self.class.new(Template.gem_root.join("shas.yml").read_yaml, {
97
+ :root => root_data
98
+ })
99
+ end
100
+ end
101
+
102
+ # ----------------------------------------------------------------------
103
+
104
+ def root_data
105
+ return @root_data || @data
106
+ end
107
+
108
+ # ----------------------------------------------------------------------
109
+
110
+ def root
111
+ Template.root.join(
112
+ root_data[:repos_dir], root_data[:name]
113
+ )
114
+ end
50
115
 
51
- def aliased
52
- tag = from_root("tag")
53
- aliases = from_root("aliases")
54
- return aliases[tag] if aliases.key?(tag)
55
- tag
116
+ # ----------------------------------------------------------------------
117
+ # Check if a part of the hash or a value is inside.
118
+ # @param val [Anytning(), Hash] - The key or key => val you wish check.
119
+ # @example metadata.include?(:key => :val) => true|false
120
+ # @example metadata.include?(:key) => true|false
121
+ # ----------------------------------------------------------------------
122
+
123
+ def include?(val)
124
+ if val.is_a?(Hash)
125
+ then val.stringify.each do |k, v|
126
+ unless @data.key?(k) && @data[k] == v
127
+ return false
128
+ end
129
+ end
130
+
131
+ true
132
+ else
133
+ @data.include?(
134
+ val
135
+ )
136
+ end
56
137
  end
57
138
 
58
- # Queries providing a default value if on the root repo hash otherwise
59
- # returning the returned value, as a `self.class` if it's a Hash.
139
+ # ----------------------------------------------------------------------
140
+ # @param key [Anything()] the key you wish to pull.
141
+ # @note we make the getter slightly more indifferent because of tags.
142
+ # Pull an indifferent key from the hash.
143
+ # ----------------------------------------------------------------------
60
144
 
61
145
  def [](key)
62
- key = determine_key(key)
63
- val = @metadata[key]
146
+ val = begin
147
+ if key =~ /^\d+\.\d+$/
148
+ @data[key] || @data[
149
+ key.to_f
150
+ ]
151
+
152
+ elsif key =~ /^\d+$/
153
+ @data[key] || @data[
154
+ key.to_i
155
+ ]
156
+
157
+ else
158
+ @data[key]
159
+ end
160
+ end
161
+
162
+ if val.is_a?(Hash)
163
+ return self.class.new(val, {
164
+ :root => root_data
165
+ })
166
+ end
64
167
 
65
- return try_default(key) if !val && is_root?
66
- return self.class.new(val, @root_metadata) if val.is_a?(Hash)
67
168
  val
68
169
  end
69
170
 
70
- #
171
+ # ----------------------------------------------------------------------
71
172
 
72
- def tags
73
- self["tags"].keys + self["aliases"].keys
173
+ def []=(key, val)
174
+ hash = { key => val }.stringify
175
+ @data.update(
176
+ hash
177
+ )
74
178
  end
75
179
 
76
- #
180
+ # ----------------------------------------------------------------------
181
+
182
+ def update(hash)
183
+ @data.update(
184
+ hash.stringify
185
+ )
186
+ end
187
+
188
+ # ----------------------------------------------------------------------
189
+
190
+ def to_enum
191
+ @data.each_with_object({}) do |(k, v), h|
192
+ if v.is_a?(Hash)
193
+ then v = self.class.new(v, {
194
+ :root => root_data
195
+ })
196
+ end
197
+
198
+ h[k] = v
199
+ end.to_enum
200
+ end
201
+
202
+ # ----------------------------------------------------------------------
203
+ # Merge a hash into the metadata. If you merge non-queryable data
204
+ # it will then get merged into the queryable data.
205
+ # ----------------------------------------------------------------------
77
206
 
78
207
  def merge(new_)
79
- @metadata.merge!(new_)
208
+ if !queryable?(:query_data => new_) && queryable?
209
+ new_ = {
210
+ :all => new_
211
+ }
212
+ end
213
+
214
+ new_ = new_.stringify
215
+ self.class.new(@data.deep_merge(new_), {
216
+ :root => root_data
217
+ })
218
+ end
219
+
220
+ # ----------------------------------------------------------------------
221
+ # Destructive merging (@see self#merge)
222
+ # ----------------------------------------------------------------------
223
+
224
+ def merge!(new_)
225
+ if !queryable?(:query_data => new_) && queryable?
226
+ new_ = {
227
+ :all => new_
228
+ }
229
+ end
230
+
231
+ @data = @data.deep_merge(
232
+ new_.stringify
233
+ )
234
+
80
235
  self
81
236
  end
82
237
 
83
- #
238
+ # --------------------------------------------------------------------
239
+ # Check if a hash is queryable. AKA has "all", "group", "tag".
240
+ # --------------------------------------------------------------------
84
241
 
85
- def as_string_set
86
- as_set.to_a.join(" ")
242
+ def queryable?(query_data: @data)
243
+ if query_data.is_a?(self.class)
244
+ then query_data \
245
+ .queryable?
246
+
247
+ elsif !query_data || !query_data.is_a?(Hash) || query_data.empty?
248
+ return false
249
+
250
+ else
251
+ (query_data.keys - %w(
252
+ group tag all
253
+ )).empty?
254
+ end
87
255
  end
88
256
 
89
- #
257
+ # --------------------------------------------------------------------
258
+ # Fallback, determining which route is the best. Tag > Group > All.
259
+ # --------------------------------------------------------------------
260
+
261
+ def fallback(group: current_group, tag: current_tag, query_data: @data)
262
+ if query_data.is_a?(self.class)
263
+ then query_data.fallback({
264
+ :group => group, :tag => tag
265
+ })
266
+
267
+ elsif !query_data || !query_data.is_a?(Hash) || query_data.empty?
268
+ return nil
269
+
270
+ else
271
+ by_tag(:tag => tag, :query_data => query_data) || \
272
+ by_parent_tag(:tag => tag, :query_data => query_data) || \
273
+ by_group(:group => group, :query_data => query_data) || \
274
+ by_parent_group(:tag => tag, :query_data => query_data) || \
275
+ for_all(:query_data => query_data)
276
+ end
277
+ end
278
+
279
+ # --------------------------------------------------------------------
90
280
 
91
- def as_hash
92
- {} \
93
- .merge(for_all.to_h) \
94
- .merge(by_type.to_h) \
95
- .merge(by_tag. to_h)
281
+ def for_all(query_data: @data)
282
+ if query_data.is_a?(self.class)
283
+ then query_data \
284
+ .for_all
285
+
286
+ elsif !query_data || !query_data.is_a?(Hash)
287
+ return nil
288
+
289
+ else
290
+ query_data.fetch(
291
+ "all", nil
292
+ )
293
+ end
96
294
  end
97
295
 
98
- #
296
+ # --------------------------------------------------------------------
99
297
 
100
- def as_set
101
- Set.new \
102
- .merge(for_all.to_a) \
103
- .merge(by_type.to_a) \
104
- .merge(by_tag .to_a)
298
+ def by_tag(tag: current_tag, query_data: @data)
299
+ if query_data.is_a?(self.class)
300
+ then query_data.by_tag({
301
+ :tag => tag
302
+ })
303
+
304
+ elsif !query_data || !query_data.is_a?(Hash)
305
+ return nil
306
+
307
+ else
308
+ query_data.fetch("tag", {}).fetch(
309
+ tag, nil
310
+ )
311
+ end
105
312
  end
106
313
 
107
- #
314
+ # ----------------------------------------------------------------------
315
+
316
+ def by_parent_tag(tag: current_tag, query_data: @data)
317
+ if aliased_tag == current_tag || !complex_alias?
318
+ return nil
108
319
 
109
- def from_root(key)
110
- root = self.class.new(@root_metadata)
111
- root[key]
320
+ else
321
+ by_tag({
322
+ :query_data => query_data,
323
+ :tag => aliased_tag({
324
+ :tag => tag
325
+ })
326
+ })
327
+ end
112
328
  end
113
329
 
114
- #
330
+ # --------------------------------------------------------------------
115
331
 
116
- def fallback
117
- by_tag || by_type || for_all
332
+ def by_group(group: current_group, query_data: @data)
333
+ if query_data.is_a?(self.class)
334
+ then query_data.by_group({
335
+ :group => group
336
+ })
337
+
338
+ elsif !query_data || !query_data.is_a?(Hash)
339
+ return nil
340
+
341
+ else
342
+ query_data.fetch("group", {}).fetch(
343
+ group, nil
344
+ )
345
+ end
118
346
  end
119
347
 
120
- # Pulls data based on the given tag through anything that provides a
121
- # "tag" key with the given tags. ("tags" is a `Hash`)
348
+ # ----------------------------------------------------------------------
122
349
 
123
- def by_tag
124
- return unless tag = aliased
125
- return unless key?("tag")
126
- hash = self["tag"]
127
- hash[tag]
350
+ def by_parent_group(tag: current_tag, query_data: @data)
351
+ if aliased_tag == current_tag || !complex_alias?
352
+ return nil
353
+
354
+ else
355
+ by_group({
356
+ :query_data => query_data,
357
+ :group => aliased_group({
358
+ :tag => tag
359
+ })
360
+ })
361
+ end
128
362
  end
129
363
 
130
- # Pull data based on the type given in { "tags" => { tag => type }}
131
- # through anything that provides a "type" key with the type as a
132
- # sub-key and the values.
364
+ # ----------------------------------------------------------------------
365
+ # Checks to see if the current metadata is an alias of another. This
366
+ # happens when the user has the tag in aliases but it's not complex.
367
+ # ----------------------------------------------------------------------
133
368
 
134
- def by_type
135
- return unless tag = aliased
136
- type = from_root("tags")[tag]
137
- return unless key?("type")
138
- return unless type
369
+ def alias?
370
+ !!(aliased_tag && aliased_tag != tag)
371
+ end
139
372
 
140
- hash = self["type"]
141
- hash[type]
373
+ # ----------------------------------------------------------------------
374
+ # A complex alias happens when the user has an alias but also tries to
375
+ # add extra data, this allows them to use data from all parties. This
376
+ # allows them to reap the benefits of having shared data but sometimes
377
+ # independent data that diverges into it's own.
378
+ # ----------------------------------------------------------------------
379
+
380
+ def complex_alias?
381
+ if !alias?
382
+ return false
383
+
384
+ else
385
+ !!root_data.find do |_, v|
386
+ (v.is_a?(self.class) || v.is_a?(Hash)) && queryable?(:query_data => v) \
387
+ && by_tag(:query_data => v)
388
+ end
389
+ end
142
390
  end
143
391
 
144
- #
392
+ # ----------------------------------------------------------------------
145
393
 
146
- private
147
- def determine_key(key)
148
- if is_root? && !key?(key) && ALIASES.key?(key)
149
- key = ALIASES[key]
394
+ def aliased_tag(tag: current_tag)
395
+ aliases = root_data[:aliases]
396
+ if aliases.nil? || !aliases.key?(tag)
397
+ tag
398
+
399
+ else
400
+ aliases[
401
+ tag
402
+ ]
150
403
  end
151
- key
152
404
  end
153
405
 
154
- #
406
+ # ----------------------------------------------------------------------
407
+
408
+ def aliased_group(tag: current_tag)
409
+ root_data[:tags][aliased_tag({
410
+ :tag => tag
411
+ })]
412
+ end
413
+
414
+ # ----------------------------------------------------------------------
415
+ # Converts the current meta into a string.
416
+ # ----------------------------------------------------------------------
417
+
418
+ def to_s(raw: false, shell: false)
419
+ if !raw && (mergeable_hash? || mergeable_array?)
420
+ to_a(:shell => shell).join(" #{
421
+ "\n" if shell
422
+ }")
423
+
424
+ elsif !raw && queryable?
425
+ then fallback \
426
+ .to_s
427
+
428
+ else
429
+ @data.to_s
430
+ end
431
+ end
432
+
433
+ # ----------------------------------------------------------------------
434
+
435
+ def to_a(raw: false, shell: false)
436
+ if raw
437
+ return to_h({
438
+ :raw => true
439
+ }).to_a
440
+
441
+ elsif !mergeable_array?
442
+ to_h.each_with_object([]) do |(k, v), a|
443
+ a << "#{k}=#{
444
+ shell ? v.to_s.shellescape : v
445
+ }"
446
+ end
447
+ else
448
+ (for_all || []) | (by_parent_group || []) | (by_group || []) | \
449
+ (by_parent_tag || []) | (by_tag || [])
450
+ end
451
+ end
452
+
453
+ # ----------------------------------------------------------------------
454
+ # Convert a `Metadata' into a normal hash. If `self' is queryable then
455
+ # we go and start merging values smartly. This means that we will merge
456
+ # all the arrays into one another and we will merge hashes into hashes.
457
+ # ----------------------------------------------------------------------
458
+
459
+ def to_h(raw: false)
460
+ return @data.to_h if raw || !queryable? || !mergeable_hash?
461
+ keys = [for_all, by_group, by_parent_group, by_tag, \
462
+ by_parent_tag].compact.map(&:keys)
463
+
464
+ keys.reduce(:+).each_with_object({}) do |k, h|
465
+ vals = [for_all, by_group, by_parent_group, by_tag, \
466
+ by_parent_tag].compact
467
+
468
+ h[k] = \
469
+ if mergeable_array?(k)
470
+ vals.map { |v| v[k].to_a } \
471
+ .compact.reduce(
472
+ :+
473
+ )
474
+
475
+ elsif mergeable_hash?(k)
476
+ vals.map { |v| v[k].to_h } \
477
+ .compact.reduce(
478
+ :deep_merge
479
+ )
480
+
481
+ else
482
+ vals.find do |v|
483
+ v[k]
484
+ end \
485
+ [k]
486
+ end
487
+ end
488
+ end
489
+
490
+ # ----------------------------------------------------------------------
491
+
492
+ def mergeable_hash?(key = nil)
493
+ return false unless queryable?
494
+ vals = [by_parent_tag, by_parent_group, \
495
+ by_tag, for_all, by_group].compact
496
+
497
+ if key
498
+ vals = vals.map do |val|
499
+ val[key]
500
+ end
501
+ end
502
+
503
+ !vals.empty? && !vals.any? do |val|
504
+ !val.is_a?(Hash) && !val.is_a?(
505
+ self.class
506
+ )
507
+ end
508
+ end
509
+
510
+ # ----------------------------------------------------------------------
511
+
512
+ def mergeable_array?(key = nil)
513
+ return false unless queryable?
514
+ vals = [by_parent_tag, by_parent_group, \
515
+ by_tag, for_all, by_group].compact
516
+
517
+ if key
518
+ vals = vals.map do |val|
519
+ val[key]
520
+ end
521
+ end
522
+
523
+ !vals.empty? && !vals.any? do |val|
524
+ !val.is_a?(
525
+ Array
526
+ )
527
+ end
528
+ end
529
+
530
+ # ----------------------------------------------------------------------
531
+
532
+ def current_group
533
+ root_data[:tags][current_tag] ||
534
+ "normal"
535
+ end
536
+
537
+ # ----------------------------------------------------------------------
538
+ # HELPER: Get a list of all the tags.
539
+ # ----------------------------------------------------------------------
540
+
541
+ def tags
542
+ (root_data[:tags] || {}).keys | (root_data[:aliases] || {}).keys
543
+ end
544
+
545
+ # ----------------------------------------------------------------------
546
+ # HELPER: Get a list of all the groups.
547
+ # ----------------------------------------------------------------------
548
+
549
+ def groups
550
+ root_data["tags"].values.uniq
551
+ end
552
+
553
+ # ----------------------------------------------------------------------
155
554
 
156
555
  private
157
- def try_default(key)
158
- val = @base[key]
159
- return self.class.new(val, @root_metadata) if val.is_a?(Hash)
160
- val
556
+ def merge_or_override(val, new_val)
557
+ return new_val unless val
558
+ return val if val.is_a?(String) && !new_val || !new_val.is_a?(val.class)
559
+ return new_val.merge(val) if val.respond_to?(:merge)
560
+ return new_val | val if val.respond_to?(:|)
561
+ end
562
+
563
+ # ----------------------------------------------------------------------
564
+
565
+ private
566
+ def string_wrapper(obj, shell: false)
567
+ return obj if obj == true || obj == false || obj.nil?
568
+ return obj.to_s(:shell => shell) if obj.is_a?(self.class)
569
+ !obj.is_a?(Array) ? obj.to_s : obj.join(
570
+ "\s"
571
+ )
161
572
  end
573
+
574
+ # ----------------------------------------------------------------------
575
+
576
+ private
577
+ def method_missing(method, *args, shell: false, &block)
578
+ key = method.to_s.gsub(/\?$/, "")
579
+ val = self[key] || self[key.singularize] \
580
+ || self[key.pluralize]
581
+
582
+ if !args.empty? || block_given?
583
+ super
584
+
585
+ elsif method !~ /\?$/
586
+ string_wrapper(
587
+ val, :shell => shell
588
+ )
589
+
590
+ else
591
+ val != false && !val.nil? && \
592
+ !val.empty?
593
+ end
594
+ end
595
+
596
+ # ----------------------------------------------------------------------
597
+
598
+ alias deep_merge merge
599
+ alias group current_group
600
+ rb_delegate :for_all, :to => :self, :type => :hash, :key => :all
601
+ rb_delegate :current_tag, :to => :root_data, :key => :tag, :type => :hash
602
+ rb_delegate :tag, :to => :root_data, :type => :hash, :key => :tag
603
+ rb_delegate :root, :to => :@root, :type => :ivar, :bool => true
604
+
605
+ # ----------------------------------------------------------------------
606
+
607
+ rb_delegate :fetch, :to => :@data
608
+ rb_delegate :delete, :to => :@data
609
+ rb_delegate :empty?, :to => :@data
610
+ rb_delegate :inspect, :to => :@data
611
+ rb_delegate :values_at, :to => :@data
612
+ rb_delegate :values, :to => :@data
613
+ rb_delegate :keys, :to => :@data
614
+ rb_delegate :key?, :to => :@data
615
+ rb_delegate :==, :to => :@data
616
+
617
+ # ----------------------------------------------------------------------
618
+
619
+ rb_delegate :inject, :to => :to_enum
620
+ rb_delegate :select, :to => :to_enum
621
+ rb_delegate :each_with_object, :to => :to_enum
622
+ rb_delegate :collect, :to => :to_enum
623
+ rb_delegate :find, :to => :to_enum
624
+ rb_delegate :each, :to => :to_enum
162
625
  end
163
626
  end
164
627
  end