tfdsl 0.1.10 → 0.2.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +24 -21
  4. data/CLAUDE.md +86 -0
  5. data/Gemfile +7 -3
  6. data/README.md +144 -35
  7. data/Rakefile +4 -5
  8. data/bin/console +3 -9
  9. data/lib/terraform_dsl/action.rb +7 -0
  10. data/lib/terraform_dsl/block.rb +64 -0
  11. data/lib/terraform_dsl/check.rb +7 -0
  12. data/lib/terraform_dsl/data_source.rb +7 -0
  13. data/lib/terraform_dsl/ephemeral.rb +7 -0
  14. data/lib/terraform_dsl/formatter.rb +26 -0
  15. data/lib/terraform_dsl/import.rb +8 -0
  16. data/lib/terraform_dsl/json_syntax.rb +33 -0
  17. data/lib/terraform_dsl/locals.rb +7 -0
  18. data/lib/terraform_dsl/moved.rb +8 -0
  19. data/lib/terraform_dsl/output.rb +7 -0
  20. data/lib/terraform_dsl/provider.rb +7 -0
  21. data/lib/terraform_dsl/removed.rb +8 -0
  22. data/lib/terraform_dsl/resource.rb +7 -0
  23. data/lib/terraform_dsl/stack.rb +40 -0
  24. data/lib/terraform_dsl/template.rb +74 -0
  25. data/lib/terraform_dsl/terraform.rb +7 -0
  26. data/lib/terraform_dsl/tfmodule.rb +7 -0
  27. data/lib/terraform_dsl/variable.rb +8 -0
  28. data/lib/terraform_dsl/version.rb +5 -0
  29. data/lib/terraform_dsl.rb +34 -0
  30. data/lib/tfdsl.rb +5 -29
  31. data/tfdsl.gemspec +9 -28
  32. metadata +32 -155
  33. data/.travis.yml +0 -8
  34. data/Gemfile.lock +0 -73
  35. data/lib/tfdsl/block.rb +0 -80
  36. data/lib/tfdsl/data_formatter.rb +0 -63
  37. data/lib/tfdsl/data_source.rb +0 -4
  38. data/lib/tfdsl/formatter.rb +0 -77
  39. data/lib/tfdsl/kind_translator.rb +0 -20
  40. data/lib/tfdsl/locals.rb +0 -4
  41. data/lib/tfdsl/output.rb +0 -4
  42. data/lib/tfdsl/provider.rb +0 -4
  43. data/lib/tfdsl/resource.rb +0 -4
  44. data/lib/tfdsl/stack.rb +0 -36
  45. data/lib/tfdsl/template.rb +0 -46
  46. data/lib/tfdsl/terraform.rb +0 -4
  47. data/lib/tfdsl/tfmodule.rb +0 -4
  48. data/lib/tfdsl/variable.rb +0 -4
  49. data/lib/tfdsl/version.rb +0 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 188ff59f9e8c63a9d7123ed9900db3079a74b17f16926b11cebf3863db5cbf41
4
- data.tar.gz: 45d14091543dfe0e02512247dffbcf97550432715c357e2ab382c62482a9856d
3
+ metadata.gz: 53dc500c87b4a8775477610f94c90265c74a8b0fb065855c1f6914dca2dcbbaa
4
+ data.tar.gz: 5d5c9b8187223be0b79da605dde02e987ac0b7316c37aefdf046f86ec82170c0
5
5
  SHA512:
