orchparty 0.1.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.
data/exe/orchparty ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ require "thor"
3
+ require "orchparty"
4
+
5
+ class OrchPartyCommand < Thor
6
+ desc "generate INPUT_FILE OUTPUT_FILE APPLICATION_NAME", "generates a configuration file for an application"
7
+ method_option :generator, :aliases => "-g", default: "docker_compose_v1", :desc => "Which generator to take"
8
+ method_option :aliases => "g"
9
+ def generate(input_file, output_file, application_name)
10
+ generator = options[:generator] || "docker_compose_v1"
11
+ File.write(output_file, Orchparty.send(generator.to_sym, input_file, application_name))
12
+ end
13
+ end
14
+
15
+ OrchPartyCommand.start
data/lib/deep_merge.rb ADDED
@@ -0,0 +1,106 @@
1
+ module Hashie
2
+ module Extensions
3
+ module DeepMergeConcat
4
+
5
+ def transform_values
6
+ result = self.class.new
7
+ each do |key, value|
8
+ result[key] = yield(value)
9
+ end
10
+ result
11
+ end
12
+
13
+ def transform_values!
14
+ each do |key, value|
15
+ self[key] = yield(value)
16
+ end
17
+ end
18
+
19
+ def transform_keys
20
+ result = self.class.new
21
+ each_key do |key|
22
+ result[yield(key)] = self[key]
23
+ end
24
+ result
25
+ end
26
+
27
+ def transform_keys!
28
+ keys.each do |key|
29
+ self[yield(key)] = delete(key)
30
+ end
31
+ self
32
+ end
33
+
34
+ # Returns a new hash with +self+ and +other_hash+ merged recursively.
35
+ def deep_merge_concat(other_hash, &block)
36
+ copy = dup
37
+ copy.extend(Hashie::Extensions::DeepMergeConcat) unless copy.respond_to?(:deep_merge_concat!)
38
+ copy.deep_merge_concat!(other_hash, &block)
39
+ end
40
+
41
+ # Returns a new hash with +self+ and +other_hash+ merged recursively.
42
+ # Modifies the receiver in place.
43
+ def deep_merge_concat!(other_hash, &block)
44
+ return self unless other_hash.is_a?(::Hash)
45
+ _recursive_merge_concat(self, other_hash, &block)
46
+ self
47
+ end
48
+
49
+ def deep_transform_values(&block)
50
+ _deep_transform_values_in_object(self, &block)
51
+ end
52
+
53
+ def deep_transform_values!(&block)
54
+ _deep_transform_values_in_object!(self, &block)
55
+ end
56
+
57
+ private
58
+
59
+ def _deep_transform_values_in_object(object, &block)
60
+ case object
61
+ when Hash
62
+ object.each_with_object({}) do |arg, result|
63
+ key = arg.first
64
+ value = arg.last
65
+ result[key] = _deep_transform_values_in_object(value, &block)
66
+ end
67
+ when Array
68
+ object.map {|e| _deep_transform_values_in_object(e, &block) }
69
+ else
70
+ yield(object)
71
+ end
72
+ end
73
+
74
+ def _deep_transform_values_in_object!(object, &block)
75
+ case object
76
+ when Hash
77
+ object.each do |key, value|
78
+ object[key] = _deep_transform_values_in_object!(value, &block)
79
+ end
80
+ when Array
81
+ object.map! {|e| _deep_transform_values_in_object!(e, &block) }
82
+ else
83
+ yield(object)
84
+ end
85
+ end
86
+
87
+
88
+ def _recursive_merge_concat(hash, other_hash, &block)
89
+ other_hash.each do |k, v|
90
+ hash[k] = if hash.key?(k) && hash[k].is_a?(::Hash) && v.is_a?(::Hash)
91
+ _recursive_merge(hash[k], v, &block)
92
+ elsif hash.key?(k) && hash[k].is_a?(::Array) && v.is_a?(::Array)
93
+ hash[k].concat(v)
94
+ else
95
+ if hash.key?(k) && block_given?
96
+ block.call(k, hash[k], v)
97
+ else
98
+ v.respond_to?(:deep_dup) ? v.deep_dup : v
99
+ end
100
+ end
101
+ end
102
+ hash
103
+ end
104
+ end
105
+ end
106
+ end
data/lib/hash.rb ADDED
@@ -0,0 +1,28 @@
1
+ class HashUtils
2
+ def self.transform_hash(original, options={}, &block)
3
+ original.inject({}){|result, (key,value)|
4
+ value = if (options[:deep] && Hash === value)
5
+ transform_hash(value, options, &block)
6
+ else
7
+ if Array === value
8
+ value.map{|v|
9
+ if Hash === v
10
+ transform_hash(v, options, &block)
11
+ else
12
+ v
13
+ end}
14
+ else
15
+ value
16
+ end
17
+ end
18
+ block.call(result,key,value)
19
+ result
20
+ }
21
+ end
22
+ # Convert keys to strings, recursively
23
+ def self.deep_stringify_keys(hash)
24
+ transform_hash(hash, :deep => true) {|hash, key, value|
25
+ hash[key.to_s] = value
26
+ }
27
+ end
28
+ end
@@ -0,0 +1,7 @@
1
+ module Orchparty
2
+ class AST
3
+ class Application < Node
4
+
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ module Orchparty
2
+ class AST
3
+ class Mixin < Node
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Orchparty
2
+ class AST
3
+ class Root < Node
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Orchparty
2
+ class AST
3
+ class Service < Node
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,16 @@
1
+ require 'hashie'
2
+
3
+ module Orchparty
4
+ class AST
5
+ class Node < ::Hashie::Mash
6
+ include Hashie::Extensions::DeepMerge
7
+ include Hashie::Extensions::DeepMergeConcat
8
+ include Hashie::Extensions::MethodAccess
9
+ include Hashie::Extensions::Mash::KeepOriginalKeys
10
+ end
11
+ end
12
+ end
13
+ require 'orchparty/ast/application'
14
+ require 'orchparty/ast/mixin'
15
+ require 'orchparty/ast/root'
16
+ require 'orchparty/ast/service'
@@ -0,0 +1,12 @@
1
+ module Orchparty
2
+ class Context < ::Hashie::Mash
3
+ include Hashie::Extensions::DeepMerge
4
+ include Hashie::Extensions::DeepMergeConcat
5
+ include Hashie::Extensions::MethodAccess
6
+ include Hashie::Extensions::Mash::KeepOriginalKeys
7
+
8
+ def context
9
+ self
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,186 @@
1
+ require 'pathname'
2
+ require 'docile'
3
+ module Orchparty
4
+ class DSLParser
5
+ attr_reader :filename
6
+
7
+ def initialize(filename)
8
+ @filename = filename
9
+ end
10
+
11
+ def parse
12
+ file_content = File.read(filename)
13
+ builder = RootBuilder.new
14
+ builder.instance_eval(file_content, filename)
15
+ builder.build
16
+ end
17
+ end
18
+
19
+ class RootBuilder
20
+
21
+ def initialize
22
+ @root = AST::Root.new(applications: {}, mixins: {})
23
+ end
24
+
25
+ def import(rel_file)
26
+ old_file_path = Pathname.new(caller[0][/[^:]+/]).parent
27
+ rel_file_path = Pathname.new rel_file
28
+ new_file_path = old_file_path + rel_file_path
29
+ file_content = File.read(new_file_path)
30
+ instance_eval(file_content, new_file_path.expand_path.to_s)
31
+ end
32
+
33
+ def application(name, &block)
34
+ @root.applications[name] = Docile.dsl_eval(ApplicationBuilder.new(name), &block).build
35
+ self
36
+ end
37
+
38
+ def mixin(name, &block)
39
+ @root.mixins[name] = Docile.dsl_eval(MixinBuilder.new(name), &block).build
40
+ self
41
+ end
42
+
43
+ def build
44
+ @root
45
+ end
46
+ end
47
+
48
+ class MixinBuilder
49
+
50
+ def initialize(name)
51
+ @mixin = AST::Mixin.new(name: name, services: {})
52
+ end
53
+
54
+ def service(name, &block)
55
+ builder = ServiceBuilder.new(name)
56
+ builder.instance_eval(&block)
57
+ @mixin.services[name] = builder._build
58
+ self
59
+ end
60
+
61
+ def build
62
+ @mixin
63
+ end
64
+ end
65
+
66
+ class ApplicationBuilder
67
+
68
+ def initialize(name)
69
+ @application = AST::Application.new(name: name, services: {}, mix: [], mixins: {})
70
+ end
71
+
72
+ def mix(name)
73
+ @application.mix << name
74
+ end
75
+
76
+ def mixin(name, &block)
77
+ builder = CommonBuilder.new
78
+ builder.instance_eval(&block)
79
+ @application.mixins[name] = builder._build
80
+ self
81
+ end
82
+
83
+ def all(&block)
84
+ builder = AllBuilder.new
85
+ builder.instance_eval(&block)
86
+ @application.all = builder._build
87
+ self
88
+ end
89
+
90
+ def variables(&block)
91
+ builder = HashBuilder.new
92
+ builder.instance_eval(&block)
93
+ @application._variables = builder._build
94
+ self
95
+ end
96
+
97
+ def service(name, &block)
98
+ builder = ServiceBuilder.new(name)
99
+ builder.instance_eval(&block)
100
+ @application.services[name] = builder._build
101
+ self
102
+ end
103
+
104
+ def build
105
+ @application
106
+ end
107
+ end
108
+
109
+ class HashBuilder
110
+
111
+ def initialize
112
+ end
113
+
114
+ def method_missing(_, *values, &block)
115
+ if block_given?
116
+ builder = HashBuilder.new
117
+ builder.instance_eval(&block)
118
+ value = builder._build
119
+ if values.count == 1
120
+ @hash ||= {}
121
+ @hash[values.first.to_sym] = value
122
+ else
123
+ @hash ||= []
124
+ @hash << value
125
+ end
126
+ else
127
+ value = values.first
128
+ if value.is_a? Hash
129
+ @hash ||= {}
130
+ key, value = value.first
131
+ @hash[key.to_sym] = value
132
+ else
133
+ @hash ||= []
134
+ @hash << value
135
+ end
136
+ end
137
+ self
138
+ end
139
+
140
+ def _build
141
+ @hash || {}
142
+ end
143
+ end
144
+
145
+ class CommonBuilder
146
+
147
+ def initialize
148
+ @service = AST::Service.new(_mix: [])
149
+ end
150
+
151
+ def mix(name)
152
+ @service._mix << name
153
+ end
154
+
155
+ def method_missing(name, *values, &block)
156
+ if block_given?
157
+ builder = HashBuilder.new
158
+ builder.instance_eval(&block)
159
+ @service[name] = builder._build
160
+ else
161
+ @service[name] = values.first
162
+ end
163
+ end
164
+
165
+ def _build
166
+ @service
167
+ end
168
+
169
+ def variables(&block)
170
+ builder = HashBuilder.new
171
+ builder.instance_eval(&block)
172
+ @service._variables = builder._build
173
+ self
174
+ end
175
+ end
176
+
177
+ class AllBuilder < CommonBuilder
178
+ end
179
+
180
+ class ServiceBuilder < CommonBuilder
181
+
182
+ def initialize(name)
183
+ @service = AST::Service.new(name: name, _mix: [])
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,20 @@
1
+ require 'psych'
2
+ module Orchparty
3
+ module Generators
4
+ class DockerComposeV1
5
+ attr_reader :ast
6
+ def initialize(ast)
7
+ @ast = ast
8
+ end
9
+
10
+ def output(application_name)
11
+ Psych.dump(ast.applications[application_name].services.map do |name, service|
12
+ service = service.to_h
13
+ service.delete(:mix)
14
+ [service.delete(:name), HashUtils.deep_stringify_keys(service.to_h)]
15
+ end.to_h)
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ require 'yaml'
2
+ module Orchparty
3
+ module Generators
4
+ class DockerComposeV2
5
+ attr_reader :ast
6
+ def initialize(ast)
7
+ @ast = ast
8
+ end
9
+
10
+ def output(application_name)
11
+ {"version" => "2",
12
+ "services" =>
13
+ ast.applications[application_name].services.map do |name,service|
14
+ service = service.to_h
15
+ service.delete(:mix)
16
+ [service.delete(:name), HashUtils.deep_stringify_keys(service.to_h)]
17
+ end.to_h
18
+ }.to_yaml
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ require 'orchparty/generators/docker_compose_v1'
2
+ require 'orchparty/generators/docker_compose_v2'
3
+
4
+ module Orchparty
5
+ module Generators
6
+ end
7
+ end
@@ -0,0 +1,19 @@
1
+
2
+ module Orchparty
3
+ module Transformations
4
+ class All
5
+ def transform(ast)
6
+ ast.applications.each do |_, application|
7
+ application.services.transform_values! do |service|
8
+ if application.all.is_a?(Hash)
9
+ AST::Service.new(application.all.deep_merge(service))
10
+ else
11
+ service
12
+ end
13
+ end
14
+ end
15
+ ast
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,35 @@
1
+ require 'byebug'
2
+
3
+ module Orchparty
4
+ module Transformations
5
+ class Mixin
6
+ def transform(ast)
7
+ ast.applications.transform_values! do |application|
8
+ current = AST::Application.new
9
+ application.mix.each do |mixin_name|
10
+ mixin = application.mixins[mixin_name] || ast.mixins[mixin_name]
11
+ current = current.deep_merge_concat(mixin)
12
+ end
13
+ transform_application(current.deep_merge_concat(application), ast)
14
+ end
15
+ ast
16
+ end
17
+
18
+ def transform_application(application, ast)
19
+ application.services = application.services.transform_values! do |service|
20
+ current = AST::Service.new
21
+ service.delete(:_mix).each do |mixin|
22
+ if mixin.include? "."
23
+ mixin_name, mixin_service_name = mixin.split(".")
24
+ current = current.deep_merge_concat(ast.mixins[mixin_name].services[mixin_service_name])
25
+ else
26
+ current = current.deep_merge_concat(application.mixins[mixin])
27
+ end
28
+ end
29
+ current.deep_merge_concat(service)
30
+ end
31
+ application
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,16 @@
1
+
2
+ module Orchparty
3
+ module Transformations
4
+ class RemoveInternal
5
+ def transform(ast)
6
+ ast.applications.each do |_, application|
7
+ application.delete_if {|k, _| k.to_s.start_with?("_")}
8
+ application.services = application.services.each do |_, service|
9
+ service.delete_if {|k, _| k.to_s.start_with?("_")}
10
+ end
11
+ end
12
+ ast
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,38 @@
1
+
2
+ module Orchparty
3
+ module Transformations
4
+ class Variable
5
+ def transform(ast)
6
+ ast.applications.each do |_, application|
7
+ application.services = application.services.each do |_, service|
8
+ service.deep_transform_values! do |v|
9
+ if v.respond_to?(:call)
10
+ eval_value(build_context(application: application, service: service), v)
11
+ elsif v.is_a? Array
12
+ v.map do |v|
13
+ if v.respond_to?(:call)
14
+ eval_value(build_context(application: application, service: service), v)
15
+ else
16
+ v
17
+ end
18
+ end
19
+ else
20
+ v
21
+ end
22
+ end
23
+ end
24
+ end
25
+ ast
26
+ end
27
+
28
+ def eval_value(context, value)
29
+ context.instance_exec(&value)
30
+ end
31
+
32
+ def build_context(application:, service:)
33
+ variables = application._variables.merge(service._variables)
34
+ Context.new(variables.merge({application: application.merge(application._variables), service: service.merge(service._variables)}))
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,16 @@
1
+ require 'ostruct'
2
+ require 'orchparty/transformations/all'
3
+ require 'orchparty/transformations/variable'
4
+ require 'orchparty/transformations/mixin'
5
+ require 'orchparty/transformations/remove_internal'
6
+
7
+ module Orchparty
8
+ module Transformations
9
+ def self.transform(ast)
10
+ ast = Mixin.new.transform(ast)
11
+ ast = All.new.transform(ast)
12
+ ast = Variable.new.transform(ast)
13
+ ast = RemoveInternal.new.transform(ast)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ module Orchparty
2
+ VERSION = "0.1.0"
3
+ end
data/lib/orchparty.rb ADDED
@@ -0,0 +1,24 @@
1
+ require "deep_merge"
2
+ require "orchparty/version"
3
+ require "orchparty/ast"
4
+ require "orchparty/context"
5
+ require "orchparty/transformations"
6
+ require "orchparty/generators"
7
+ require "orchparty/dsl_parser"
8
+ require "hash"
9
+
10
+ module Orchparty
11
+
12
+
13
+ def self.ast(input_file)
14
+ Transformations.transform(Orchparty::DSLParser.new(input_file).parse)
15
+ end
16
+
17
+ def self.docker_compose_v1(input_file, application_name)
18
+ Orchparty::Generators::DockerComposeV1.new(ast(input_file)).output(application_name)
19
+ end
20
+
21
+ def self.docker_compose_v2(input_file, application_name)
22
+ Orchparty::Generators::DockerComposeV2.new(ast(input_file)).output(application_name)
23
+ end
24
+ end
data/orchparty.gemspec ADDED
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'orchparty/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "orchparty"
8
+ spec.version = Orchparty::VERSION
9
+ spec.authors = ["Jannis Huebl"]
10
+ spec.email = ["jannis.huebl@gmail.com"]
11
+
12
+ spec.summary = %q{Write your orchestration configuration with a Ruby DSL that allows you to have mixins, imports and variables.}
13
+ spec.description = <<-EOF
14
+ With this gem you can write docker-compose like orchestration configuration with a Ruby DSL, that supports mixins, imports and variables.!
15
+ Out of this you can generate docker-compose.yml v1 or v2.
16
+ EOF
17
+ spec.homepage = "https://orch.party"
18
+ spec.license = "LGPL-3.0"
19
+
20
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
21
+ f.match(%r{^(test|spec|features)/})
22
+ end
23
+ spec.bindir = "exe"
24
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
+ spec.require_paths = ["lib"]
26
+
27
+ spec.add_dependency "hashie", "~> 3.5.6"
28
+ spec.add_dependency "docile", "~> 1.1.5"
29
+ spec.add_dependency "thor", "~> 0.19.4"
30
+ spec.add_dependency 'psych', '~> 2.2', '>= 2.2.4'
31
+ spec.add_development_dependency "bundler", "~> 1.13"
32
+ spec.add_development_dependency "rake", "~> 10.0"
33
+ spec.add_development_dependency "rspec", "~> 3.0"
34
+ end