docker-template 0.2.0 → 0.3.0

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