6
- metadata.gz: 71f58ea39f028ddb1dfc1ddf06388e02975bb0583bd3da4db9571e6ea14b10d8f60ec66b7bc19824417e106a3d961c52414b98913c7b265ad272d9aab3357b49
7
- data.tar.gz: 4dff89253f6ede3be10703f0a1a7ee41d43ffbeac17de45f69ee8c9b8fc7a187451ab5288ad37b6d2a8f5514a70ff87a9fd487cca46013e163263e631b194cb7
6
+ metadata.gz: a77d38c0cf2c1546a8bbf3fe37bc3a8c7383bb46fec86b7797cac517bba012738a5b180586fc4dd3d3c657f339d4b7c2296e55268145ec4ef44c38f1d4549642
7
+ data.tar.gz: efb9ed85d08976da942ea83b32851a0ff6f05b6d608bf848d8395d1d506daf2ff23d78b6cdfbb3bd878e6a2edbcde40f6314360543df1a5f2c9c0697765d34a1
data/.gitignore CHANGED
@@ -6,3 +6,4 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
+ /Gemfile.lock
data/.rubocop.yml CHANGED
@@ -1,27 +1,30 @@
1
- Style/ConditionalAssignment:
1
+ AllCops:
2
+ TargetRubyVersion: 2.7
3
+ NewCops: enable
4
+ SuggestExtensions: false
5
+
6
+ Style/Documentation:
2
7
  Enabled: false
3
- Layout/IndentArray:
4
- Enabled: false
5
- Style/ClassVars:
8
+
9
+ Layout/LineLength:
10
+ Max: 120
11
+
12
+ Metrics/BlockLength:
6
13
  Exclude:
7
- - lib/**/block.rb
8
- Metrics/AbcSize:
9
- Exclude:
10
- - lib/**/formatter.rb
11
- - lib/**/data_formatter.rb
12
- - lib/**/block.rb
14
+ - '*.gemspec'
15
+ - test/**/*
16
+
13
17
  Metrics/MethodLength:
14
- Max: 35
15
- Metrics/LineLength:
16
- Max: 120
17
18
  Exclude:
18
19
  - test/**/*
19
- Layout/ElseAlignment:
20
- Enabled: false
21
- Metrics/CyclomaticComplexity:
22
- Max: 10
23
- Metrics/PerceivedComplexity:
24
- Max: 10
25
- Metrics/BlockLength:
20
+
21
+ Style/ClassAndModuleChildren:
22
+ Exclude:
23
+ - test/**/*
24
+
25
+ Metrics/AbcSize:
26
+ Max: 20
27
+
28
+ Metrics:
26
29
  Exclude:
27
- - '*.gemspec'
30
+ - test/**/*
data/CLAUDE.md ADDED
@@ -0,0 +1,86 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What this is
6
+
7
+ `tfdsl` is a small Ruby gem (no runtime dependencies) that renders a Chef-like Ruby DSL as Terraform
8
+ HCL text. The output is meant to be byte-identical to what `terraform fmt` produces. The gem is
9
+ published as `tfdsl` (the name `terraform_dsl` on rubygems.org belongs to someone else); the code lives
10
+ under `lib/terraform_dsl/` in the `TerraformDSL` module, and `lib/tfdsl.rb` adds the legacy `TFDSL`
11
+ alias. Releases go out with `bundle exec rake release` after bumping `lib/terraform_dsl/version.rb`.
12
+
13
+ ## Commands
14
+
15
+ ```bash
16
+ bin/setup # bundle install
17
+ bundle exec rake # rubocop + full test suite (the default task, what CI runs)
18
+ bundle exec rake test # tests only
19
+ bundle exec rubocop # lint only (RuboCop 1.x, NewCops enabled, TargetRubyVersion 2.7)
20
+ bundle exec rubocop -a # autocorrect safe offenses
21
+
22
+ # Single test file / single test
23
+ bundle exec ruby -Ilib -Itest test/terraform_dsl/template_test.rb
24
+ bundle exec ruby -Ilib -Itest test/terraform_dsl/block_test.rb -n test_assignment_syntax_and_multiple_arguments
25
+
26
+ bin/console # IRB with the gem loaded
27
+ ```
28
+
29
+ SimpleCov enforces 95% minimum line coverage; the suite fails below that.
30
+
31
+ `test/terraform_dsl/terraform_test.rb` shells out to the real `terraform` binary (`fmt -check`,
32
+ `init -backend=false`, `validate`) and is skipped when `terraform` is not on the PATH. CI installs
33
+ Terraform, so locally install it too if you touch rendering. The validate fixture deliberately uses only
34
+ the builtin `terraform_data` resource so no provider download is needed.
35
+
36
+ `Gemfile.lock` is intentionally not committed. Dev dependencies live in the `Gemfile`, not the gemspec.
37
+
38
+ ## Architecture
39
+
40
+ HCL rendering flows `Stack -> Block -> Formatter -> Template`; JSON rendering flows
41
+ `Stack#to_h -> Block#to_h -> JsonSyntax`:
42
+
43
+ - **`Block`** (`lib/terraform_dsl/block.rb`) is the whole DSL. `method_missing` turns a call with
44
+ arguments into an entry in the ordered `@__attrs__` hash and a call with a Ruby block into a child
45
+ `Block` in `@__blocks__` (its name is `__type__`, extra args are `__labels__`). Attribute order is
46
+ declaration order. So that almost any HCL attribute name works, the class body **undefines every
47
+ inherited Object method except `KEEP`**, plus a few private Kernel methods (`format`, `test`,
48
+ `select`, `open`, `system`). Consequences: anything you need to call on a Block instance from
49
+ library or test code (e.g. `instance_of?` for Minitest's `assert_instance_of`) must be in `KEEP`;
50
+ and Block is a plain `Object`, not `BasicObject`, precisely so that `puts`, `raise` and top-level
51
+ helper methods still work inside DSL blocks.
52
+ - **Marker subclasses** (`Resource`, `Variable`, `Moved`, `Check`, `Action`, ...) each only set
53
+ `KEYWORD`. `Formatter.header` uses `block.class::KEYWORD` to decide top-level vs nested rendering:
54
+ top-level quotes `__type__` and labels after the keyword (`resource "aws_vpc" "main"`), nested
55
+ prints `__type__` bare and quotes only labels (`backend "local"`). Adding a Terraform top-level
56
+ block type = new 7-line subclass file + `require_relative` in `lib/terraform_dsl.rb` + an entry in
57
+ `Stack::BLOCKS`. Nested block kinds (`lifecycle`, `precondition`, `dynamic`, ...) need no code.
58
+ - **`Stack`** defines one method per `BLOCKS` entry and joins the blocks' `to_tf` with blank lines.
59
+ `tfmodule` exists because `module` is a Ruby keyword; `data`/`datasource` are aliases.
60
+ - **`Formatter`** (stateless module, depth passed as a parameter) renders a block's header,
61
+ attributes, children and closing brace with 2-space indentation.
62
+ - **`Template`** maps Ruby values to HCL: String -> quoted/escaped (heredoc `<<-EOT` when it ends
63
+ with `\n`), **Symbol -> raw expression** (this is how references, functions and types are
64
+ written), Numeric/true/false bare, nil -> `null`, Array/Hash recursive. A Hash argument is a map
65
+ *attribute* (`tags = {}`); a Ruby block is a nested *block*. `Template.assignments` reproduces
66
+ `terraform fmt`'s `=` alignment: consecutive single-line values share a column, a multi-line
67
+ `{`/`[` value breaks the run and is itself unaligned. Blank lines inside heredocs must stay empty
68
+ (Terraform ignores them when computing `<<-` indentation; padding them changes the value).
69
+ - **`JsonSyntax`** backs `Block#to_h`, `Stack#to_h` and `Stack#to_json` (Terraform JSON syntax, used by
70
+ consumers that write `.tf.json`). `JsonSyntax.value` wraps Symbols as `"${...}"`, except for attributes
71
+ listed in the block class's `STATIC_REFS` (`provider`, `depends_on`, `ignore_changes`, a variable's
72
+ `type`, `moved`/`import`/`removed` addresses), which JSON syntax requires as bare reference strings.
73
+ `terraform validate` rejects `${}` there, so extend `STATIC_REFS` when adding such an argument. `JsonSyntax.insert` nests a body under
74
+ `[keyword, type, *labels]` and turns a repeated path into an array, which is how repeated blocks
75
+ (`provider "aws"` twice, two `ingress` blocks) are expressed. `terraform_test.rb` validates a generated
76
+ `.tf.json` as well as the `.tf`.
77
+
78
+ ## Testing conventions
79
+
80
+ - `test/terraform_dsl/stack_test.rb` holds one golden `EXPECTED` string covering every block kind,
81
+ exposed as `StackTest.stack` so `terraform_test.rb` can run `terraform fmt -check` on the same
82
+ output. Any change to rendering must keep that fixture passing `fmt -check`; update `EXPECTED` to
83
+ the canonical `terraform fmt` form, never the other way round.
84
+ - Fixtures must be valid Terraform 0.12+ (e.g. `provisioner`, not `provider`, inside a resource).
85
+ - `alias` is a Ruby keyword; in the DSL it is written `self.alias = 'west'` (the `=` suffix is stripped
86
+ by `method_missing`).
data/Gemfile CHANGED
@@ -1,6 +1,10 @@
1
- source 'https://rubygems.org'
1
+ # frozen_string_literal: true
2
2
 
3
- git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
3
+ source 'https://rubygems.org'
4
4
 
5
- # Specify your gem's dependencies in tfdsl.gemspec
6
5
  gemspec
6
+
7
+ gem 'minitest', '~> 5.0'
8
+ gem 'rake', '~> 13.0'
9
+ gem 'rubocop', '~> 1.50'
10
+ gem 'simplecov', '~> 0.22'
data/README.md CHANGED
@@ -1,13 +1,10 @@
1
- [![Gem Version](https://badge.fury.io/rb/tfdsl.svg)](http://badge.fury.io/rb/tfdsl)
2
- [![Build Status](https://travis-ci.org/tlopo-ruby/tfdsl.svg?branch=master)](https://travis-ci.org/tlopo-ruby/tfdsl)
3
- [![Code Climate](https://codeclimate.com/github/tlopo-ruby/tfdsl/badges/gpa.svg)](https://codeclimate.com/github/tlopo-ruby/tfdsl)
4
- [![Coverage Status](https://coveralls.io/repos/github/tlopo-ruby/tfdsl/badge.svg?branch=master)](https://coveralls.io/github/tlopo-ruby/tfdsl?branch=master)
1
+ # tfdsl
5
2
 
6
- # TFDSL
3
+ A Chef like Ruby DSL that renders Terraform (HCL) configuration.
7
4
 
8
- A Chef like DSL for Terraform.
9
-
10
- Terraform uses HCL for its configuration, which is better than json or yaml in my opinion, but still not quite as flexible as Chef DSL, so here it's a chef like DSL for terraform.
5
+ HCL is a fine configuration language, but it is not a programming language. This gem lets
6
+ you describe Terraform blocks in Ruby, with loops, helper methods and everything else Ruby
7
+ gives you, and renders them as `terraform fmt` formatted HCL.
11
8
 
12
9
  ## Installation
13
10
 
@@ -23,68 +20,178 @@ And then execute:
23
20
 
24
21
  Or install it yourself as:
25
22
 
26
- $ gem install terraform_dsl
23
+ $ gem install tfdsl
27
24
 
28
25
  ## Usage
29
26
 
30
- A quick usage example
31
27
  ```ruby
32
- require 'tfdsl'
33
-
34
- stack = TFDSL.stack do
28
+ require 'terraform_dsl'
29
+
30
+ stack = TerraformDSL.stack do
35
31
  resource 'aws_vpc', 'main' do
36
32
  cidr_block '10.0.0.0/16'
37
33
  instance_tenancy 'dedicated'
38
- tags do
39
- Name 'main'
34
+ tags Name: 'main'
35
+ end
36
+
37
+ resource 'aws_subnet', 'public' do
38
+ vpc_id :'aws_vpc.main.id'
39
+ cidr_block :'cidrsubnet(aws_vpc.main.cidr_block, 8, 1)'
40
+ lifecycle do
41
+ precondition do
42
+ condition :'aws_vpc.main.enable_dns_support'
43
+ error_message 'DNS support must be enabled'
44
+ end
40
45
  end
41
46
  end
42
47
  end
43
48
 
44
49
  puts stack
45
50
  ```
46
- The code above will produce the output below:
51
+
52
+ The code above produces:
53
+
47
54
  ```hcl
48
55
  resource "aws_vpc" "main" {
49
- cidr_block = "10.0.0.0/16"
56
+ cidr_block = "10.0.0.0/16"
50
57
  instance_tenancy = "dedicated"
51
-
52
58
  tags = {
53
59
  Name = "main"
54
60
  }
55
61
  }
62
+
63
+ resource "aws_subnet" "public" {
64
+ vpc_id = aws_vpc.main.id
65
+ cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, 1)
66
+ lifecycle {
67
+ precondition {
68
+ condition = aws_vpc.main.enable_dns_support
69
+ error_message = "DNS support must be enabled"
70
+ }
71
+ }
72
+ }
73
+ ```
74
+
75
+ ### Rules
76
+
77
+ Inside a block, every method call is either an **attribute** (when given arguments) or a
78
+ **nested block** (when given a Ruby block). Nested blocks can carry labels, so
79
+ `backend 'local' do ... end` renders `backend "local" { ... }`. Any nested block Terraform
80
+ knows works this way: `lifecycle`, `precondition`, `postcondition`, `validation`, `dynamic`,
81
+ `content`, `provisioner`, `connection`, `assert`, `required_providers`, and so on.
82
+
83
+ Ruby values map to HCL values:
84
+
85
+ | Ruby | HCL |
86
+ |------------------------|---------------------------------------|
87
+ | `'text'` | `"text"`, escaped as needed |
88
+ | `"multi\nline\n"` | `<<-EOT` heredoc (string ends in `\n`)|
89
+ | `:'aws_vpc.main.id'` | raw expression `aws_vpc.main.id` |
90
+ | `:string`, `:'list(string)'` | type expression |
91
+ | `1`, `1.5`, `true` | `1`, `1.5`, `true` |
92
+ | `nil` | `null` |
93
+ | `[80, 443]` | `[80, 443]` list |
94
+ | `{ Name: 'main' }` | `{ Name = "main" }` map attribute |
95
+
96
+ So a Hash argument gives a map **attribute** (`tags = { ... }`) while a Ruby block gives a
97
+ nested **block** (`lifecycle { ... }`). Symbols are written verbatim, which is how you
98
+ reference resources, variables, functions and types.
99
+
100
+ ### Top level blocks
101
+
102
+ | DSL | HCL |
103
+ |--------------------------------------|-----------------------------------|
104
+ | `terraform do ... end` | `terraform { }` |
105
+ | `provider 'aws' do ... end` | `provider "aws" { }` |
106
+ | `variable 'name' do ... end` | `variable "name" { }` |
107
+ | `locals do ... end` | `locals { }` |
108
+ | `tfmodule 'vpc' do ... end` | `module "vpc" { }` |
109
+ | `data 'aws_ami', 'web' do ... end` | `data "aws_ami" "web" { }` |
110
+ | `resource 'aws_vpc', 'main' do ... end` | `resource "aws_vpc" "main" { }` |
111
+ | `output 'id' do ... end` | `output "id" { }` |
112
+ | `moved do ... end` | `moved { }` |
113
+ | `import do ... end` | `import { }` |
114
+ | `removed do ... end` | `removed { }` |
115
+ | `check 'health' do ... end` | `check "health" { }` |
116
+ | `ephemeral 'random_password', 'db' do ... end` | `ephemeral "random_password" "db" { }` |
117
+ | `action 'aws_lambda_invoke', 'x' do ... end` | `action "aws_lambda_invoke" "x" { }` |
118
+
119
+ `module` is a Ruby keyword, hence `tfmodule`. `datasource` is kept as an alias of `data`.
120
+
121
+ ### Using Ruby inside blocks
122
+
123
+ Blocks are evaluated with `instance_eval`, so local variables, constants and top level
124
+ helper methods are all available inside them:
125
+
126
+ ```ruby
127
+ def ami_for(region)
128
+ AMIS.fetch(region)
129
+ end
130
+
131
+ stack = TerraformDSL.stack do
132
+ REGIONS.each do |region|
133
+ resource 'aws_instance', "web_#{region}" do
134
+ ami ami_for(region)
135
+ instance_type 'm1.small'
136
+ end
137
+ end
138
+ end
56
139
  ```
57
140
 
58
- ## Ruby Reserved Words
141
+ Nearly every name works as an attribute, including Ruby method names such as `hash`,
142
+ `method`, `format` or `test`. The exceptions are `class`, `inspect`, `to_s` and a few other
143
+ basics a Ruby object needs, plus private Kernel methods that you would expect to keep working
144
+ inside a block, such as `puts`, `p`, `raise`, `require`, `lambda` or `loop`. Methods of a
145
+ surrounding class are not reachable from inside a block because `self` is the block.
146
+
147
+ Ruby keywords such as `alias` cannot be called bare, so use the assignment form:
148
+ `self.alias = 'west'`.
59
149
 
60
- Since some terraform object names use Ruby reserved words, we had to do some slight modifications on how resources are named.
61
- We use `datasource` for `data` and `tfmodule` for `module`
150
+ `require 'tfdsl'` also works and defines `TFDSL` as an alias of `TerraformDSL`, for code written
151
+ against tfdsl 0.1.x. Note that 0.2.0 changed the output format: values are typed, references are
152
+ written as Symbols, and `to_json` renders bare strings for static references.
62
153
 
63
- Example:
154
+ Each block class can also be used on its own:
64
155
 
65
- In ruby
66
156
  ```ruby
67
- datasource 'aws_ami', 'web' do
68
- ...
69
- end
157
+ TerraformDSL::Resource.new('aws_vpc', 'main') { cidr_block '10.0.0.0/16' }.to_tf
70
158
  ```
71
- In HCL
72
- ```hcl
73
- data 'aws_ami' 'web' {
74
- ...
75
- }
159
+
160
+ ### JSON output
161
+
162
+ A stack also renders in
163
+ [Terraform JSON syntax](https://developer.hashicorp.com/terraform/language/syntax/json),
164
+ for writing a `.tf.json` file:
165
+
166
+ ```ruby
167
+ stack.to_json # pretty printed JSON string
168
+ stack.to_h # the same as a Hash
76
169
  ```
77
170
 
171
+ Symbols become `"${...}"` template strings, except where JSON syntax wants a plain reference
172
+ string: a variable's `type`, `provider`, `providers`, `depends_on`, `ignore_changes`,
173
+ `replace_triggered_by`, and the addresses in `moved`, `import` and `removed` blocks. Repeated
174
+ blocks, such as two `provider "aws"` blocks or two `ingress` blocks, become arrays.
78
175
 
79
176
  ## Development
80
177
 
81
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
178
+ After checking out the repo, run `bin/setup` to install dependencies. Then run `rake` to
179
+ run RuboCop and the tests. You can also run `bin/console` for an interactive prompt.
180
+
181
+ When the `terraform` binary is on the PATH the test suite also checks the generated
182
+ configuration with `terraform fmt -check` and `terraform validate`.
82
183
 
83
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
184
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new
185
+ version, update the version number in `version.rb`, and then run `bundle exec rake release`,
186
+ which will create a git tag for the version, push git commits and tags, and push the `.gem`
187
+ file to [rubygems.org](https://rubygems.org).
84
188
 
85
189
  ## Contributing
86
190
 
87
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/terraform_dsl. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
191
+ Bug reports and pull requests are welcome on GitHub at
192
+ https://github.com/tlopo-ruby/terraform-dsl. This project is intended to be a safe, welcoming
193
+ space for collaboration, and contributors are expected to adhere to the
194
+ [Contributor Covenant](http://contributor-covenant.org) code of conduct.
88
195
 
89
196
  ## License
90
197
 
@@ -92,4 +199,6 @@ The gem is available as open source under the terms of the [MIT License](https:/
92
199
 
93
200
  ## Code of Conduct
94
201
 
95
- Everyone interacting in the TerraformDsl project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/terraform_dsl/blob/master/CODE_OF_CONDUCT.md).
202
+ Everyone interacting in the TerraformDSL project's codebases, issue trackers, chat rooms and
203
+ mailing lists is expected to follow the
204
+ [code of conduct](https://github.com/tlopo-ruby/terraform-dsl/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile CHANGED
@@ -1,7 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/gem_tasks'
2
4
  require 'rake/testtask'
3
5
  require 'rubocop/rake_task'
4
- require 'coveralls/rake/task'
5
6
 
6
7
  Rake::TestTask.new(:test) do |t|
7
8
  t.libs << 'test'
@@ -9,8 +10,6 @@ Rake::TestTask.new(:test) do |t|
9
10
  t.test_files = FileList['test/**/*_test.rb']
10
11
  end
11
12
 
12
- task default: %i[rubocop test]
13
- task test_with_coveralls: [:default, 'coveralls:push']
14
-
15
13
  RuboCop::RakeTask.new
16
- Coveralls::RakeTask.new
14
+
15
+ task default: %i[rubocop test]
data/bin/console CHANGED
@@ -1,14 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'bundler/setup'
4
- require 'tfdsl'
5
-
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
5
+ require 'terraform_dsl'
13
6
  require 'irb'
7
+
14
8
  IRB.start(__FILE__)
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TerraformDSL
4
+ class Action < Block
5
+ KEYWORD = 'action'
6
+ end
7
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TerraformDSL
4
+ # A terraform configuration block: https://developer.hashicorp.com/terraform/language/syntax/configuration
5
+ #
6
+ # Inside the DSL block every bare method call becomes either an attribute
7
+ # (when called with arguments) or a nested block (when called with a Ruby
8
+ # block). Subclasses set KEYWORD to the top-level block keyword.
9
+ class Block
10
+ KEYWORD = nil
11
+ # Arguments that take static references, which JSON syntax wants as plain
12
+ # strings rather than "${...}" expressions. Subclasses extend the list.
13
+ STATIC_REFS = %w[provider providers depends_on ignore_changes replace_triggered_by].freeze
14
+
15
+ # Ruby methods a Block keeps. Everything else Object provides is undefined
16
+ # so that it can be used as an attribute name (class, hash, method, ...).
17
+ KEEP = %i[instance_eval instance_exec class is_a? kind_of? instance_of? respond_to? object_id
18
+ __send__ __id__ equal? == != ! inspect to_s freeze frozen? nil?].freeze
19
+ (instance_methods - KEEP).each { |m| undef_method m }
20
+ # Private Kernel methods that are plausible attribute names.
21
+ %i[format test select open system].each { |m| undef_method m }
22
+
23
+ attr_accessor :__type__, :__labels__
24
+ attr_reader :__attrs__, :__blocks__
25
+
26
+ def initialize(type = nil, *labels, &block)
27
+ @__type__ = type
28
+ @__labels__ = labels
29
+ @__attrs__ = {}
30
+ @__blocks__ = []
31
+ instance_eval(&block) if block
32
+ end
33
+
34
+ def method_missing(name, *args, &block)
35
+ name = name.to_s.chomp('=')
36
+ return (@__blocks__ << Block.new(name, *args, &block)).last if block
37
+ return @__attrs__[name] if args.empty?
38
+
39
+ @__attrs__[name] = args.size == 1 ? args.first : args
40
+ end
41
+
42
+ def respond_to_missing?(_name, _include_private = false)
43
+ false
44
+ end
45
+
46
+ def to_tf
47
+ "#{Formatter.format(self)}\n"
48
+ end
49
+
50
+ # The block body in Terraform JSON syntax: attributes plus nested blocks
51
+ # keyed by type and labels.
52
+ def to_h
53
+ body = @__attrs__.to_h do |name, value|
54
+ [name, JsonSyntax.value(value, static: self.class::STATIC_REFS.include?(name))]
55
+ end
56
+ @__blocks__.each { |child| JsonSyntax.insert(body, [child.__type__, *child.__labels__], child.to_h) }
57
+ body
58
+ end
59
+
60
+ def to_s
61
+ to_tf
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TerraformDSL
4
+ class Check < Block
5
+ KEYWORD = 'check'
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TerraformDSL
4
+ class DataSource < Block
5
+ KEYWORD = 'data'
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TerraformDSL
4
+ class Ephemeral < Block
5
+ KEYWORD = 'ephemeral'
6
+ end
7
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TerraformDSL
4
+ # Renders a Block (and its nested blocks) as HCL text.
5
+ module Formatter
6
+ module_function
7
+
8
+ def format(block, depth = 0)
9
+ pad = INDENT * depth
10
+ lines = ["#{pad}#{header(block)} {"]
11
+ attrs = block.__attrs__.map { |name, value| [name, Template.value(value, depth + 1)] }
12
+ lines.concat Template.assignments(attrs, pad + INDENT)
13
+ block.__blocks__.each { |child| lines << format(child, depth + 1) }
14
+ lines << "#{pad}}"
15
+ lines.join("\n")
16
+ end
17
+
18
+ # Top level blocks: resource "aws_vpc" "main"
19
+ # Nested blocks: backend "local"
20
+ def header(block)
21
+ keyword = block.class::KEYWORD
22
+ name, *labels = keyword ? [keyword, block.__type__, *block.__labels__] : [block.__type__, *block.__labels__]
23
+ [name, *labels.compact.map { |label| Template.string(label.to_s) }].join(' ')
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TerraformDSL
4
+ class Import < Block
5
+ KEYWORD = 'import'
6
+ STATIC_REFS = (Block::STATIC_REFS + %w[to]).freeze
7
+ end
8
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TerraformDSL
4
+ # Helpers for rendering blocks in Terraform JSON syntax:
5
+ # https://developer.hashicorp.com/terraform/language/syntax/json
6
+ module JsonSyntax
7
+ module_function
8
+
9
+ # Symbols (raw HCL expressions) become "${...}" template strings, or bare
10
+ # strings for static references such as depends_on or a variable's type.
11
+ def value(obj, static: false)
12
+ case obj
13
+ when Hash then obj.to_h { |k, v| [k.to_s, value(v, static: static)] }
14
+ when Array then obj.map { |v| value(v, static: static) }
15
+ when Symbol then static ? obj.to_s : "${#{obj}}"
16
+ else obj
17
+ end
18
+ end
19
+
20
+ # Stores body under the nested keys. A repeated key turns into an array of
21
+ # bodies, which is how JSON syntax represents repeated blocks.
22
+ def insert(hash, keys, body)
23
+ key, *rest = keys.map(&:to_s)
24
+ return insert(hash[key] ||= {}, rest, body) unless rest.empty?
25
+
26
+ hash[key] = case (existing = hash[key])
27
+ when nil then body
28
+ when Array then existing + [body]
29
+ else [existing, body]
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TerraformDSL
4
+ class Locals < Block
5
+ KEYWORD = 'locals'
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TerraformDSL
4
+ class Moved < Block
5
+ KEYWORD = 'moved'
6
+ STATIC_REFS = (Block::STATIC_REFS + %w[from to]).freeze
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TerraformDSL
4
+ class Output < Block
5
+ KEYWORD = 'output'
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TerraformDSL
4
+ class Provider < Block
5
+ KEYWORD = 'provider'
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TerraformDSL
4
+ class Removed < Block
5
+ KEYWORD = 'removed'
6
+ STATIC_REFS = (Block::STATIC_REFS + %w[from]).freeze
7
+ end
8
+ end