parent
09339bf500
commit
87c4bf1df9
@ -0,0 +1,2 @@
|
||||
README.html
|
||||
coverage.out
|
@ -0,0 +1,70 @@
|
||||
language: go
|
||||
sudo: false
|
||||
go:
|
||||
- 1.8
|
||||
- 1.7.5
|
||||
- 1.7.4
|
||||
- 1.7.3
|
||||
- 1.7.2
|
||||
- 1.7.1
|
||||
- 1.7
|
||||
- tip
|
||||
- 1.6.4
|
||||
- 1.6.3
|
||||
- 1.6.2
|
||||
- 1.6.1
|
||||
- 1.6
|
||||
- 1.5.4
|
||||
- 1.5.3
|
||||
- 1.5.2
|
||||
- 1.5.1
|
||||
- 1.5
|
||||
- 1.4.3
|
||||
- 1.4.2
|
||||
- 1.4.1
|
||||
- 1.4
|
||||
- 1.3.3
|
||||
- 1.3.2
|
||||
- 1.3.1
|
||||
- 1.3
|
||||
- 1.2.2
|
||||
- 1.2.1
|
||||
- 1.2
|
||||
- 1.1.2
|
||||
- 1.1.1
|
||||
- 1.1
|
||||
before_install:
|
||||
- go get github.com/mattn/goveralls
|
||||
script:
|
||||
- $HOME/gopath/bin/goveralls -service=travis-ci
|
||||
notifications:
|
||||
email:
|
||||
on_success: never
|
||||
matrix:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- go: tip
|
||||
- go: 1.6.4
|
||||
- go: 1.6.3
|
||||
- go: 1.6.2
|
||||
- go: 1.6.1
|
||||
- go: 1.6
|
||||
- go: 1.5.4
|
||||
- go: 1.5.3
|
||||
- go: 1.5.2
|
||||
- go: 1.5.1
|
||||
- go: 1.5
|
||||
- go: 1.4.3
|
||||
- go: 1.4.2
|
||||
- go: 1.4.1
|
||||
- go: 1.4
|
||||
- go: 1.3.3
|
||||
- go: 1.3.2
|
||||
- go: 1.3.1
|
||||
- go: 1.3
|
||||
- go: 1.2.2
|
||||
- go: 1.2.1
|
||||
- go: 1.2
|
||||
- go: 1.1.2
|
||||
- go: 1.1.1
|
||||
- go: 1.1
|
@ -0,0 +1,36 @@
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
660 York Street, Suite 102,
|
||||
San Francisco, CA 94110 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
@ -0,0 +1 @@
|
||||
Alex Bucataru <alex@alrux.com> (@AlexBucataru)
|
@ -0,0 +1,5 @@
|
||||
Alrux Go EXTensions (AGExt) - package levenshtein
|
||||
Copyright 2016 ALRUX Inc.
|
||||
|
||||
This product includes software developed at ALRUX Inc.
|
||||
(http://www.alrux.com/).
|
@ -0,0 +1,290 @@
|
||||
// Copyright 2016 ALRUX Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/*
|
||||
Package levenshtein implements distance and similarity metrics for strings, based on the Levenshtein measure.
|
||||
|
||||
The Levenshtein `Distance` between two strings is the minimum total cost of edits that would convert the first string into the second. The allowed edit operations are insertions, deletions, and substitutions, all at character (one UTF-8 code point) level. Each operation has a default cost of 1, but each can be assigned its own cost equal to or greater than 0.
|
||||
|
||||
A `Distance` of 0 means the two strings are identical, and the higher the value the more different the strings. Since in practice we are interested in finding if the two strings are "close enough", it often does not make sense to continue the calculation once the result is mathematically guaranteed to exceed a desired threshold. Providing this value to the `Distance` function allows it to take a shortcut and return a lower bound instead of an exact cost when the threshold is exceeded.
|
||||
|
||||
The `Similarity` function calculates the distance, then converts it into a normalized metric within the range 0..1, with 1 meaning the strings are identical, and 0 that they have nothing in common. A minimum similarity threshold can be provided to speed up the calculation of the metric for strings that are far too dissimilar for the purpose at hand. All values under this threshold are rounded down to 0.
|
||||
|
||||
The `Match` function provides a similarity metric, with the same range and meaning as `Similarity`, but with a bonus for string pairs that share a common prefix and have a similarity above a "bonus threshold". It uses the same method as proposed by Winkler for the Jaro distance, and the reasoning behind it is that these string pairs are very likely spelling variations or errors, and they are more closely linked than the edit distance alone would suggest.
|
||||
|
||||
The underlying `Calculate` function is also exported, to allow the building of other derivative metrics, if needed.
|
||||
*/
|
||||
package levenshtein
|
||||
|
||||
// Calculate determines the Levenshtein distance between two strings, using
|
||||
// the given costs for each edit operation. It returns the distance along with
|
||||
// the lengths of the longest common prefix and suffix.
|
||||
//
|
||||
// If maxCost is non-zero, the calculation stops as soon as the distance is determined
|
||||
// to be greater than maxCost. Therefore, any return value higher than maxCost is a
|
||||
// lower bound for the actual distance.
|
||||
func Calculate(str1, str2 []rune, maxCost, insCost, subCost, delCost int) (dist, prefixLen, suffixLen int) {
|
||||
l1, l2 := len(str1), len(str2)
|
||||
// trim common prefix, if any, as it doesn't affect the distance
|
||||
for ; prefixLen < l1 && prefixLen < l2; prefixLen++ {
|
||||
if str1[prefixLen] != str2[prefixLen] {
|
||||
break
|
||||
}
|
||||
}
|
||||
str1, str2 = str1[prefixLen:], str2[prefixLen:]
|
||||
l1 -= prefixLen
|
||||
l2 -= prefixLen
|
||||
// trim common suffix, if any, as it doesn't affect the distance
|
||||
for 0 < l1 && 0 < l2 {
|
||||
if str1[l1-1] != str2[l2-1] {
|
||||
str1, str2 = str1[:l1], str2[:l2]
|
||||
break
|
||||
}
|
||||
l1--
|
||||
l2--
|
||||
suffixLen++
|
||||
}
|
||||
// if the first string is empty, the distance is the length of the second string times the cost of insertion
|
||||
if l1 == 0 {
|
||||
dist = l2 * insCost
|
||||
return
|
||||
}
|
||||
// if the second string is empty, the distance is the length of the first string times the cost of deletion
|
||||
if l2 == 0 {
|
||||
dist = l1 * delCost
|
||||
return
|
||||
}
|
||||
|
||||
// variables used in inner "for" loops
|
||||
var y, dy, c, l int
|
||||
|
||||
// if maxCost is greater than or equal to the maximum possible distance, it's equivalent to 'unlimited'
|
||||
if maxCost > 0 {
|
||||
if subCost < delCost+insCost {
|
||||
if maxCost >= l1*subCost+(l2-l1)*insCost {
|
||||
maxCost = 0
|
||||
}
|
||||
} else {
|
||||
if maxCost >= l1*delCost+l2*insCost {
|
||||
maxCost = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if maxCost > 0 {
|
||||
// prefer the longer string first, to minimize time;
|
||||
// a swap also transposes the meanings of insertion and deletion.
|
||||
if l1 < l2 {
|
||||
str1, str2, l1, l2, insCost, delCost = str2, str1, l2, l1, delCost, insCost
|
||||
}
|
||||
|
||||
// the length differential times cost of deletion is a lower bound for the cost;
|
||||
// if it is higher than the maxCost, there is no point going into the main calculation.
|
||||
if dist = (l1 - l2) * delCost; dist > maxCost {
|
||||
return
|
||||
}
|
||||
|
||||
d := make([]int, l1+1)
|
||||
|
||||
// offset and length of d in the current row
|
||||
doff, dlen := 0, 1
|
||||
for y, dy = 1, delCost; y <= l1 && dy <= maxCost; dlen++ {
|
||||
d[y] = dy
|
||||
y++
|
||||
dy = y * delCost
|
||||
}
|
||||
// fmt.Printf("%q -> %q: init doff=%d dlen=%d d[%d:%d]=%v\n", str1, str2, doff, dlen, doff, doff+dlen, d[doff:doff+dlen])
|
||||
|
||||
for x := 0; x < l2; x++ {
|
||||
dy, d[doff] = d[doff], d[doff]+insCost
|
||||
for d[doff] > maxCost && dlen > 0 {
|
||||
if str1[doff] != str2[x] {
|
||||
dy += subCost
|
||||
}
|
||||
doff++
|
||||
dlen--
|
||||
if c = d[doff] + insCost; c < dy {
|
||||
dy = c
|
||||
}
|
||||
dy, d[doff] = d[doff], dy
|
||||
}
|
||||
for y, l = doff, doff+dlen-1; y < l; dy, d[y] = d[y], dy {
|
||||
if str1[y] != str2[x] {
|
||||
dy += subCost
|
||||
}
|
||||
if c = d[y] + delCost; c < dy {
|
||||
dy = c
|
||||
}
|
||||
y++
|
||||
if c = d[y] + insCost; c < dy {
|
||||
dy = c
|
||||
}
|
||||
}
|
||||
if y < l1 {
|
||||
if str1[y] != str2[x] {
|
||||
dy += subCost
|
||||
}
|
||||
if c = d[y] + delCost; c < dy {
|
||||
dy = c
|
||||
}
|
||||
for ; dy <= maxCost && y < l1; dy, d[y] = dy+delCost, dy {
|
||||
y++
|
||||
dlen++
|
||||
}
|
||||
}
|
||||
// fmt.Printf("%q -> %q: x=%d doff=%d dlen=%d d[%d:%d]=%v\n", str1, str2, x, doff, dlen, doff, doff+dlen, d[doff:doff+dlen])
|
||||
if dlen == 0 {
|
||||
dist = maxCost + 1
|
||||
return
|
||||
}
|
||||
}
|
||||
if doff+dlen-1 < l1 {
|
||||
dist = maxCost + 1
|
||||
return
|
||||
}
|
||||
dist = d[l1]
|
||||
} else {
|
||||
// ToDo: This is O(l1*l2) time and O(min(l1,l2)) space; investigate if it is
|
||||
// worth to implement diagonal approach - O(l1*(1+dist)) time, up to O(l1*l2) space
|
||||
// http://www.csse.monash.edu.au/~lloyd/tildeStrings/Alignment/92.IPL.html
|
||||
|
||||
// prefer the shorter string first, to minimize space; time is O(l1*l2) anyway;
|
||||
// a swap also transposes the meanings of insertion and deletion.
|
||||
if l1 > l2 {
|
||||
str1, str2, l1, l2, insCost, delCost = str2, str1, l2, l1, delCost, insCost
|
||||
}
|
||||
d := make([]int, l1+1)
|
||||
|
||||
for y = 1; y <= l1; y++ {
|
||||
d[y] = y * delCost
|
||||
}
|
||||
for x := 0; x < l2; x++ {
|
||||
dy, d[0] = d[0], d[0]+insCost
|
||||
for y = 0; y < l1; dy, d[y] = d[y], dy {
|
||||
if str1[y] != str2[x] {
|
||||
dy += subCost
|
||||
}
|
||||
if c = d[y] + delCost; c < dy {
|
||||
dy = c
|
||||
}
|
||||
y++
|
||||
if c = d[y] + insCost; c < dy {
|
||||
dy = c
|
||||
}
|
||||
}
|
||||
}
|
||||
dist = d[l1]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Distance returns the Levenshtein distance between str1 and str2, using the
|
||||
// default or provided cost values. Pass nil for the third argument to use the
|
||||
// default cost of 1 for all three operations, with no maximum.
|
||||
func Distance(str1, str2 string, p *Params) int {
|
||||
if p == nil {
|
||||
p = defaultParams
|
||||
}
|
||||
dist, _, _ := Calculate([]rune(str1), []rune(str2), p.maxCost, p.insCost, p.subCost, p.delCost)
|
||||
return dist
|
||||
}
|
||||
|
||||
// Similarity returns a score in the range of 0..1 for how similar the two strings are.
|
||||
// A score of 1 means the strings are identical, and 0 means they have nothing in common.
|
||||
//
|
||||
// A nil third argument uses the default cost of 1 for all three operations.
|
||||
//
|
||||
// If a non-zero MinScore value is provided in the parameters, scores lower than it
|
||||
// will be returned as 0.
|
||||
func Similarity(str1, str2 string, p *Params) float64 {
|
||||
return Match(str1, str2, p.Clone().BonusThreshold(1.1)) // guaranteed no bonus
|
||||
}
|
||||
|
||||
// Match returns a similarity score adjusted by the same method as proposed by Winkler for
|
||||
// the Jaro distance - giving a bonus to string pairs that share a common prefix, only if their
|
||||
// similarity score is already over a threshold.
|
||||
//
|
||||
// The score is in the range of 0..1, with 1 meaning the strings are identical,
|
||||
// and 0 meaning they have nothing in common.
|
||||
//
|
||||
// A nil third argument uses the default cost of 1 for all three operations, maximum length of
|
||||
// common prefix to consider for bonus of 4, scaling factor of 0.1, and bonus threshold of 0.7.
|
||||
//
|
||||
// If a non-zero MinScore value is provided in the parameters, scores lower than it
|
||||
// will be returned as 0.
|
||||
func Match(str1, str2 string, p *Params) float64 {
|
||||
s1, s2 := []rune(str1), []rune(str2)
|
||||
l1, l2 := len(s1), len(s2)
|
||||
// two empty strings are identical; shortcut also avoids divByZero issues later on.
|
||||
if l1 == 0 && l2 == 0 {
|
||||
return 1
|
||||
}
|
||||
|
||||
if p == nil {
|
||||
p = defaultParams
|
||||
}
|
||||
|
||||
// a min over 1 can never be satisfied, so the score is 0.
|
||||
if p.minScore > 1 {
|
||||
return 0
|
||||
}
|
||||
|
||||
insCost, delCost, maxDist, max := p.insCost, p.delCost, 0, 0
|
||||
if l1 > l2 {
|
||||
l1, l2, insCost, delCost = l2, l1, delCost, insCost
|
||||
}
|
||||
|
||||
if p.subCost < delCost+insCost {
|
||||
maxDist = l1*p.subCost + (l2-l1)*insCost
|
||||
} else {
|
||||
maxDist = l1*delCost + l2*insCost
|
||||
}
|
||||
|
||||
// a zero min is always satisfied, so no need to set a max cost.
|
||||
if p.minScore > 0 {
|
||||
// if p.minScore is lower than p.bonusThreshold, we can use a simplified formula
|
||||
// for the max cost, because a sim score below min cannot receive a bonus.
|
||||
if p.minScore < p.bonusThreshold {
|
||||
// round down the max - a cost equal to a rounded up max would already be under min.
|
||||
max = int((1 - p.minScore) * float64(maxDist))
|
||||
} else {
|
||||
// p.minScore <= sim + p.bonusPrefix*p.bonusScale*(1-sim)
|
||||
// p.minScore <= (1-dist/maxDist) + p.bonusPrefix*p.bonusScale*(1-(1-dist/maxDist))
|
||||
// p.minScore <= 1 - dist/maxDist + p.bonusPrefix*p.bonusScale*dist/maxDist
|
||||
// 1 - p.minScore >= dist/maxDist - p.bonusPrefix*p.bonusScale*dist/maxDist
|
||||
// (1-p.minScore)*maxDist/(1-p.bonusPrefix*p.bonusScale) >= dist
|
||||
max = int((1 - p.minScore) * float64(maxDist) / (1 - float64(p.bonusPrefix)*p.bonusScale))
|
||||
}
|
||||
}
|
||||
|
||||
dist, pl, _ := Calculate(s1, s2, max, p.insCost, p.subCost, p.delCost)
|
||||
if max > 0 && dist > max {
|
||||
return 0
|
||||
}
|
||||
sim := 1 - float64(dist)/float64(maxDist)
|
||||
|
||||
if sim >= p.bonusThreshold && sim < 1 && p.bonusPrefix > 0 && p.bonusScale > 0 {
|
||||
if pl > p.bonusPrefix {
|
||||
pl = p.bonusPrefix
|
||||
}
|
||||
sim += float64(pl) * p.bonusScale * (1 - sim)
|
||||
}
|
||||
|
||||
if sim < p.minScore {
|
||||
return 0
|
||||
}
|
||||
|
||||
return sim
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
// Copyright 2016 ALRUX Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package levenshtein
|
||||
|
||||
// Params represents a set of parameter values for the various formulas involved
|
||||
// in the calculation of the Levenshtein string metrics.
|
||||
type Params struct {
|
||||
insCost int
|
||||
subCost int
|
||||
delCost int
|
||||
maxCost int
|
||||
minScore float64
|
||||
bonusPrefix int
|
||||
bonusScale float64
|
||||
bonusThreshold float64
|
||||
}
|
||||
|
||||
var (
|
||||
defaultParams = NewParams()
|
||||
)
|
||||
|
||||
// NewParams creates a new set of parameters and initializes it with the default values.
|
||||
func NewParams() *Params {
|
||||
return &Params{
|
||||
insCost: 1,
|
||||
subCost: 1,
|
||||
delCost: 1,
|
||||
maxCost: 0,
|
||||
minScore: 0,
|
||||
bonusPrefix: 4,
|
||||
bonusScale: .1,
|
||||
bonusThreshold: .7,
|
||||
}
|
||||
}
|
||||
|
||||
// Clone returns a pointer to a copy of the receiver parameter set, or of a new
|
||||
// default parameter set if the receiver is nil.
|
||||
func (p *Params) Clone() *Params {
|
||||
if p == nil {
|
||||
return NewParams()
|
||||
}
|
||||
return &Params{
|
||||
insCost: p.insCost,
|
||||
subCost: p.subCost,
|
||||
delCost: p.delCost,
|
||||
maxCost: p.maxCost,
|
||||
minScore: p.minScore,
|
||||
bonusPrefix: p.bonusPrefix,
|
||||
bonusScale: p.bonusScale,
|
||||
bonusThreshold: p.bonusThreshold,
|
||||
}
|
||||
}
|
||||
|
||||
// InsCost overrides the default value of 1 for the cost of insertion.
|
||||
// The new value must be zero or positive.
|
||||
func (p *Params) InsCost(v int) *Params {
|
||||
if v >= 0 {
|
||||
p.insCost = v
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// SubCost overrides the default value of 1 for the cost of substitution.
|
||||
// The new value must be zero or positive.
|
||||
func (p *Params) SubCost(v int) *Params {
|
||||
if v >= 0 {
|
||||
p.subCost = v
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// DelCost overrides the default value of 1 for the cost of deletion.
|
||||
// The new value must be zero or positive.
|
||||
func (p *Params) DelCost(v int) *Params {
|
||||
if v >= 0 {
|
||||
p.delCost = v
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// MaxCost overrides the default value of 0 (meaning unlimited) for the maximum cost.
|
||||
// The calculation of Distance() stops when the result is guaranteed to exceed
|
||||
// this maximum, returning a lower-bound rather than exact value.
|
||||
// The new value must be zero or positive.
|
||||
func (p *Params) MaxCost(v int) *Params {
|
||||
if v >= 0 {
|
||||
p.maxCost = v
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// MinScore overrides the default value of 0 for the minimum similarity score.
|
||||
// Scores below this threshold are returned as 0 by Similarity() and Match().
|
||||
// The new value must be zero or positive. Note that a minimum greater than 1
|
||||
// can never be satisfied, resulting in a score of 0 for any pair of strings.
|
||||
func (p *Params) MinScore(v float64) *Params {
|
||||
if v >= 0 {
|
||||
p.minScore = v
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// BonusPrefix overrides the default value for the maximum length of
|
||||
// common prefix to be considered for bonus by Match().
|
||||
// The new value must be zero or positive.
|
||||
func (p *Params) BonusPrefix(v int) *Params {
|
||||
if v >= 0 {
|
||||
p.bonusPrefix = v
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// BonusScale overrides the default value for the scaling factor used by Match()
|
||||
// in calculating the bonus.
|
||||
// The new value must be zero or positive. To guarantee that the similarity score
|
||||
// remains in the interval 0..1, this scaling factor is not allowed to exceed
|
||||
// 1 / BonusPrefix.
|
||||
func (p *Params) BonusScale(v float64) *Params {
|
||||
if v >= 0 {
|
||||
p.bonusScale = v
|
||||
}
|
||||
|
||||
// the bonus cannot exceed (1-sim), or the score may become greater than 1.
|
||||
if float64(p.bonusPrefix)*p.bonusScale > 1 {
|
||||
p.bonusScale = 1 / float64(p.bonusPrefix)
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// BonusThreshold overrides the default value for the minimum similarity score
|
||||
// for which Match() can assign a bonus.
|
||||
// The new value must be zero or positive. Note that a threshold greater than 1
|
||||
// effectively makes Match() become the equivalent of Similarity().
|
||||
func (p *Params) BonusThreshold(v float64) *Params {
|
||||
if v >= 0 {
|
||||
p.bonusThreshold = v
|
||||
}
|
||||
return p
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
Copyright (c) 2017 Martin Atkins
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---------
|
||||
|
||||
Unicode table generation programs are under a separate copyright and license:
|
||||
|
||||
Copyright (c) 2014 Couchbase, Inc.
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
|
||||
except in compliance with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the
|
||||
License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
||||
either express or implied. See the License for the specific language governing permissions
|
||||
and limitations under the License.
|
||||
|
||||
---------
|
||||
|
||||
Grapheme break data is provided as part of the Unicode character database,
|
||||
copright 2016 Unicode, Inc, which is provided with the following license:
|
||||
|
||||
Unicode Data Files include all data files under the directories
|
||||
http://www.unicode.org/Public/, http://www.unicode.org/reports/,
|
||||
http://www.unicode.org/cldr/data/, http://source.icu-project.org/repos/icu/, and
|
||||
http://www.unicode.org/utility/trac/browser/.
|
||||
|
||||
Unicode Data Files do not include PDF online code charts under the
|
||||
directory http://www.unicode.org/Public/.
|
||||
|
||||
Software includes any source code published in the Unicode Standard
|
||||
or under the directories
|
||||
http://www.unicode.org/Public/, http://www.unicode.org/reports/,
|
||||
http://www.unicode.org/cldr/data/, http://source.icu-project.org/repos/icu/, and
|
||||
http://www.unicode.org/utility/trac/browser/.
|
||||
|
||||
NOTICE TO USER: Carefully read the following legal agreement.
|
||||
BY DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING UNICODE INC.'S
|
||||
DATA FILES ("DATA FILES"), AND/OR SOFTWARE ("SOFTWARE"),
|
||||
YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
|
||||
TERMS AND CONDITIONS OF THIS AGREEMENT.
|
||||
IF YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE
|
||||
THE DATA FILES OR SOFTWARE.
|
||||
|
||||
COPYRIGHT AND PERMISSION NOTICE
|
||||
|
||||
Copyright © 1991-2017 Unicode, Inc. All rights reserved.
|
||||
Distributed under the Terms of Use in http://www.unicode.org/copyright.html.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Unicode data files and any associated documentation
|
||||
(the "Data Files") or Unicode software and any associated documentation
|
||||
(the "Software") to deal in the Data Files or Software
|
||||
without restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, and/or sell copies of
|
||||
the Data Files or Software, and to permit persons to whom the Data Files
|
||||
or Software are furnished to do so, provided that either
|
||||
(a) this copyright and permission notice appear with all copies
|
||||
of the Data Files or Software, or
|
||||
(b) this copyright and permission notice appear in associated
|
||||
Documentation.
|
||||
|
||||
THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
||||
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT OF THIRD PARTY RIGHTS.
|
||||
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS
|
||||
NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL
|
||||
DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
|
||||
DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THE DATA FILES OR SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the name of a copyright holder
|
||||
shall not be used in advertising or otherwise to promote the sale,
|
||||
use or other dealings in these Data Files or Software without prior
|
||||
written authorization of the copyright holder.
|
@ -0,0 +1,30 @@
|
||||
package textseg
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
)
|
||||
|
||||
// AllTokens is a utility that uses a bufio.SplitFunc to produce a slice of
|
||||
// all of the recognized tokens in the given buffer.
|
||||
func AllTokens(buf []byte, splitFunc bufio.SplitFunc) ([][]byte, error) {
|
||||
scanner := bufio.NewScanner(bytes.NewReader(buf))
|
||||
scanner.Split(splitFunc)
|
||||
var ret [][]byte
|
||||
for scanner.Scan() {
|
||||
ret = append(ret, scanner.Bytes())
|
||||
}
|
||||
return ret, scanner.Err()
|
||||
}
|
||||
|
||||
// TokenCount is a utility that uses a bufio.SplitFunc to count the number of
|
||||
// recognized tokens in the given buffer.
|
||||
func TokenCount(buf []byte, splitFunc bufio.SplitFunc) (int, error) {
|
||||
scanner := bufio.NewScanner(bytes.NewReader(buf))
|
||||
scanner.Split(splitFunc)
|
||||
var ret int
|
||||
for scanner.Scan() {
|
||||
ret++
|
||||
}
|
||||
return ret, scanner.Err()
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package textseg
|
||||
|
||||
//go:generate go run make_tables.go -output tables.go
|
||||
//go:generate go run make_test_tables.go -output tables_test.go
|
||||
//go:generate ruby unicode2ragel.rb --url=https://www.unicode.org/Public/12.0.0/ucd/auxiliary/GraphemeBreakProperty.txt -m GraphemeCluster -p "Prepend,CR,LF,Control,Extend,Regional_Indicator,SpacingMark,L,V,T,LV,LVT,ZWJ" -o grapheme_clusters_table.rl
|
||||
//go:generate ruby unicode2ragel.rb --url=https://www.unicode.org/Public/emoji/12.0/emoji-data.txt -m Emoji -p "Extended_Pictographic" -o emoji_table.rl
|
||||
//go:generate ragel -Z grapheme_clusters.rl
|
||||
//go:generate gofmt -w grapheme_clusters.go
|
4078
vendor/github.com/apparentlymart/go-textseg/v12/textseg/grapheme_clusters.go
generated
vendored
4078
vendor/github.com/apparentlymart/go-textseg/v12/textseg/grapheme_clusters.go
generated
vendored
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,133 @@
|
||||
package textseg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Generated from grapheme_clusters.rl. DO NOT EDIT
|
||||
%%{
|
||||
# (except you are actually in grapheme_clusters.rl here, so edit away!)
|
||||
|
||||
machine graphclust;
|
||||
write data;
|
||||
}%%
|
||||
|
||||
var Error = errors.New("invalid UTF8 text")
|
||||
|
||||
// ScanGraphemeClusters is a split function for bufio.Scanner that splits
|
||||
// on grapheme cluster boundaries.
|
||||
func ScanGraphemeClusters(data []byte, atEOF bool) (int, []byte, error) {
|
||||
if len(data) == 0 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
// Ragel state
|
||||
cs := 0 // Current State
|
||||
p := 0 // "Pointer" into data
|
||||
pe := len(data) // End-of-data "pointer"
|
||||
ts := 0
|
||||
te := 0
|
||||
act := 0
|
||||
eof := pe
|
||||
|
||||
// Make Go compiler happy
|
||||
_ = ts
|
||||
_ = te
|
||||
_ = act
|
||||
_ = eof
|
||||
|
||||
startPos := 0
|
||||
endPos := 0
|
||||
|
||||
%%{
|
||||
include GraphemeCluster "grapheme_clusters_table.rl";
|
||||
include Emoji "emoji_table.rl";
|
||||
|
||||
action start {
|
||||
startPos = p
|
||||
}
|
||||
|
||||
action end {
|
||||
endPos = p
|
||||
}
|
||||
|
||||
action emit {
|
||||
return endPos+1, data[startPos:endPos+1], nil
|
||||
}
|
||||
|
||||
ZWJGlue = ZWJ (Extended_Pictographic Extend*)?;
|
||||
AnyExtender = Extend | ZWJGlue | SpacingMark;
|
||||
Extension = AnyExtender*;
|
||||
ReplacementChar = (0xEF 0xBF 0xBD);
|
||||
|
||||
CRLFSeq = CR LF;
|
||||
ControlSeq = Control | ReplacementChar;
|
||||
HangulSeq = (
|
||||
L+ (((LV? V+ | LVT) T*)?|LV?) |
|
||||
LV V* T* |
|
||||
V+ T* |
|
||||
LVT T* |
|
||||
T+
|
||||
) Extension;
|
||||
EmojiSeq = Extended_Pictographic Extend* Extension;
|
||||
ZWJSeq = ZWJ (ZWJ | Extend | SpacingMark)*;
|
||||
EmojiFlagSeq = Regional_Indicator Regional_Indicator? Extension;
|
||||
|
||||
UTF8Cont = 0x80 .. 0xBF;
|
||||
AnyUTF8 = (
|
||||
0x00..0x7F |
|
||||
0xC0..0xDF . UTF8Cont |
|
||||
0xE0..0xEF . UTF8Cont . UTF8Cont |
|
||||
0xF0..0xF7 . UTF8Cont . UTF8Cont . UTF8Cont
|
||||
);
|
||||
|
||||
# OtherSeq is any character that isn't at the start of one of the extended sequences above, followed by extension
|
||||
OtherSeq = (AnyUTF8 - (CR|LF|Control|ReplacementChar|L|LV|V|LVT|T|Extended_Pictographic|ZWJ|Regional_Indicator|Prepend)) (Extend | ZWJ | SpacingMark)*;
|
||||
|
||||
# PrependSeq is prepend followed by any of the other patterns above, except control characters which explicitly break
|
||||
PrependSeq = Prepend+ (HangulSeq|EmojiSeq|ZWJSeq|EmojiFlagSeq|OtherSeq)?;
|
||||
|
||||
CRLFTok = CRLFSeq >start @end;
|
||||
ControlTok = ControlSeq >start @end;
|
||||
HangulTok = HangulSeq >start @end;
|
||||
EmojiTok = EmojiSeq >start @end;
|
||||
ZWJTok = ZWJSeq >start @end;
|
||||
EmojiFlagTok = EmojiFlagSeq >start @end;
|
||||
OtherTok = OtherSeq >start @end;
|
||||
PrependTok = PrependSeq >start @end;
|
||||
|
||||
main := |*
|
||||
CRLFTok => emit;
|
||||
ControlTok => emit;
|
||||
HangulTok => emit;
|
||||
EmojiTok => emit;
|
||||
ZWJTok => emit;
|
||||
EmojiFlagTok => emit;
|
||||
PrependTok => emit;
|
||||
OtherTok => emit;
|
||||
|
||||
# any single valid UTF-8 character would also be valid per spec,
|
||||
# but we'll handle that separately after the loop so we can deal
|
||||
# with requesting more bytes if we're not at EOF.
|
||||
*|;
|
||||
|
||||
write init;
|
||||
write exec;
|
||||
}%%
|
||||
|
||||
// If we fall out here then we were unable to complete a sequence.
|
||||
// If we weren't able to complete a sequence then either we've
|
||||
// reached the end of a partial buffer (so there's more data to come)
|
||||
// or we have an isolated symbol that would normally be part of a
|
||||
// grapheme cluster but has appeared in isolation here.
|
||||
|
||||
if !atEOF {
|
||||
// Request more
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
// Just take the first UTF-8 sequence and return that.
|
||||
_, seqLen := utf8.DecodeRune(data)
|
||||
return seqLen, data[:seqLen], nil
|
||||
}
|
1589
vendor/github.com/apparentlymart/go-textseg/v12/textseg/grapheme_clusters_table.rl
generated
vendored
1589
vendor/github.com/apparentlymart/go-textseg/v12/textseg/grapheme_clusters_table.rl
generated
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,335 @@
|
||||
#!/usr/bin/env ruby
|
||||
#
|
||||
# This scripted has been updated to accept more command-line arguments:
|
||||
#
|
||||
# -u, --url URL to process
|
||||
# -m, --machine Machine name
|
||||
# -p, --properties Properties to add to the machine
|
||||
# -o, --output Write output to file
|
||||
#
|
||||
# Updated by: Marty Schoch <marty.schoch@gmail.com>
|
||||
#
|
||||
# This script uses the unicode spec to generate a Ragel state machine
|
||||
# that recognizes unicode alphanumeric characters. It generates 5
|
||||
# character classes: uupper, ulower, ualpha, udigit, and ualnum.
|
||||
# Currently supported encodings are UTF-8 [default] and UCS-4.
|
||||
#
|
||||
# Usage: unicode2ragel.rb [options]
|
||||
# -e, --encoding [ucs4 | utf8] Data encoding
|
||||
# -h, --help Show this message
|
||||
#
|
||||
# This script was originally written as part of the Ferret search
|
||||
# engine library.
|
||||
#
|
||||
# Author: Rakan El-Khalil <rakan@well.com>
|
||||
|
||||
require 'optparse'
|
||||
require 'open-uri'
|
||||
|
||||
ENCODINGS = [ :utf8, :ucs4 ]
|
||||
ALPHTYPES = { :utf8 => "byte", :ucs4 => "rune" }
|
||||
DEFAULT_CHART_URL = "http://www.unicode.org/Public/5.1.0/ucd/DerivedCoreProperties.txt"
|
||||
DEFAULT_MACHINE_NAME= "WChar"
|
||||
|
||||
###
|
||||
# Display vars & default option
|
||||
|
||||
TOTAL_WIDTH = 80
|
||||
RANGE_WIDTH = 23
|
||||
@encoding = :utf8
|
||||
@chart_url = DEFAULT_CHART_URL
|
||||
machine_name = DEFAULT_MACHINE_NAME
|
||||
properties = []
|
||||
@output = $stdout
|
||||
|
||||
###
|
||||
# Option parsing
|
||||
|
||||
cli_opts = OptionParser.new do |opts|
|
||||
opts.on("-e", "--encoding [ucs4 | utf8]", "Data encoding") do |o|
|
||||
@encoding = o.downcase.to_sym
|
||||
end
|
||||
opts.on("-h", "--help", "Show this message") do
|
||||
puts opts
|
||||
exit
|
||||
end
|
||||
opts.on("-u", "--url URL", "URL to process") do |o|
|
||||
@chart_url = o
|
||||
end
|
||||
opts.on("-m", "--machine MACHINE_NAME", "Machine name") do |o|
|
||||
machine_name = o
|
||||
end
|
||||
opts.on("-p", "--properties x,y,z", Array, "Properties to add to machine") do |o|
|
||||
properties = o
|
||||
end
|
||||
opts.on("-o", "--output FILE", "output file") do |o|
|
||||
@output = File.new(o, "w+")
|
||||
end
|
||||
end
|
||||
|
||||
cli_opts.parse(ARGV)
|
||||
unless ENCODINGS.member? @encoding
|
||||
puts "Invalid encoding: #{@encoding}"
|
||||
puts cli_opts
|
||||
exit
|
||||
end
|
||||
|
||||
##
|
||||
# Downloads the document at url and yields every alpha line's hex
|
||||
# range and description.
|
||||
|
||||
def each_alpha( url, property )
|
||||
open( url ) do |file|
|
||||
file.each_line do |line|
|
||||
next if line =~ /^#/;
|
||||
next if line !~ /; #{property} *#/;
|
||||
|
||||
range, description = line.split(/;/)
|
||||
range.strip!
|
||||
description.gsub!(/.*#/, '').strip!
|
||||
|
||||
if range =~ /\.\./
|
||||
start, stop = range.split '..'
|
||||
else start = stop = range
|
||||
end
|
||||
|
||||
yield start.hex .. stop.hex, description
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
###
|
||||
# Formats to hex at minimum width
|
||||
|
||||
def to_hex( n )
|
||||
r = "%0X" % n
|
||||
r = "0#{r}" unless (r.length % 2).zero?
|
||||
r
|
||||
end
|
||||
|
||||
###
|
||||
# UCS4 is just a straight hex conversion of the unicode codepoint.
|
||||
|
||||
def to_ucs4( range )
|
||||
rangestr = "0x" + to_hex(range.begin)
|
||||
rangestr << "..0x" + to_hex(range.end) if range.begin != range.end
|
||||
[ rangestr ]
|
||||
end
|
||||
|
||||
##
|
||||
# 0x00 - 0x7f -> 0zzzzzzz[7]
|
||||
# 0x80 - 0x7ff -> 110yyyyy[5] 10zzzzzz[6]
|
||||
# 0x800 - 0xffff -> 1110xxxx[4] 10yyyyyy[6] 10zzzzzz[6]
|
||||
# 0x010000 - 0x10ffff -> 11110www[3] 10xxxxxx[6] 10yyyyyy[6] 10zzzzzz[6]
|
||||
|
||||
UTF8_BOUNDARIES = [0x7f, 0x7ff, 0xffff, 0x10ffff]
|
||||
|
||||
def to_utf8_enc( n )
|
||||
r = 0
|
||||
if n <= 0x7f
|
||||
r = n
|
||||
elsif n <= 0x7ff
|
||||
y = 0xc0 | (n >> 6)
|
||||
z = 0x80 | (n & 0x3f)
|
||||
r = y << 8 | z
|
||||
elsif n <= 0xffff
|
||||
x = 0xe0 | (n >> 12)
|
||||
y = 0x80 | (n >> 6) & 0x3f
|
||||
z = 0x80 | n & 0x3f
|
||||
r = x << 16 | y << 8 | z
|
||||
elsif n <= 0x10ffff
|
||||
w = 0xf0 | (n >> 18)
|
||||
x = 0x80 | (n >> 12) & 0x3f
|
||||
y = 0x80 | (n >> 6) & 0x3f
|
||||
z = 0x80 | n & 0x3f
|
||||
r = w << 24 | x << 16 | y << 8 | z
|
||||
end
|
||||
|
||||
to_hex(r)
|
||||
end
|
||||
|
||||
def from_utf8_enc( n )
|
||||
n = n.hex
|
||||
r = 0
|
||||
if n <= 0x7f
|
||||
r = n
|
||||
elsif n <= 0xdfff
|
||||
y = (n >> 8) & 0x1f
|
||||
z = n & 0x3f
|
||||
r = y << 6 | z
|
||||
elsif n <= 0xefffff
|
||||
x = (n >> 16) & 0x0f
|
||||
y = (n >> 8) & 0x3f
|
||||
z = n & 0x3f
|
||||
r = x << 10 | y << 6 | z
|
||||
elsif n <= 0xf7ffffff
|
||||
w = (n >> 24) & 0x07
|
||||
x = (n >> 16) & 0x3f
|
||||
y = (n >> 8) & 0x3f
|
||||
z = n & 0x3f
|
||||
r = w << 18 | x << 12 | y << 6 | z
|
||||
end
|
||||
r
|
||||
end
|
||||
|
||||
###
|
||||
# Given a range, splits it up into ranges that can be continuously
|
||||
# encoded into utf8. Eg: 0x00 .. 0xff => [0x00..0x7f, 0x80..0xff]
|
||||
# This is not strictly needed since the current [5.1] unicode standard
|
||||
# doesn't have ranges that straddle utf8 boundaries. This is included
|
||||
# for completeness as there is no telling if that will ever change.
|
||||
|
||||
def utf8_ranges( range )
|
||||
ranges = []
|
||||
UTF8_BOUNDARIES.each do |max|
|
||||
if range.begin <= max
|
||||
if range.end <= max
|
||||
ranges << range
|
||||
return ranges
|
||||
end
|
||||
|
||||
ranges << (range.begin .. max)
|
||||
range = (max + 1) .. range.end
|
||||
end
|
||||
end
|
||||
ranges
|
||||
end
|
||||
|
||||
def build_range( start, stop )
|
||||
size = start.size/2
|
||||
left = size - 1
|
||||
return [""] if size < 1
|
||||
|
||||
a = start[0..1]
|
||||
b = stop[0..1]
|
||||
|
||||
###
|
||||
# Shared prefix
|
||||
|
||||
if a == b
|
||||
return build_range(start[2..-1], stop[2..-1]).map do |elt|
|
||||
"0x#{a} " + elt
|
||||
end
|
||||
end
|
||||
|
||||
###
|
||||
# Unshared prefix, end of run
|
||||
|
||||
return ["0x#{a}..0x#{b} "] if left.zero?
|
||||
|
||||
###
|
||||
# Unshared prefix, not end of run
|
||||
# Range can be 0x123456..0x56789A
|
||||
# Which is equivalent to:
|
||||
# 0x123456 .. 0x12FFFF
|
||||
# 0x130000 .. 0x55FFFF
|
||||
# 0x560000 .. 0x56789A
|
||||
|
||||
ret = []
|
||||
ret << build_range(start, a + "FF" * left)
|
||||
|
||||
###
|
||||
# Only generate middle range if need be.
|
||||
|
||||
if a.hex+1 != b.hex
|
||||
max = to_hex(b.hex - 1)
|
||||
max = "FF" if b == "FF"
|
||||
ret << "0x#{to_hex(a.hex+1)}..0x#{max} " + "0x00..0xFF " * left
|
||||
end
|
||||
|
||||
###
|
||||
# Don't generate last range if it is covered by first range
|
||||
|
||||
ret << build_range(b + "00" * left, stop) unless b == "FF"
|
||||
ret.flatten!
|
||||
end
|
||||
|
||||
def to_utf8( range )
|
||||
utf8_ranges( range ).map do |r|
|
||||
begin_enc = to_utf8_enc(r.begin)
|
||||
end_enc = to_utf8_enc(r.end)
|
||||
build_range begin_enc, end_enc
|
||||
end.flatten!
|
||||
end
|
||||
|
||||
##
|
||||
# Perform a 3-way comparison of the number of codepoints advertised by
|
||||
# the unicode spec for the given range, the originally parsed range,
|
||||
# and the resulting utf8 encoded range.
|
||||
|
||||
def count_codepoints( code )
|
||||
code.split(' ').inject(1) do |acc, elt|
|
||||
if elt =~ /0x(.+)\.\.0x(.+)/
|
||||
if @encoding == :utf8
|
||||
acc * (from_utf8_enc($2) - from_utf8_enc($1) + 1)
|
||||
else
|
||||
acc * ($2.hex - $1.hex + 1)
|
||||
end
|
||||
else
|
||||
acc
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def is_valid?( range, desc, codes )
|
||||
spec_count = 1
|
||||
spec_count = $1.to_i if desc =~ /\[(\d+)\]/
|
||||
range_count = range.end - range.begin + 1
|
||||
|
||||
sum = codes.inject(0) { |acc, elt| acc + count_codepoints(elt) }
|
||||
sum == spec_count and sum == range_count
|
||||
end
|
||||
|
||||
##
|
||||
# Generate the state maching to stdout
|
||||
|
||||
def generate_machine( name, property )
|
||||
pipe = " "
|
||||
@output.puts " #{name} = "
|
||||
each_alpha( @chart_url, property ) do |range, desc|
|
||||
|
||||
codes = (@encoding == :ucs4) ? to_ucs4(range) : to_utf8(range)
|
||||
|
||||
#raise "Invalid encoding of range #{range}: #{codes.inspect}" unless
|
||||
# is_valid? range, desc, codes
|
||||
|
||||
range_width = codes.map { |a| a.size }.max
|
||||
range_width = RANGE_WIDTH if range_width < RANGE_WIDTH
|
||||
|
||||
desc_width = TOTAL_WIDTH - RANGE_WIDTH - 11
|
||||
desc_width -= (range_width - RANGE_WIDTH) if range_width > RANGE_WIDTH
|
||||
|
||||
if desc.size > desc_width
|
||||
desc = desc[0..desc_width - 4] + "..."
|
||||
end
|
||||
|
||||
codes.each_with_index do |r, idx|
|
||||
desc = "" unless idx.zero?
|
||||
code = "%-#{range_width}s" % r
|
||||
@output.puts " #{pipe} #{code} ##{desc}"
|
||||
pipe = "|"
|
||||
end
|
||||
end
|
||||
@output.puts " ;"
|
||||
@output.puts ""
|
||||
end
|
||||
|
||||
@output.puts <<EOF
|
||||
# The following Ragel file was autogenerated with #{$0}
|
||||
# from: #{@chart_url}
|
||||
#
|
||||
# It defines #{properties}.
|
||||
#
|
||||
# To use this, make sure that your alphtype is set to #{ALPHTYPES[@encoding]},
|
||||
# and that your input is in #{@encoding}.
|
||||
|
||||
%%{
|
||||
machine #{machine_name};
|
||||
|
||||
EOF
|
||||
|
||||
properties.each { |x| generate_machine( x, x ) }
|
||||
|
||||
@output.puts <<EOF
|
||||
}%%
|
||||
EOF
|
@ -0,0 +1,19 @@
|
||||
package textseg
|
||||
|
||||
import "unicode/utf8"
|
||||
|
||||
// ScanGraphemeClusters is a split function for bufio.Scanner that splits
|
||||
// on UTF8 sequence boundaries.
|
||||
//
|
||||
// This is included largely for completeness, since this behavior is already
|
||||
// built in to Go when ranging over a string.
|
||||
func ScanUTF8Sequences(data []byte, atEOF bool) (int, []byte, error) {
|
||||
if len(data) == 0 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
r, seqLen := utf8.DecodeRune(data)
|
||||
if r == utf8.RuneError && !atEOF {
|
||||
return 0, nil, nil
|
||||
}
|
||||
return seqLen, data[:seqLen], nil
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
y.output
|
||||
|
||||
# ignore intellij files
|
||||
.idea
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
*.test
|
@ -1,13 +0,0 @@
|
||||
sudo: false
|
||||
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.x
|
||||
- tip
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
|
||||
script: make test
|
@ -1,18 +0,0 @@
|
||||
TEST?=./...
|
||||
|
||||
default: test
|
||||
|
||||
fmt: generate
|
||||
go fmt ./...
|
||||
|
||||
test: generate
|
||||
go get -t ./...
|
||||
go test $(TEST) $(TESTARGS)
|
||||
|
||||
generate:
|
||||
go generate ./...
|
||||
|
||||
updatedeps:
|
||||
go get -u golang.org/x/tools/cmd/stringer
|
||||
|
||||
.PHONY: default generate test updatedeps
|
@ -1,125 +0,0 @@
|
||||
# HCL
|
||||
|
||||
[![GoDoc](https://godoc.org/github.com/hashicorp/hcl?status.png)](https://godoc.org/github.com/hashicorp/hcl) [![Build Status](https://travis-ci.org/hashicorp/hcl.svg?branch=master)](https://travis-ci.org/hashicorp/hcl)
|
||||
|
||||
HCL (HashiCorp Configuration Language) is a configuration language built
|
||||
by HashiCorp. The goal of HCL is to build a structured configuration language
|
||||
that is both human and machine friendly for use with command-line tools, but
|
||||
specifically targeted towards DevOps tools, servers, etc.
|
||||
|
||||
HCL is also fully JSON compatible. That is, JSON can be used as completely
|
||||
valid input to a system expecting HCL. This helps makes systems
|
||||
interoperable with other systems.
|
||||
|
||||
HCL is heavily inspired by
|
||||
[libucl](https://github.com/vstakhov/libucl),
|
||||
nginx configuration, and others similar.
|
||||
|
||||
## Why?
|
||||
|
||||
A common question when viewing HCL is to ask the question: why not
|
||||
JSON, YAML, etc.?
|
||||
|
||||
Prior to HCL, the tools we built at [HashiCorp](http://www.hashicorp.com)
|
||||
used a variety of configuration languages from full programming languages
|
||||
such as Ruby to complete data structure languages such as JSON. What we
|
||||
learned is that some people wanted human-friendly configuration languages
|
||||
and some people wanted machine-friendly languages.
|
||||
|
||||
JSON fits a nice balance in this, but is fairly verbose and most
|
||||
importantly doesn't support comments. With YAML, we found that beginners
|
||||
had a really hard time determining what the actual structure was, and
|
||||
ended up guessing more often than not whether to use a hyphen, colon, etc.
|
||||
in order to represent some configuration key.
|
||||
|
||||
Full programming languages such as Ruby enable complex behavior
|
||||
a configuration language shouldn't usually allow, and also forces
|
||||
people to learn some set of Ruby.
|
||||
|
||||
Because of this, we decided to create our own configuration language
|
||||
that is JSON-compatible. Our configuration language (HCL) is designed
|
||||
to be written and modified by humans. The API for HCL allows JSON
|
||||
as an input so that it is also machine-friendly (machines can generate
|
||||
JSON instead of trying to generate HCL).
|
||||
|
||||
Our goal with HCL is not to alienate other configuration languages.
|
||||
It is instead to provide HCL as a specialized language for our tools,
|
||||
and JSON as the interoperability layer.
|
||||
|
||||
## Syntax
|
||||
|
||||
For a complete grammar, please see the parser itself. A high-level overview
|
||||
of the syntax and grammar is listed here.
|
||||
|
||||
* Single line comments start with `#` or `//`
|
||||
|
||||
* Multi-line comments are wrapped in `/*` and `*/`. Nested block comments
|
||||
are not allowed. A multi-line comment (also known as a block comment)
|
||||
terminates at the first `*/` found.
|
||||
|
||||
* Values are assigned with the syntax `key = value` (whitespace doesn't
|
||||
matter). The value can be any primitive: a string, number, boolean,
|
||||
object, or list.
|
||||
|
||||
* Strings are double-quoted and can contain any UTF-8 characters.
|
||||
Example: `"Hello, World"`
|
||||
|
||||
* Multi-line strings start with `<<EOF` at the end of a line, and end
|
||||
with `EOF` on its own line ([here documents](https://en.wikipedia.org/wiki/Here_document)).
|
||||
Any text may be used in place of `EOF`. Example:
|
||||
```
|
||||
<<FOO
|
||||
hello
|
||||
world
|
||||
FOO
|
||||
```
|
||||
|
||||
* Numbers are assumed to be base 10. If you prefix a number with 0x,
|
||||
it is treated as a hexadecimal. If it is prefixed with 0, it is
|
||||
treated as an octal. Numbers can be in scientific notation: "1e10".
|
||||
|
||||
* Boolean values: `true`, `false`
|
||||
|
||||
* Arrays can be made by wrapping it in `[]`. Example:
|
||||
`["foo", "bar", 42]`. Arrays can contain primitives,
|
||||
other arrays, and objects. As an alternative, lists
|
||||
of objects can be created with repeated blocks, using
|
||||
this structure:
|
||||
|
||||
```hcl
|
||||
service {
|
||||
key = "value"
|
||||
}
|
||||
|
||||
service {
|
||||
key = "value"
|
||||
}
|
||||
```
|
||||
|
||||
Objects and nested objects are created using the structure shown below:
|
||||
|
||||
```
|
||||
variable "ami" {
|
||||
description = "the AMI to use"
|
||||
}
|
||||
```
|
||||
This would be equivalent to the following json:
|
||||
``` json
|
||||
{
|
||||
"variable": {
|
||||
"ami": {
|
||||
"description": "the AMI to use"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Thanks
|
||||
|
||||
Thanks to:
|
||||
|
||||
* [@vstakhov](https://github.com/vstakhov) - The original libucl parser
|
||||
and syntax that HCL was based off of.
|
||||
|
||||
* [@fatih](https://github.com/fatih) - The rewritten HCL parser
|
||||
in pure Go (no goyacc) and support for a printer.
|
@ -1,19 +0,0 @@
|
||||
version: "build-{branch}-{build}"
|
||||
image: Visual Studio 2015
|
||||
clone_folder: c:\gopath\src\github.com\hashicorp\hcl
|
||||
environment:
|
||||
GOPATH: c:\gopath
|
||||
init:
|
||||
- git config --global core.autocrlf false
|
||||
install:
|
||||
- cmd: >-
|
||||
echo %Path%
|
||||
|
||||
go version
|
||||
|
||||
go env
|
||||
|
||||
go get -t ./...
|
||||
|
||||
build_script:
|
||||
- cmd: go test -v ./...
|
@ -1,729 +0,0 @@
|
||||
package hcl
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hcl/hcl/ast"
|
||||
"github.com/hashicorp/hcl/hcl/parser"
|
||||
"github.com/hashicorp/hcl/hcl/token"
|
||||
)
|
||||
|
||||
// This is the tag to use with structures to have settings for HCL
|
||||
const tagName = "hcl"
|
||||
|
||||
var (
|
||||
// nodeType holds a reference to the type of ast.Node
|
||||
nodeType reflect.Type = findNodeType()
|
||||
)
|
||||
|
||||
// Unmarshal accepts a byte slice as input and writes the
|
||||
// data to the value pointed to by v.
|
||||
func Unmarshal(bs []byte, v interface{}) error {
|
||||
root, err := parse(bs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return DecodeObject(v, root)
|
||||
}
|
||||
|
||||
// Decode reads the given input and decodes it into the structure
|
||||
// given by `out`.
|
||||
func Decode(out interface{}, in string) error {
|
||||
obj, err := Parse(in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return DecodeObject(out, obj)
|
||||
}
|
||||
|
||||
// DecodeObject is a lower-level version of Decode. It decodes a
|
||||
// raw Object into the given output.
|
||||
func DecodeObject(out interface{}, n ast.Node) error {
|
||||
val := reflect.ValueOf(out)
|
||||
if val.Kind() != reflect.Ptr {
|
||||
return errors.New("result must be a pointer")
|
||||
}
|
||||
|
||||
// If we have the file, we really decode the root node
|
||||
if f, ok := n.(*ast.File); ok {
|
||||
n = f.Node
|
||||
}
|
||||
|
||||
var d decoder
|
||||
return d.decode("root", n, val.Elem())
|
||||
}
|
||||
|
||||
type decoder struct {
|
||||
stack []reflect.Kind
|
||||
}
|
||||
|
||||
func (d *decoder) decode(name string, node ast.Node, result reflect.Value) error {
|
||||
k := result
|
||||
|
||||
// If we have an interface with a valid value, we use that
|
||||
// for the check.
|
||||
if result.Kind() == reflect.Interface {
|
||||
elem := result.Elem()
|
||||
if elem.IsValid() {
|
||||
k = elem
|
||||
}
|
||||
}
|
||||
|
||||
// Push current onto stack unless it is an interface.
|
||||
if k.Kind() != reflect.Interface {
|
||||
d.stack = append(d.stack, k.Kind())
|
||||
|
||||
// Schedule a pop
|
||||
defer func() {
|
||||
d.stack = d.stack[:len(d.stack)-1]
|
||||
}()
|
||||
}
|
||||
|
||||
switch k.Kind() {
|
||||
case reflect.Bool:
|
||||
return d.decodeBool(name, node, result)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return d.decodeFloat(name, node, result)
|
||||
case reflect.Int, reflect.Int32, reflect.Int64:
|
||||
return d.decodeInt(name, node, result)
|
||||
case reflect.Interface:
|
||||
// When we see an interface, we make our own thing
|
||||
return d.decodeInterface(name, node, result)
|
||||
case reflect.Map:
|
||||
return d.decodeMap(name, node, result)
|
||||
case reflect.Ptr:
|
||||
return d.decodePtr(name, node, result)
|
||||
case reflect.Slice:
|
||||
return d.decodeSlice(name, node, result)
|
||||
case reflect.String:
|
||||
return d.decodeString(name, node, result)
|
||||
case reflect.Struct:
|
||||
return d.decodeStruct(name, node, result)
|
||||
default:
|
||||
return &parser.PosError{
|
||||
Pos: node.Pos(),
|
||||
Err: fmt.Errorf("%s: unknown kind to decode into: %s", name, k.Kind()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) decodeBool(name string, node ast.Node, result reflect.Value) error {
|
||||
switch n := node.(type) {
|
||||
case *ast.LiteralType:
|
||||
if n.Token.Type == token.BOOL {
|
||||
v, err := strconv.ParseBool(n.Token.Text)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result.Set(reflect.ValueOf(v))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return &parser.PosError{
|
||||
Pos: node.Pos(),
|
||||
Err: fmt.Errorf("%s: unknown type %T", name, node),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) decodeFloat(name string, node ast.Node, result reflect.Value) error {
|
||||
switch n := node.(type) {
|
||||
case *ast.LiteralType:
|
||||
if n.Token.Type == token.FLOAT || n.Token.Type == token.NUMBER {
|
||||
v, err := strconv.ParseFloat(n.Token.Text, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result.Set(reflect.ValueOf(v).Convert(result.Type()))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return &parser.PosError{
|
||||
Pos: node.Pos(),
|
||||
Err: fmt.Errorf("%s: unknown type %T", name, node),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) decodeInt(name string, node ast.Node, result reflect.Value) error {
|
||||
switch n := node.(type) {
|
||||
case *ast.LiteralType:
|
||||
switch n.Token.Type {
|
||||
case token.NUMBER:
|
||||
v, err := strconv.ParseInt(n.Token.Text, 0, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.Kind() == reflect.Interface {
|
||||
result.Set(reflect.ValueOf(int(v)))
|
||||
} else {
|
||||
result.SetInt(v)
|
||||
}
|
||||
return nil
|
||||
case token.STRING:
|
||||
v, err := strconv.ParseInt(n.Token.Value().(string), 0, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.Kind() == reflect.Interface {
|
||||
result.Set(reflect.ValueOf(int(v)))
|
||||
} else {
|
||||
result.SetInt(v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return &parser.PosError{
|
||||
Pos: node.Pos(),
|
||||
Err: fmt.Errorf("%s: unknown type %T", name, node),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) decodeInterface(name string, node ast.Node, result reflect.Value) error {
|
||||
// When we see an ast.Node, we retain the value to enable deferred decoding.
|
||||
// Very useful in situations where we want to preserve ast.Node information
|
||||
// like Pos
|
||||
if result.Type() == nodeType && result.CanSet() {
|
||||
result.Set(reflect.ValueOf(node))
|
||||
return nil
|
||||
}
|
||||
|
||||
var set reflect.Value
|
||||
redecode := true
|
||||
|
||||
// For testing types, ObjectType should just be treated as a list. We
|
||||
// set this to a temporary var because we want to pass in the real node.
|
||||
testNode := node
|
||||
if ot, ok := node.(*ast.ObjectType); ok {
|
||||
testNode = ot.List
|
||||
}
|
||||
|
||||
switch n := testNode.(type) {
|
||||
case *ast.ObjectList:
|
||||
// If we're at the root or we're directly within a slice, then we
|
||||
// decode objects into map[string]interface{}, otherwise we decode
|
||||
// them into lists.
|
||||
if len(d.stack) == 0 || d.stack[len(d.stack)-1] == reflect.Slice {
|
||||
var temp map[string]interface{}
|
||||
tempVal := reflect.ValueOf(temp)
|
||||
result := reflect.MakeMap(
|
||||
reflect.MapOf(
|
||||
reflect.TypeOf(""),
|
||||
tempVal.Type().Elem()))
|
||||
|
||||
set = result
|
||||
} else {
|
||||
var temp []map[string]interface{}
|
||||
tempVal := reflect.ValueOf(temp)
|
||||
result := reflect.MakeSlice(
|
||||
reflect.SliceOf(tempVal.Type().Elem()), 0, len(n.Items))
|
||||
set = result
|
||||
}
|
||||
case *ast.ObjectType:
|
||||
// If we're at the root or we're directly within a slice, then we
|
||||
// decode objects into map[string]interface{}, otherwise we decode
|
||||
// them into lists.
|
||||
if len(d.stack) == 0 || d.stack[len(d.stack)-1] == reflect.Slice {
|
||||
var temp map[string]interface{}
|
||||
tempVal := reflect.ValueOf(temp)
|
||||
result := reflect.MakeMap(
|
||||
reflect.MapOf(
|
||||
reflect.TypeOf(""),
|
||||
tempVal.Type().Elem()))
|
||||
|
||||
set = result
|
||||
} else {
|
||||
var temp []map[string]interface{}
|
||||
tempVal := reflect.ValueOf(temp)
|
||||
result := reflect.MakeSlice(
|
||||
reflect.SliceOf(tempVal.Type().Elem()), 0, 1)
|
||||
set = result
|
||||
}
|
||||
case *ast.ListType:
|
||||
var temp []interface{}
|
||||
tempVal := reflect.ValueOf(temp)
|
||||
result := reflect.MakeSlice(
|
||||
reflect.SliceOf(tempVal.Type().Elem()), 0, 0)
|
||||
set = result
|
||||
case *ast.LiteralType:
|
||||
switch n.Token.Type {
|
||||
case token.BOOL:
|
||||
var result bool
|
||||
set = reflect.Indirect(reflect.New(reflect.TypeOf(result)))
|
||||
case token.FLOAT:
|
||||
var result float64
|
||||
set = reflect.Indirect(reflect.New(reflect.TypeOf(result)))
|
||||
case token.NUMBER:
|
||||
var result int
|
||||
set = reflect.Indirect(reflect.New(reflect.TypeOf(result)))
|
||||
case token.STRING, token.HEREDOC:
|
||||
set = reflect.Indirect(reflect.New(reflect.TypeOf("")))
|
||||
default:
|
||||
return &parser.PosError{
|
||||
Pos: node.Pos(),
|
||||
Err: fmt.Errorf("%s: cannot decode into interface: %T", name, node),
|
||||
}
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf(
|
||||
"%s: cannot decode into interface: %T",
|
||||
name, node)
|
||||
}
|
||||
|
||||
// Set the result to what its supposed to be, then reset
|
||||
// result so we don't reflect into this method anymore.
|
||||
result.Set(set)
|
||||
|
||||
if redecode {
|
||||
// Revisit the node so that we can use the newly instantiated
|
||||
// thing and populate it.
|
||||
if err := d.decode(name, node, result); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *decoder) decodeMap(name string, node ast.Node, result reflect.Value) error {
|
||||
if item, ok := node.(*ast.ObjectItem); ok {
|
||||
node = &ast.ObjectList{Items: []*ast.ObjectItem{item}}
|
||||
}
|
||||
|
||||
if ot, ok := node.(*ast.ObjectType); ok {
|
||||
node = ot.List
|
||||
}
|
||||
|
||||
n, ok := node.(*ast.ObjectList)
|
||||
if !ok {
|
||||
return &parser.PosError{
|
||||
Pos: node.Pos(),
|
||||
Err: fmt.Errorf("%s: not an object type for map (%T)", name, node),
|
||||
}
|
||||
}
|
||||
|
||||
// If we have an interface, then we can address the interface,
|
||||
// but not the slice itself, so get the element but set the interface
|
||||
set := result
|
||||
if result.Kind() == reflect.Interface {
|
||||
result = result.Elem()
|
||||
}
|
||||
|
||||
resultType := result.Type()
|
||||
resultElemType := resultType.Elem()
|
||||
resultKeyType := resultType.Key()
|
||||
if resultKeyType.Kind() != reflect.String {
|
||||
return &parser.PosError{
|
||||
Pos: node.Pos(),
|
||||
Err: fmt.Errorf("%s: map must have string keys", name),
|
||||
}
|
||||
}
|
||||
|
||||
// Make a map if it is nil
|
||||
resultMap := result
|
||||
if result.IsNil() {
|
||||
resultMap = reflect.MakeMap(
|
||||
reflect.MapOf(resultKeyType, resultElemType))
|
||||
}
|
||||
|
||||
// Go through each element and decode it.
|
||||
done := make(map[string]struct{})
|
||||
for _, item := range n.Items {
|
||||
if item.Val == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// github.com/hashicorp/terraform/issue/5740
|
||||
if len(item.Keys) == 0 {
|
||||
return &parser.PosError{
|
||||
Pos: node.Pos(),
|
||||
Err: fmt.Errorf("%s: map must have string keys", name),
|
||||
}
|
||||
}
|
||||
|
||||
// Get the key we're dealing with, which is the first item
|
||||
keyStr := item.Keys[0].Token.Value().(string)
|
||||
|
||||
// If we've already processed this key, then ignore it
|
||||
if _, ok := done[keyStr]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Determine the value. If we have more than one key, then we
|
||||
// get the objectlist of only these keys.
|
||||
itemVal := item.Val
|
||||
if len(item.Keys) > 1 {
|
||||
itemVal = n.Filter(keyStr)
|
||||
done[keyStr] = struct{}{}
|
||||
}
|
||||
|
||||
// Make the field name
|
||||
fieldName := fmt.Sprintf("%s.%s", name, keyStr)
|
||||
|
||||
// Get the key/value as reflection values
|
||||
key := reflect.ValueOf(keyStr)
|
||||
val := reflect.Indirect(reflect.New(resultElemType))
|
||||
|
||||
// If we have a pre-existing value in the map, use that
|
||||
oldVal := resultMap.MapIndex(key)
|
||||
if oldVal.IsValid() {
|
||||
val.Set(oldVal)
|
||||
}
|
||||
|
||||
// Decode!
|
||||
if err := d.decode(fieldName, itemVal, val); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the value on the map
|
||||
resultMap.SetMapIndex(key, val)
|
||||
}
|
||||
|
||||
// Set the final map if we can
|
||||
set.Set(resultMap)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *decoder) decodePtr(name string, node ast.Node, result reflect.Value) error {
|
||||
// Create an element of the concrete (non pointer) type and decode
|
||||
// into that. Then set the value of the pointer to this type.
|
||||
resultType := result.Type()
|
||||
resultElemType := resultType.Elem()
|
||||
val := reflect.New(resultElemType)
|
||||
if err := d.decode(name, node, reflect.Indirect(val)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result.Set(val)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *decoder) decodeSlice(name string, node ast.Node, result reflect.Value) error {
|
||||
// If we have an interface, then we can address the interface,
|
||||
// but not the slice itself, so get the element but set the interface
|
||||
set := result
|
||||
if result.Kind() == reflect.Interface {
|
||||
result = result.Elem()
|
||||
}
|
||||
// Create the slice if it isn't nil
|
||||
resultType := result.Type()
|
||||
resultElemType := resultType.Elem()
|
||||
if result.IsNil() {
|
||||
resultSliceType := reflect.SliceOf(resultElemType)
|
||||
result = reflect.MakeSlice(
|
||||
resultSliceType, 0, 0)
|
||||
}
|
||||
|
||||
// Figure out the items we'll be copying into the slice
|
||||
var items []ast.Node
|
||||
switch n := node.(type) {
|
||||
case *ast.ObjectList:
|
||||
items = make([]ast.Node, len(n.Items))
|
||||
for i, item := range n.Items {
|
||||
items[i] = item
|
||||
}
|
||||
case *ast.ObjectType:
|
||||
items = []ast.Node{n}
|
||||
case *ast.ListType:
|
||||
items = n.List
|
||||
default:
|
||||
return &parser.PosError{
|
||||
Pos: node.Pos(),
|
||||
Err: fmt.Errorf("unknown slice type: %T", node),
|
||||
}
|
||||
}
|
||||
|
||||
for i, item := range items {
|
||||
fieldName := fmt.Sprintf("%s[%d]", name, i)
|
||||
|
||||
// Decode
|
||||
val := reflect.Indirect(reflect.New(resultElemType))
|
||||
|
||||
// if item is an object that was decoded from ambiguous JSON and
|
||||
// flattened, make sure it's expanded if it needs to decode into a
|
||||
// defined structure.
|
||||
item := expandObject(item, val)
|
||||
|
||||
if err := d.decode(fieldName, item, val); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Append it onto the slice
|
||||
result = reflect.Append(result, val)
|
||||
}
|
||||
|
||||
set.Set(result)
|
||||
return nil
|
||||
}
|
||||
|
||||
// expandObject detects if an ambiguous JSON object was flattened to a List which
|
||||
// should be decoded into a struct, and expands the ast to properly deocode.
|
||||
func expandObject(node ast.Node, result reflect.Value) ast.Node {
|
||||
item, ok := node.(*ast.ObjectItem)
|
||||
if !ok {
|
||||
return node
|
||||
}
|
||||
|
||||
elemType := result.Type()
|
||||
|
||||
// our target type must be a struct
|
||||
switch elemType.Kind() {
|
||||
case reflect.Ptr:
|
||||
switch elemType.Elem().Kind() {
|
||||
case reflect.Struct:
|
||||
//OK
|
||||
default:
|
||||
return node
|
||||
}
|
||||
case reflect.Struct:
|
||||
//OK
|
||||
default:
|
||||
return node
|
||||
}
|
||||
|
||||
// A list value will have a key and field name. If it had more fields,
|
||||
// it wouldn't have been flattened.
|
||||
if len(item.Keys) != 2 {
|
||||
return node
|
||||
}
|
||||
|
||||
keyToken := item.Keys[0].Token
|
||||
item.Keys = item.Keys[1:]
|
||||
|
||||
// we need to un-flatten the ast enough to decode
|
||||
newNode := &ast.ObjectItem{
|
||||
Keys: []*ast.ObjectKey{
|
||||
&ast.ObjectKey{
|
||||
Token: keyToken,
|
||||
},
|
||||
},
|
||||
Val: &ast.ObjectType{
|
||||
List: &ast.ObjectList{
|
||||
Items: []*ast.ObjectItem{item},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return newNode
|
||||
}
|
||||
|
||||
func (d *decoder) decodeString(name string, node ast.Node, result reflect.Value) error {
|
||||
switch n := node.(type) {
|
||||
case *ast.LiteralType:
|
||||
switch n.Token.Type {
|
||||
case token.NUMBER:
|
||||
result.Set(reflect.ValueOf(n.Token.Text).Convert(result.Type()))
|
||||
return nil
|
||||
case token.STRING, token.HEREDOC:
|
||||
result.Set(reflect.ValueOf(n.Token.Value()).Convert(result.Type()))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return &parser.PosError{
|
||||
Pos: node.Pos(),
|
||||
Err: fmt.Errorf("%s: unknown type for string %T", name, node),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) decodeStruct(name string, node ast.Node, result reflect.Value) error {
|
||||
var item *ast.ObjectItem
|
||||
if it, ok := node.(*ast.ObjectItem); ok {
|
||||
item = it
|
||||
node = it.Val
|
||||
}
|
||||
|
||||
if ot, ok := node.(*ast.ObjectType); ok {
|
||||
node = ot.List
|
||||
}
|
||||
|
||||
// Handle the special case where the object itself is a literal. Previously
|
||||
// the yacc parser would always ensure top-level elements were arrays. The new
|
||||
// parser does not make the same guarantees, thus we need to convert any
|
||||
// top-level literal elements into a list.
|
||||
if _, ok := node.(*ast.LiteralType); ok && item != nil {
|
||||
node = &ast.ObjectList{Items: []*ast.ObjectItem{item}}
|
||||
}
|
||||
|
||||
list, ok := node.(*ast.ObjectList)
|
||||
if !ok {
|
||||
return &parser.PosError{
|
||||
Pos: node.Pos(),
|
||||
Err: fmt.Errorf("%s: not an object type for struct (%T)", name, node),
|
||||
}
|
||||
}
|
||||
|
||||
// This slice will keep track of all the structs we'll be decoding.
|
||||
// There can be more than one struct if there are embedded structs
|
||||
// that are squashed.
|
||||
structs := make([]reflect.Value, 1, 5)
|
||||
structs[0] = result
|
||||
|
||||
// Compile the list of all the fields that we're going to be decoding
|
||||
// from all the structs.
|
||||
type field struct {
|
||||
field reflect.StructField
|
||||
val reflect.Value
|
||||
}
|
||||
fields := []field{}
|
||||
for len(structs) > 0 {
|
||||
structVal := structs[0]
|
||||
structs = structs[1:]
|
||||
|
||||
structType := structVal.Type()
|
||||
for i := 0; i < structType.NumField(); i++ {
|
||||
fieldType := structType.Field(i)
|
||||
tagParts := strings.Split(fieldType.Tag.Get(tagName), ",")
|
||||
|
||||
// Ignore fields with tag name "-"
|
||||
if tagParts[0] == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
if fieldType.Anonymous {
|
||||
fieldKind := fieldType.Type.Kind()
|
||||
if fieldKind != reflect.Struct {
|
||||
return &parser.PosError{
|
||||
Pos: node.Pos(),
|
||||
Err: fmt.Errorf("%s: unsupported type to struct: %s",
|
||||
fieldType.Name, fieldKind),
|
||||
}
|
||||
}
|
||||
|
||||
// We have an embedded field. We "squash" the fields down
|
||||
// if specified in the tag.
|
||||
squash := false
|
||||
for _, tag := range tagParts[1:] {
|
||||
if tag == "squash" {
|
||||
squash = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if squash {
|
||||
structs = append(
|
||||
structs, result.FieldByName(fieldType.Name))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Normal struct field, store it away
|
||||
fields = append(fields, field{fieldType, structVal.Field(i)})
|
||||
}
|
||||
}
|
||||
|
||||
usedKeys := make(map[string]struct{})
|
||||
decodedFields := make([]string, 0, len(fields))
|
||||
decodedFieldsVal := make([]reflect.Value, 0)
|
||||
unusedKeysVal := make([]reflect.Value, 0)
|
||||
for _, f := range fields {
|
||||
field, fieldValue := f.field, f.val
|
||||
if !fieldValue.IsValid() {
|
||||
// This should never happen
|
||||
panic("field is not valid")
|
||||
}
|
||||
|
||||
// If we can't set the field, then it is unexported or something,
|
||||
// and we just continue onwards.
|
||||
if !fieldValue.CanSet() {
|
||||
continue
|
||||
}
|
||||
|
||||
fieldName := field.Name
|
||||
|
||||
tagValue := field.Tag.Get(tagName)
|
||||
tagParts := strings.SplitN(tagValue, ",", 2)
|
||||
if len(tagParts) >= 2 {
|
||||
switch tagParts[1] {
|
||||
case "decodedFields":
|
||||
decodedFieldsVal = append(decodedFieldsVal, fieldValue)
|
||||
continue
|
||||
case "key":
|
||||
if item == nil {
|
||||
return &parser.PosError{
|
||||
Pos: node.Pos(),
|
||||
Err: fmt.Errorf("%s: %s asked for 'key', impossible",
|
||||
name, fieldName),
|
||||
}
|
||||
}
|
||||
|
||||
fieldValue.SetString(item.Keys[0].Token.Value().(string))
|
||||
continue
|
||||
case "unusedKeys":
|
||||
unusedKeysVal = append(unusedKeysVal, fieldValue)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if tagParts[0] != "" {
|
||||
fieldName = tagParts[0]
|
||||
}
|
||||
|
||||
// Determine the element we'll use to decode. If it is a single
|
||||
// match (only object with the field), then we decode it exactly.
|
||||
// If it is a prefix match, then we decode the matches.
|
||||
filter := list.Filter(fieldName)
|
||||
|
||||
prefixMatches := filter.Children()
|
||||
matches := filter.Elem()
|
||||
if len(matches.Items) == 0 && len(prefixMatches.Items) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Track the used key
|
||||
usedKeys[fieldName] = struct{}{}
|
||||
|
||||
// Create the field name and decode. We range over the elements
|
||||
// because we actually want the value.
|
||||
fieldName = fmt.Sprintf("%s.%s", name, fieldName)
|
||||
if len(prefixMatches.Items) > 0 {
|
||||
if err := d.decode(fieldName, prefixMatches, fieldValue); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, match := range matches.Items {
|
||||
var decodeNode ast.Node = match.Val
|
||||
if ot, ok := decodeNode.(*ast.ObjectType); ok {
|
||||
decodeNode = &ast.ObjectList{Items: ot.List.Items}
|
||||
}
|
||||
|
||||
if err := d.decode(fieldName, decodeNode, fieldValue); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
decodedFields = append(decodedFields, field.Name)
|
||||
}
|
||||
|
||||
if len(decodedFieldsVal) > 0 {
|
||||
// Sort it so that it is deterministic
|
||||
sort.Strings(decodedFields)
|
||||
|
||||
for _, v := range decodedFieldsVal {
|
||||
v.Set(reflect.ValueOf(decodedFields))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// findNodeType returns the type of ast.Node
|
||||
func findNodeType() reflect.Type {
|
||||
var nodeContainer struct {
|
||||
Node ast.Node
|
||||
}
|
||||
value := reflect.ValueOf(nodeContainer).FieldByName("Node")
|
||||
return value.Type()
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
module github.com/hashicorp/hcl
|
||||
|
||||
require github.com/davecgh/go-spew v1.1.1
|
@ -1,2 +0,0 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
@ -1,11 +0,0 @@
|
||||
// Package hcl decodes HCL into usable Go structures.
|
||||
//
|
||||
// hcl input can come in either pure HCL format or JSON format.
|
||||
// It can be parsed into an AST, and then decoded into a structure,
|
||||
// or it can be decoded directly from a string into a structure.
|
||||
//
|
||||
// If you choose to parse HCL into a raw AST, the benefit is that you
|
||||
// can write custom visitor implementations to implement custom
|
||||
// semantic checks. By default, HCL does not perform any semantic
|
||||
// checks.
|
||||
package hcl
|
@ -1,219 +0,0 @@
|
||||
// Package ast declares the types used to represent syntax trees for HCL
|
||||
// (HashiCorp Configuration Language)
|
||||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hcl/hcl/token"
|
||||
)
|
||||
|
||||
// Node is an element in the abstract syntax tree.
|
||||
type Node interface {
|
||||
node()
|
||||
Pos() token.Pos
|
||||
}
|
||||
|
||||
func (File) node() {}
|
||||
func (ObjectList) node() {}
|
||||
func (ObjectKey) node() {}
|
||||
func (ObjectItem) node() {}
|
||||
func (Comment) node() {}
|
||||
func (CommentGroup) node() {}
|
||||
func (ObjectType) node() {}
|
||||
func (LiteralType) node() {}
|
||||
func (ListType) node() {}
|
||||
|
||||
// File represents a single HCL file
|
||||
type File struct {
|
||||
Node Node // usually a *ObjectList
|
||||
Comments []*CommentGroup // list of all comments in the source
|
||||
}
|
||||
|
||||
func (f *File) Pos() token.Pos {
|
||||
return f.Node.Pos()
|
||||
}
|
||||
|
||||
// ObjectList represents a list of ObjectItems. An HCL file itself is an
|
||||
// ObjectList.
|
||||
type ObjectList struct {
|
||||
Items []*ObjectItem
|
||||
}
|
||||
|
||||
func (o *ObjectList) Add(item *ObjectItem) {
|
||||
o.Items = append(o.Items, item)
|
||||
}
|
||||
|
||||
// Filter filters out the objects with the given key list as a prefix.
|
||||
//
|
||||
// The returned list of objects contain ObjectItems where the keys have
|
||||
// this prefix already stripped off. This might result in objects with
|
||||
// zero-length key lists if they have no children.
|
||||
//
|
||||
// If no matches are found, an empty ObjectList (non-nil) is returned.
|
||||
func (o *ObjectList) Filter(keys ...string) *ObjectList {
|
||||
var result ObjectList
|
||||
for _, item := range o.Items {
|
||||
// If there aren't enough keys, then ignore this
|
||||
if len(item.Keys) < len(keys) {
|
||||
continue
|
||||
}
|
||||
|
||||
match := true
|
||||
for i, key := range item.Keys[:len(keys)] {
|
||||
key := key.Token.Value().(string)
|
||||
if key != keys[i] && !strings.EqualFold(key, keys[i]) {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
|
||||
// Strip off the prefix from the children
|
||||
newItem := *item
|
||||
newItem.Keys = newItem.Keys[len(keys):]
|
||||
result.Add(&newItem)
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
// Children returns further nested objects (key length > 0) within this
|
||||
// ObjectList. This should be used with Filter to get at child items.
|
||||
func (o *ObjectList) Children() *ObjectList {
|
||||
var result ObjectList
|
||||
for _, item := range o.Items {
|
||||
if len(item.Keys) > 0 {
|
||||
result.Add(item)
|
||||
}
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
// Elem returns items in the list that are direct element assignments
|
||||
// (key length == 0). This should be used with Filter to get at elements.
|
||||
func (o *ObjectList) Elem() *ObjectList {
|
||||
var result ObjectList
|
||||
for _, item := range o.Items {
|
||||
if len(item.Keys) == 0 {
|
||||
result.Add(item)
|
||||
}
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
func (o *ObjectList) Pos() token.Pos {
|
||||
// always returns the uninitiliazed position
|
||||
return o.Items[0].Pos()
|
||||
}
|
||||
|
||||
// ObjectItem represents a HCL Object Item. An item is represented with a key
|
||||
// (or keys). It can be an assignment or an object (both normal and nested)
|
||||
type ObjectItem struct {
|
||||
// keys is only one length long if it's of type assignment. If it's a
|
||||
// nested object it can be larger than one. In that case "assign" is
|
||||
// invalid as there is no assignments for a nested object.
|
||||
Keys []*ObjectKey
|
||||
|
||||
// assign contains the position of "=", if any
|
||||
Assign token.Pos
|
||||
|
||||
// val is the item itself. It can be an object,list, number, bool or a
|
||||
// string. If key length is larger than one, val can be only of type
|
||||
// Object.
|
||||
Val Node
|
||||
|
||||
LeadComment *CommentGroup // associated lead comment
|
||||
LineComment *CommentGroup // associated line comment
|
||||
}
|
||||
|
||||
func (o *ObjectItem) Pos() token.Pos {
|
||||
// I'm not entirely sure what causes this, but removing this causes
|
||||
// a test failure. We should investigate at some point.
|
||||
if len(o.Keys) == 0 {
|
||||
return token.Pos{}
|
||||
}
|
||||
|
||||
return o.Keys[0].Pos()
|
||||
}
|
||||
|
||||
// ObjectKeys are either an identifier or of type string.
|
||||
type ObjectKey struct {
|
||||
Token token.Token
|
||||
}
|
||||
|
||||
func (o *ObjectKey) Pos() token.Pos {
|
||||
return o.Token.Pos
|
||||
}
|
||||
|
||||
// LiteralType represents a literal of basic type. Valid types are:
|
||||
// token.NUMBER, token.FLOAT, token.BOOL and token.STRING
|
||||
type LiteralType struct {
|
||||
Token token.Token
|
||||
|
||||
// comment types, only used when in a list
|
||||
LeadComment *CommentGroup
|
||||
LineComment *CommentGroup
|
||||
}
|
||||
|
||||
func (l *LiteralType) Pos() token.Pos {
|
||||
return l.Token.Pos
|
||||
}
|
||||
|
||||
// ListStatement represents a HCL List type
|
||||
type ListType struct {
|
||||
Lbrack token.Pos // position of "["
|
||||
Rbrack token.Pos // position of "]"
|
||||
List []Node // the elements in lexical order
|
||||
}
|
||||
|
||||
func (l *ListType) Pos() token.Pos {
|
||||
return l.Lbrack
|
||||
}
|
||||
|
||||
func (l *ListType) Add(node Node) {
|
||||
l.List = append(l.List, node)
|
||||
}
|
||||
|
||||
// ObjectType represents a HCL Object Type
|
||||
type ObjectType struct {
|
||||
Lbrace token.Pos // position of "{"
|
||||
Rbrace token.Pos // position of "}"
|
||||
List *ObjectList // the nodes in lexical order
|
||||
}
|
||||
|
||||
func (o *ObjectType) Pos() token.Pos {
|
||||
return o.Lbrace
|
||||
}
|
||||
|
||||
// Comment node represents a single //, # style or /*- style commment
|
||||
type Comment struct {
|
||||
Start token.Pos // position of / or #
|
||||
Text string
|
||||
}
|
||||
|
||||
func (c *Comment) Pos() token.Pos {
|
||||
return c.Start
|
||||
}
|
||||
|
||||
// CommentGroup node represents a sequence of comments with no other tokens and
|
||||
// no empty lines between.
|
||||
type CommentGroup struct {
|
||||
List []*Comment // len(List) > 0
|
||||
}
|
||||
|
||||
func (c *CommentGroup) Pos() token.Pos {
|
||||
return c.List[0].Pos()
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// GoStringer
|
||||
//-------------------------------------------------------------------
|
||||
|
||||
func (o *ObjectKey) GoString() string { return fmt.Sprintf("*%#v", *o) }
|
||||
func (o *ObjectList) GoString() string { return fmt.Sprintf("*%#v", *o) }
|
@ -1,52 +0,0 @@
|
||||
package ast
|
||||
|
||||
import "fmt"
|
||||
|
||||
// WalkFunc describes a function to be called for each node during a Walk. The
|
||||
// returned node can be used to rewrite the AST. Walking stops the returned
|
||||
// bool is false.
|
||||
type WalkFunc func(Node) (Node, bool)
|
||||
|
||||
// Walk traverses an AST in depth-first order: It starts by calling fn(node);
|
||||
// node must not be nil. If fn returns true, Walk invokes fn recursively for
|
||||
// each of the non-nil children of node, followed by a call of fn(nil). The
|
||||
// returned node of fn can be used to rewrite the passed node to fn.
|
||||
func Walk(node Node, fn WalkFunc) Node {
|
||||
rewritten, ok := fn(node)
|
||||
if !ok {
|
||||
return rewritten
|
||||
}
|
||||
|
||||
switch n := node.(type) {
|
||||
case *File:
|
||||
n.Node = Walk(n.Node, fn)
|
||||
case *ObjectList:
|
||||
for i, item := range n.Items {
|
||||
n.Items[i] = Walk(item, fn).(*ObjectItem)
|
||||
}
|
||||
case *ObjectKey:
|
||||
// nothing to do
|
||||
case *ObjectItem:
|
||||
for i, k := range n.Keys {
|
||||
n.Keys[i] = Walk(k, fn).(*ObjectKey)
|
||||
}
|
||||
|
||||
if n.Val != nil {
|
||||
n.Val = Walk(n.Val, fn)
|
||||
}
|
||||
case *LiteralType:
|
||||
// nothing to do
|
||||
case *ListType:
|
||||
for i, l := range n.List {
|
||||
n.List[i] = Walk(l, fn)
|
||||
}
|
||||
case *ObjectType:
|
||||
n.List = Walk(n.List, fn).(*ObjectList)
|
||||
default:
|
||||
// should we panic here?
|
||||
fmt.Printf("unknown type: %T\n", n)
|
||||
}
|
||||
|
||||
fn(nil)
|
||||
return rewritten
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl/hcl/token"
|
||||
)
|
||||
|
||||
// PosError is a parse error that contains a position.
|
||||
type PosError struct {
|
||||
Pos token.Pos
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *PosError) Error() string {
|
||||
return fmt.Sprintf("At %s: %s", e.Pos, e.Err)
|
||||
}
|
@ -1,532 +0,0 @@
|
||||
// Package parser implements a parser for HCL (HashiCorp Configuration
|
||||
// Language)
|
||||
package parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hcl/hcl/ast"
|
||||
"github.com/hashicorp/hcl/hcl/scanner"
|
||||
"github.com/hashicorp/hcl/hcl/token"
|
||||
)
|
||||
|
||||
type Parser struct {
|
||||
sc *scanner.Scanner
|
||||
|
||||
// Last read token
|
||||
tok token.Token
|
||||
commaPrev token.Token
|
||||
|
||||
comments []*ast.CommentGroup
|
||||
leadComment *ast.CommentGroup // last lead comment
|
||||
lineComment *ast.CommentGroup // last line comment
|
||||
|
||||
enableTrace bool
|
||||
indent int
|
||||
n int // buffer size (max = 1)
|
||||
}
|
||||
|
||||
func newParser(src []byte) *Parser {
|
||||
return &Parser{
|
||||
sc: scanner.New(src),
|
||||
}
|
||||
}
|
||||
|
||||
// Parse returns the fully parsed source and returns the abstract syntax tree.
|
||||
func Parse(src []byte) (*ast.File, error) {
|
||||
// normalize all line endings
|
||||
// since the scanner and output only work with "\n" line endings, we may
|
||||
// end up with dangling "\r" characters in the parsed data.
|
||||
src = bytes.Replace(src, []byte("\r\n"), []byte("\n"), -1)
|
||||
|
||||
p := newParser(src)
|
||||
return p.Parse()
|
||||
}
|
||||
|
||||
var errEofToken = errors.New("EOF token found")
|
||||
|
||||
// Parse returns the fully parsed source and returns the abstract syntax tree.
|
||||
func (p *Parser) Parse() (*ast.File, error) {
|
||||
f := &ast.File{}
|
||||
var err, scerr error
|
||||
p.sc.Error = func(pos token.Pos, msg string) {
|
||||
scerr = &PosError{Pos: pos, Err: errors.New(msg)}
|
||||
}
|
||||
|
||||
f.Node, err = p.objectList(false)
|
||||
if scerr != nil {
|
||||
return nil, scerr
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f.Comments = p.comments
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// objectList parses a list of items within an object (generally k/v pairs).
|
||||
// The parameter" obj" tells this whether to we are within an object (braces:
|
||||
// '{', '}') or just at the top level. If we're within an object, we end
|
||||
// at an RBRACE.
|
||||
func (p *Parser) objectList(obj bool) (*ast.ObjectList, error) {
|
||||
defer un(trace(p, "ParseObjectList"))
|
||||
node := &ast.ObjectList{}
|
||||
|
||||
for {
|
||||
if obj {
|
||||
tok := p.scan()
|
||||
p.unscan()
|
||||
if tok.Type == token.RBRACE {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
n, err := p.objectItem()
|
||||
if err == errEofToken {
|
||||
break // we are finished
|
||||
}
|
||||
|
||||
// we don't return a nil node, because might want to use already
|
||||
// collected items.
|
||||
if err != nil {
|
||||
return node, err
|
||||
}
|
||||
|
||||
node.Add(n)
|
||||
|
||||
// object lists can be optionally comma-delimited e.g. when a list of maps
|
||||
// is being expressed, so a comma is allowed here - it's simply consumed
|
||||
tok := p.scan()
|
||||
if tok.Type != token.COMMA {
|
||||
p.unscan()
|
||||
}
|
||||
}
|
||||
return node, nil
|
||||
}
|
||||
|
||||
func (p *Parser) consumeComment() (comment *ast.Comment, endline int) {
|
||||
endline = p.tok.Pos.Line
|
||||
|
||||
// count the endline if it's multiline comment, ie starting with /*
|
||||
if len(p.tok.Text) > 1 && p.tok.Text[1] == '*' {
|
||||
// don't use range here - no need to decode Unicode code points
|
||||
for i := 0; i < len(p.tok.Text); i++ {
|
||||
if p.tok.Text[i] == '\n' {
|
||||
endline++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
comment = &ast.Comment{Start: p.tok.Pos, Text: p.tok.Text}
|
||||
p.tok = p.sc.Scan()
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Parser) consumeCommentGroup(n int) (comments *ast.CommentGroup, endline int) {
|
||||
var list []*ast.Comment
|
||||
endline = p.tok.Pos.Line
|
||||
|
||||
for p.tok.Type == token.COMMENT && p.tok.Pos.Line <= endline+n {
|
||||
var comment *ast.Comment
|
||||
comment, endline = p.consumeComment()
|
||||
list = append(list, comment)
|
||||
}
|
||||
|
||||
// add comment group to the comments list
|
||||
comments = &ast.CommentGroup{List: list}
|
||||
p.comments = append(p.comments, comments)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// objectItem parses a single object item
|
||||
func (p *Parser) objectItem() (*ast.ObjectItem, error) {
|
||||
defer un(trace(p, "ParseObjectItem"))
|
||||
|
||||
keys, err := p.objectKey()
|
||||
if len(keys) > 0 && err == errEofToken {
|
||||
// We ignore eof token here since it is an error if we didn't
|
||||
// receive a value (but we did receive a key) for the item.
|
||||
err = nil
|
||||
}
|
||||
if len(keys) > 0 && err != nil && p.tok.Type == token.RBRACE {
|
||||
// This is a strange boolean statement, but what it means is:
|
||||
// We have keys with no value, and we're likely in an object
|
||||
// (since RBrace ends an object). For this, we set err to nil so
|
||||
// we continue and get the error below of having the wrong value
|
||||
// type.
|
||||
err = nil
|
||||
|
||||
// Reset the token type so we don't think it completed fine. See
|
||||
// objectType which uses p.tok.Type to check if we're done with
|
||||
// the object.
|
||||
p.tok.Type = token.EOF
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
o := &ast.ObjectItem{
|
||||
Keys: keys,
|
||||
}
|
||||
|
||||
if p.leadComment != nil {
|
||||
o.LeadComment = p.leadComment
|
||||
p.leadComment = nil
|
||||
}
|
||||
|
||||
switch p.tok.Type {
|
||||
case token.ASSIGN:
|
||||
o.Assign = p.tok.Pos
|
||||
o.Val, err = p.object()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case token.LBRACE:
|
||||
o.Val, err = p.objectType()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
keyStr := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
keyStr = append(keyStr, k.Token.Text)
|
||||
}
|
||||
|
||||
return nil, &PosError{
|
||||
Pos: p.tok.Pos,
|
||||
Err: fmt.Errorf(
|
||||
"key '%s' expected start of object ('{') or assignment ('=')",
|
||||
strings.Join(keyStr, " ")),
|
||||
}
|
||||
}
|
||||
|
||||
// key=#comment
|
||||
// val
|
||||
if p.lineComment != nil {
|
||||
o.LineComment, p.lineComment = p.lineComment, nil
|
||||
}
|
||||
|
||||
// do a look-ahead for line comment
|
||||
p.scan()
|
||||
if len(keys) > 0 && o.Val.Pos().Line == keys[0].Pos().Line && p.lineComment != nil {
|
||||
o.LineComment = p.lineComment
|
||||
p.lineComment = nil
|
||||
}
|
||||
p.unscan()
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// objectKey parses an object key and returns a ObjectKey AST
|
||||
func (p *Parser) objectKey() ([]*ast.ObjectKey, error) {
|
||||
keyCount := 0
|
||||
keys := make([]*ast.ObjectKey, 0)
|
||||
|
||||
for {
|
||||
tok := p.scan()
|
||||
switch tok.Type {
|
||||
case token.EOF:
|
||||
// It is very important to also return the keys here as well as
|
||||
// the error. This is because we need to be able to tell if we
|
||||
// did parse keys prior to finding the EOF, or if we just found
|
||||
// a bare EOF.
|
||||
return keys, errEofToken
|
||||
case token.ASSIGN:
|
||||
// assignment or object only, but not nested objects. this is not
|
||||
// allowed: `foo bar = {}`
|
||||
if keyCount > 1 {
|
||||
return nil, &PosError{
|
||||
Pos: p.tok.Pos,
|
||||
Err: fmt.Errorf("nested object expected: LBRACE got: %s", p.tok.Type),
|
||||
}
|
||||
}
|
||||
|
||||
if keyCount == 0 {
|
||||
return nil, &PosError{
|
||||
Pos: p.tok.Pos,
|
||||
Err: errors.New("no object keys found!"),
|
||||
}
|
||||
}
|
||||
|
||||
return keys, nil
|
||||
case token.LBRACE:
|
||||
var err error
|
||||
|
||||
// If we have no keys, then it is a syntax error. i.e. {{}} is not
|
||||
// allowed.
|
||||
if len(keys) == 0 {
|
||||
err = &PosError{
|
||||
Pos: p.tok.Pos,
|
||||
Err: fmt.Errorf("expected: IDENT | STRING got: %s", p.tok.Type),
|
||||
}
|
||||
}
|
||||
|
||||
// object
|
||||
return keys, err
|
||||
case token.IDENT, token.STRING:
|
||||
keyCount++
|
||||
keys = append(keys, &ast.ObjectKey{Token: p.tok})
|
||||
case token.ILLEGAL:
|
||||
return keys, &PosError{
|
||||
Pos: p.tok.Pos,
|
||||
Err: fmt.Errorf("illegal character"),
|
||||
}
|
||||
default:
|
||||
return keys, &PosError{
|
||||
Pos: p.tok.Pos,
|
||||
Err: fmt.Errorf("expected: IDENT | STRING | ASSIGN | LBRACE got: %s", p.tok.Type),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// object parses any type of object, such as number, bool, string, object or
|
||||
// list.
|
||||
func (p *Parser) object() (ast.Node, error) {
|
||||
defer un(trace(p, "ParseType"))
|
||||
tok := p.scan()
|
||||
|
||||
switch tok.Type {
|
||||
case token.NUMBER, token.FLOAT, token.BOOL, token.STRING, token.HEREDOC:
|
||||
return p.literalType()
|
||||
case token.LBRACE:
|
||||
return p.objectType()
|
||||
case token.LBRACK:
|
||||
return p.listType()
|
||||
case token.COMMENT:
|
||||
// implement comment
|
||||
case token.EOF:
|
||||
return nil, errEofToken
|
||||
}
|
||||
|
||||
return nil, &PosError{
|
||||
Pos: tok.Pos,
|
||||
Err: fmt.Errorf("Unknown token: %+v", tok),
|
||||
}
|
||||
}
|
||||
|
||||
// objectType parses an object type and returns a ObjectType AST
|
||||
func (p *Parser) objectType() (*ast.ObjectType, error) {
|
||||
defer un(trace(p, "ParseObjectType"))
|
||||
|
||||
// we assume that the currently scanned token is a LBRACE
|
||||
o := &ast.ObjectType{
|
||||
Lbrace: p.tok.Pos,
|
||||
}
|
||||
|
||||
l, err := p.objectList(true)
|
||||
|
||||
// if we hit RBRACE, we are good to go (means we parsed all Items), if it's
|
||||
// not a RBRACE, it's an syntax error and we just return it.
|
||||
if err != nil && p.tok.Type != token.RBRACE {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// No error, scan and expect the ending to be a brace
|
||||
if tok := p.scan(); tok.Type != token.RBRACE {
|
||||
return nil, &PosError{
|
||||
Pos: tok.Pos,
|
||||
Err: fmt.Errorf("object expected closing RBRACE got: %s", tok.Type),
|
||||
}
|
||||
}
|
||||
|
||||
o.List = l
|
||||
o.Rbrace = p.tok.Pos // advanced via parseObjectList
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// listType parses a list type and returns a ListType AST
|
||||
func (p *Parser) listType() (*ast.ListType, error) {
|
||||
defer un(trace(p, "ParseListType"))
|
||||
|
||||
// we assume that the currently scanned token is a LBRACK
|
||||
l := &ast.ListType{
|
||||
Lbrack: p.tok.Pos,
|
||||
}
|
||||
|
||||
needComma := false
|
||||
for {
|
||||
tok := p.scan()
|
||||
if needComma {
|
||||
switch tok.Type {
|
||||
case token.COMMA, token.RBRACK:
|
||||
default:
|
||||
return nil, &PosError{
|
||||
Pos: tok.Pos,
|
||||
Err: fmt.Errorf(
|
||||
"error parsing list, expected comma or list end, got: %s",
|
||||
tok.Type),
|
||||
}
|
||||
}
|
||||
}
|
||||
switch tok.Type {
|
||||
case token.BOOL, token.NUMBER, token.FLOAT, token.STRING, token.HEREDOC:
|
||||
node, err := p.literalType()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If there is a lead comment, apply it
|
||||
if p.leadComment != nil {
|
||||
node.LeadComment = p.leadComment
|
||||
p.leadComment = nil
|
||||
}
|
||||
|
||||
l.Add(node)
|
||||
needComma = true
|
||||
case token.COMMA:
|
||||
// get next list item or we are at the end
|
||||
// do a look-ahead for line comment
|
||||
p.scan()
|
||||
if p.lineComment != nil && len(l.List) > 0 {
|
||||
lit, ok := l.List[len(l.List)-1].(*ast.LiteralType)
|
||||
if ok {
|
||||
lit.LineComment = p.lineComment
|
||||
l.List[len(l.List)-1] = lit
|
||||
p.lineComment = nil
|
||||
}
|
||||
}
|
||||
p.unscan()
|
||||
|
||||
needComma = false
|
||||
continue
|
||||
case token.LBRACE:
|
||||
// Looks like a nested object, so parse it out
|
||||
node, err := p.objectType()
|
||||
if err != nil {
|
||||
return nil, &PosError{
|
||||
Pos: tok.Pos,
|
||||
Err: fmt.Errorf(
|
||||
"error while trying to parse object within list: %s", err),
|
||||
}
|
||||
}
|
||||
l.Add(node)
|
||||
needComma = true
|
||||
case token.LBRACK:
|
||||
node, err := p.listType()
|
||||
if err != nil {
|
||||
return nil, &PosError{
|
||||
Pos: tok.Pos,
|
||||
Err: fmt.Errorf(
|
||||
"error while trying to parse list within list: %s", err),
|
||||
}
|
||||
}
|
||||
l.Add(node)
|
||||
case token.RBRACK:
|
||||
// finished
|
||||
l.Rbrack = p.tok.Pos
|
||||
return l, nil
|
||||
default:
|
||||
return nil, &PosError{
|
||||
Pos: tok.Pos,
|
||||
Err: fmt.Errorf("unexpected token while parsing list: %s", tok.Type),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// literalType parses a literal type and returns a LiteralType AST
|
||||
func (p *Parser) literalType() (*ast.LiteralType, error) {
|
||||
defer un(trace(p, "ParseLiteral"))
|
||||
|
||||
return &ast.LiteralType{
|
||||
Token: p.tok,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// scan returns the next token from the underlying scanner. If a token has
|
||||
// been unscanned then read that instead. In the process, it collects any
|
||||
// comment groups encountered, and remembers the last lead and line comments.
|
||||
func (p *Parser) scan() token.Token {
|
||||
// If we have a token on the buffer, then return it.
|
||||
if p.n != 0 {
|
||||
p.n = 0
|
||||
return p.tok
|
||||
}
|
||||
|
||||
// Otherwise read the next token from the scanner and Save it to the buffer
|
||||
// in case we unscan later.
|
||||
prev := p.tok
|
||||
p.tok = p.sc.Scan()
|
||||
|
||||
if p.tok.Type == token.COMMENT {
|
||||
var comment *ast.CommentGroup
|
||||
var endline int
|
||||
|
||||
// fmt.Printf("p.tok.Pos.Line = %+v prev: %d endline %d \n",
|
||||
// p.tok.Pos.Line, prev.Pos.Line, endline)
|
||||
if p.tok.Pos.Line == prev.Pos.Line {
|
||||
// The comment is on same line as the previous token; it
|
||||
// cannot be a lead comment but may be a line comment.
|
||||
comment, endline = p.consumeCommentGroup(0)
|
||||
if p.tok.Pos.Line != endline {
|
||||
// The next token is on a different line, thus
|
||||
// the last comment group is a line comment.
|
||||
p.lineComment = comment
|
||||
}
|
||||
}
|
||||
|
||||
// consume successor comments, if any
|
||||
endline = -1
|
||||
for p.tok.Type == token.COMMENT {
|
||||
comment, endline = p.consumeCommentGroup(1)
|
||||
}
|
||||
|
||||
if endline+1 == p.tok.Pos.Line && p.tok.Type != token.RBRACE {
|
||||
switch p.tok.Type {
|
||||
case token.RBRACE, token.RBRACK:
|
||||
// Do not count for these cases
|
||||
default:
|
||||
// The next token is following on the line immediately after the
|
||||
// comment group, thus the last comment group is a lead comment.
|
||||
p.leadComment = comment
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return p.tok
|
||||
}
|
||||
|
||||
// unscan pushes the previously read token back onto the buffer.
|
||||
func (p *Parser) unscan() {
|
||||
p.n = 1
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Parsing support
|
||||
|
||||
func (p *Parser) printTrace(a ...interface{}) {
|
||||
if !p.enableTrace {
|
||||
return
|
||||
}
|
||||
|
||||
const dots = ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . "
|
||||
const n = len(dots)
|
||||
fmt.Printf("%5d:%3d: ", p.tok.Pos.Line, p.tok.Pos.Column)
|
||||
|
||||
i := 2 * p.indent
|
||||
for i > n {
|
||||
fmt.Print(dots)
|
||||
i -= n
|
||||
}
|
||||
// i <= n
|
||||
fmt.Print(dots[0:i])
|
||||
fmt.Println(a...)
|
||||
}
|
||||
|
||||
func trace(p *Parser, msg string) *Parser {
|
||||
p.printTrace(msg, "(")
|
||||
p.indent++
|
||||
return p
|
||||
}
|
||||
|
||||
// Usage pattern: defer un(trace(p, "..."))
|
||||
func un(p *Parser) {
|
||||
p.indent--
|
||||
p.printTrace(")")
|
||||
}
|
@ -1,652 +0,0 @@
|
||||
// Package scanner implements a scanner for HCL (HashiCorp Configuration
|
||||
// Language) source text.
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/hashicorp/hcl/hcl/token"
|
||||
)
|
||||
|
||||
// eof represents a marker rune for the end of the reader.
|
||||
const eof = rune(0)
|
||||
|
||||
// Scanner defines a lexical scanner
|
||||
type Scanner struct {
|
||||
buf *bytes.Buffer // Source buffer for advancing and scanning
|
||||
src []byte // Source buffer for immutable access
|
||||
|
||||
// Source Position
|
||||
srcPos token.Pos // current position
|
||||
prevPos token.Pos // previous position, used for peek() method
|
||||
|
||||
lastCharLen int // length of last character in bytes
|
||||
lastLineLen int // length of last line in characters (for correct column reporting)
|
||||
|
||||
tokStart int // token text start position
|
||||
tokEnd int // token text end position
|
||||
|
||||
// Error is called for each error encountered. If no Error
|
||||
// function is set, the error is reported to os.Stderr.
|
||||
Error func(pos token.Pos, msg string)
|
||||
|
||||
// ErrorCount is incremented by one for each error encountered.
|
||||
ErrorCount int
|
||||
|
||||
// tokPos is the start position of most recently scanned token; set by
|
||||
// Scan. The Filename field is always left untouched by the Scanner. If
|
||||
// an error is reported (via Error) and Position is invalid, the scanner is
|
||||
// not inside a token.
|
||||
tokPos token.Pos
|
||||
}
|
||||
|
||||
// New creates and initializes a new instance of Scanner using src as
|
||||
// its source content.
|
||||
func New(src []byte) *Scanner {
|
||||
// even though we accept a src, we read from a io.Reader compatible type
|
||||
// (*bytes.Buffer). So in the future we might easily change it to streaming
|
||||
// read.
|
||||
b := bytes.NewBuffer(src)
|
||||
s := &Scanner{
|
||||
buf: b,
|
||||
src: src,
|
||||
}
|
||||
|
||||
// srcPosition always starts with 1
|
||||
s.srcPos.Line = 1
|
||||
return s
|
||||
}
|
||||
|
||||
// next reads the next rune from the bufferred reader. Returns the rune(0) if
|
||||
// an error occurs (or io.EOF is returned).
|
||||
func (s *Scanner) next() rune {
|
||||
ch, size, err := s.buf.ReadRune()
|
||||
if err != nil {
|
||||
// advance for error reporting
|
||||
s.srcPos.Column++
|
||||
s.srcPos.Offset += size
|
||||
s.lastCharLen = size
|
||||
return eof
|
||||
}
|
||||
|
||||
// remember last position
|
||||
s.prevPos = s.srcPos
|
||||
|
||||
s.srcPos.Column++
|
||||
s.lastCharLen = size
|
||||
s.srcPos.Offset += size
|
||||
|
||||
if ch == utf8.RuneError && size == 1 {
|
||||
s.err("illegal UTF-8 encoding")
|
||||
return ch
|
||||
}
|
||||
|
||||
if ch == '\n' {
|
||||
s.srcPos.Line++
|
||||
s.lastLineLen = s.srcPos.Column
|
||||
s.srcPos.Column = 0
|
||||
}
|
||||
|
||||
if ch == '\x00' {
|
||||
s.err("unexpected null character (0x00)")
|
||||
return eof
|
||||
}
|
||||
|
||||
if ch == '\uE123' {
|
||||
s.err("unicode code point U+E123 reserved for internal use")
|
||||
return utf8.RuneError
|
||||
}
|
||||
|
||||
// debug
|
||||
// fmt.Printf("ch: %q, offset:column: %d:%d\n", ch, s.srcPos.Offset, s.srcPos.Column)
|
||||
return ch
|
||||
}
|
||||
|
||||
// unread unreads the previous read Rune and updates the source position
|
||||
func (s *Scanner) unread() {
|
||||
if err := s.buf.UnreadRune(); err != nil {
|
||||
panic(err) // this is user fault, we should catch it
|
||||
}
|
||||
s.srcPos = s.prevPos // put back last position
|
||||
}
|
||||
|
||||
// peek returns the next rune without advancing the reader.
|
||||
func (s *Scanner) peek() rune {
|
||||
peek, _, err := s.buf.ReadRune()
|
||||
if err != nil {
|
||||
return eof
|
||||
}
|
||||
|
||||
s.buf.UnreadRune()
|
||||
return peek
|
||||
}
|
||||
|
||||
// Scan scans the next token and returns the token.
|
||||
func (s *Scanner) Scan() token.Token {
|
||||
ch := s.next()
|
||||
|
||||
// skip white space
|
||||
for isWhitespace(ch) {
|
||||
ch = s.next()
|
||||
}
|
||||
|
||||
var tok token.Type
|
||||
|
||||
// token text markings
|
||||
s.tokStart = s.srcPos.Offset - s.lastCharLen
|
||||
|
||||
// token position, initial next() is moving the offset by one(size of rune
|
||||
// actually), though we are interested with the starting point
|
||||
s.tokPos.Offset = s.srcPos.Offset - s.lastCharLen
|
||||
if s.srcPos.Column > 0 {
|
||||
// common case: last character was not a '\n'
|
||||
s.tokPos.Line = s.srcPos.Line
|
||||
s.tokPos.Column = s.srcPos.Column
|
||||
} else {
|
||||
// last character was a '\n'
|
||||
// (we cannot be at the beginning of the source
|
||||
// since we have called next() at least once)
|
||||
s.tokPos.Line = s.srcPos.Line - 1
|
||||
s.tokPos.Column = s.lastLineLen
|
||||
}
|
||||
|
||||
switch {
|
||||
case isLetter(ch):
|
||||
tok = token.IDENT
|
||||
lit := s.scanIdentifier()
|
||||
if lit == "true" || lit == "false" {
|
||||
tok = token.BOOL
|
||||
}
|
||||
case isDecimal(ch):
|
||||
tok = s.scanNumber(ch)
|
||||
default:
|
||||
switch ch {
|
||||
case eof:
|
||||
tok = token.EOF
|
||||
case '"':
|
||||
tok = token.STRING
|
||||
s.scanString()
|
||||
case '#', '/':
|
||||
tok = token.COMMENT
|
||||
s.scanComment(ch)
|
||||
case '.':
|
||||
tok = token.PERIOD
|
||||
ch = s.peek()
|
||||
if isDecimal(ch) {
|
||||
tok = token.FLOAT
|
||||
ch = s.scanMantissa(ch)
|
||||
ch = s.scanExponent(ch)
|
||||
}
|
||||
case '<':
|
||||
tok = token.HEREDOC
|
||||
s.scanHeredoc()
|
||||
case '[':
|
||||
tok = token.LBRACK
|
||||
case ']':
|
||||
tok = token.RBRACK
|
||||
case '{':
|
||||
tok = token.LBRACE
|
||||
case '}':
|
||||
tok = token.RBRACE
|
||||
case ',':
|
||||
tok = token.COMMA
|
||||
case '=':
|
||||
tok = token.ASSIGN
|
||||
case '+':
|
||||
tok = token.ADD
|
||||
case '-':
|
||||
if isDecimal(s.peek()) {
|
||||
ch := s.next()
|
||||
tok = s.scanNumber(ch)
|
||||
} else {
|
||||
tok = token.SUB
|
||||
}
|
||||
default:
|
||||
s.err("illegal char")
|
||||
}
|
||||
}
|
||||
|
||||
// finish token ending
|
||||
s.tokEnd = s.srcPos.Offset
|
||||
|
||||
// create token literal
|
||||
var tokenText string
|
||||
if s.tokStart >= 0 {
|
||||
tokenText = string(s.src[s.tokStart:s.tokEnd])
|
||||
}
|
||||
s.tokStart = s.tokEnd // ensure idempotency of tokenText() call
|
||||
|
||||
return token.Token{
|
||||
Type: tok,
|
||||
Pos: s.tokPos,
|
||||
Text: tokenText,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scanner) scanComment(ch rune) {
|
||||
// single line comments
|
||||
if ch == '#' || (ch == '/' && s.peek() != '*') {
|
||||
if ch == '/' && s.peek() != '/' {
|
||||
s.err("expected '/' for comment")
|
||||
return
|
||||
}
|
||||
|
||||
ch = s.next()
|
||||
for ch != '\n' && ch >= 0 && ch != eof {
|
||||
ch = s.next()
|
||||
}
|
||||
if ch != eof && ch >= 0 {
|
||||
s.unread()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// be sure we get the character after /* This allows us to find comment's
|
||||
// that are not erminated
|
||||
if ch == '/' {
|
||||
s.next()
|
||||
ch = s.next() // read character after "/*"
|
||||
}
|
||||
|
||||
// look for /* - style comments
|
||||
for {
|
||||
if ch < 0 || ch == eof {
|
||||
s.err("comment not terminated")
|
||||
break
|
||||
}
|
||||
|
||||
ch0 := ch
|
||||
ch = s.next()
|
||||
if ch0 == '*' && ch == '/' {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// scanNumber scans a HCL number definition starting with the given rune
|
||||
func (s *Scanner) scanNumber(ch rune) token.Type {
|
||||
if ch == '0' {
|
||||
// check for hexadecimal, octal or float
|
||||
ch = s.next()
|
||||
if ch == 'x' || ch == 'X' {
|
||||
// hexadecimal
|
||||
ch = s.next()
|
||||
found := false
|
||||
for isHexadecimal(ch) {
|
||||
ch = s.next()
|
||||
found = true
|
||||
}
|
||||
|
||||
if !found {
|
||||
s.err("illegal hexadecimal number")
|
||||
}
|
||||
|
||||
if ch != eof {
|
||||
s.unread()
|
||||
}
|
||||
|
||||
return token.NUMBER
|
||||
}
|
||||
|
||||
// now it's either something like: 0421(octal) or 0.1231(float)
|
||||
illegalOctal := false
|
||||
for isDecimal(ch) {
|
||||
ch = s.next()
|
||||
if ch == '8' || ch == '9' {
|
||||
// this is just a possibility. For example 0159 is illegal, but
|
||||
// 0159.23 is valid. So we mark a possible illegal octal. If
|
||||
// the next character is not a period, we'll print the error.
|
||||
illegalOctal = true
|
||||
}
|
||||
}
|
||||
|
||||
if ch == 'e' || ch == 'E' {
|
||||
ch = s.scanExponent(ch)
|
||||
return token.FLOAT
|
||||
}
|
||||
|
||||
if ch == '.' {
|
||||
ch = s.scanFraction(ch)
|
||||
|
||||
if ch == 'e' || ch == 'E' {
|
||||
ch = s.next()
|
||||
ch = s.scanExponent(ch)
|
||||
}
|
||||
return token.FLOAT
|
||||
}
|
||||
|
||||
if illegalOctal {
|
||||
s.err("illegal octal number")
|
||||
}
|
||||
|
||||
if ch != eof {
|
||||
s.unread()
|
||||
}
|
||||
return token.NUMBER
|
||||
}
|
||||
|
||||
s.scanMantissa(ch)
|
||||
ch = s.next() // seek forward
|
||||
if ch == 'e' || ch == 'E' {
|
||||
ch = s.scanExponent(ch)
|
||||
return token.FLOAT
|
||||
}
|
||||
|
||||
if ch == '.' {
|
||||
ch = s.scanFraction(ch)
|
||||
if ch == 'e' || ch == 'E' {
|
||||
ch = s.next()
|
||||
ch = s.scanExponent(ch)
|
||||
}
|
||||
return token.FLOAT
|
||||
}
|
||||
|
||||
if ch != eof {
|
||||
s.unread()
|
||||
}
|
||||
return token.NUMBER
|
||||
}
|
||||
|
||||
// scanMantissa scans the mantissa beginning from the rune. It returns the next
|
||||
// non decimal rune. It's used to determine wheter it's a fraction or exponent.
|
||||
func (s *Scanner) scanMantissa(ch rune) rune {
|
||||
scanned := false
|
||||
for isDecimal(ch) {
|
||||
ch = s.next()
|
||||
scanned = true
|
||||
}
|
||||
|
||||
if scanned && ch != eof {
|
||||
s.unread()
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
// scanFraction scans the fraction after the '.' rune
|
||||
func (s *Scanner) scanFraction(ch rune) rune {
|
||||
if ch == '.' {
|
||||
ch = s.peek() // we peek just to see if we can move forward
|
||||
ch = s.scanMantissa(ch)
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
// scanExponent scans the remaining parts of an exponent after the 'e' or 'E'
|
||||
// rune.
|
||||
func (s *Scanner) scanExponent(ch rune) rune {
|
||||
if ch == 'e' || ch == 'E' {
|
||||
ch = s.next()
|
||||
if ch == '-' || ch == '+' {
|
||||
ch = s.next()
|
||||
}
|
||||
ch = s.scanMantissa(ch)
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
// scanHeredoc scans a heredoc string
|
||||
func (s *Scanner) scanHeredoc() {
|
||||
// Scan the second '<' in example: '<<EOF'
|
||||
if s.next() != '<' {
|
||||
s.err("heredoc expected second '<', didn't see it")
|
||||
return
|
||||
}
|
||||
|
||||
// Get the original offset so we can read just the heredoc ident
|
||||
offs := s.srcPos.Offset
|
||||
|
||||
// Scan the identifier
|
||||
ch := s.next()
|
||||
|
||||
// Indented heredoc syntax
|
||||
if ch == '-' {
|
||||
ch = s.next()
|
||||
}
|
||||
|
||||
for isLetter(ch) || isDigit(ch) {
|
||||
ch = s.next()
|
||||
}
|
||||
|
||||
// If we reached an EOF then that is not good
|
||||
if ch == eof {
|
||||
s.err("heredoc not terminated")
|
||||
return
|
||||
}
|
||||
|
||||
// Ignore the '\r' in Windows line endings
|
||||
if ch == '\r' {
|
||||
if s.peek() == '\n' {
|
||||
ch = s.next()
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't reach a newline then that is also not good
|
||||
if ch != '\n' {
|
||||
s.err("invalid characters in heredoc anchor")
|
||||
return
|
||||
}
|
||||
|
||||
// Read the identifier
|
||||
identBytes := s.src[offs : s.srcPos.Offset-s.lastCharLen]
|
||||
if len(identBytes) == 0 || (len(identBytes) == 1 && identBytes[0] == '-') {
|
||||
s.err("zero-length heredoc anchor")
|
||||
return
|
||||
}
|
||||
|
||||
var identRegexp *regexp.Regexp
|
||||
if identBytes[0] == '-' {
|
||||
identRegexp = regexp.MustCompile(fmt.Sprintf(`^[[:space:]]*%s\r*\z`, identBytes[1:]))
|
||||
} else {
|
||||
identRegexp = regexp.MustCompile(fmt.Sprintf(`^[[:space:]]*%s\r*\z`, identBytes))
|
||||
}
|
||||
|
||||
// Read the actual string value
|
||||
lineStart := s.srcPos.Offset
|
||||
for {
|
||||
ch := s.next()
|
||||
|
||||
// Special newline handling.
|
||||
if ch == '\n' {
|
||||
// Math is fast, so we first compare the byte counts to see if we have a chance
|
||||
// of seeing the same identifier - if the length is less than the number of bytes
|
||||
// in the identifier, this cannot be a valid terminator.
|
||||
lineBytesLen := s.srcPos.Offset - s.lastCharLen - lineStart
|
||||
if lineBytesLen >= len(identBytes) && identRegexp.Match(s.src[lineStart:s.srcPos.Offset-s.lastCharLen]) {
|
||||
break
|
||||
}
|
||||
|
||||
// Not an anchor match, record the start of a new line
|
||||
lineStart = s.srcPos.Offset
|
||||
}
|
||||
|
||||
if ch == eof {
|
||||
s.err("heredoc not terminated")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// scanString scans a quoted string
|
||||
func (s *Scanner) scanString() {
|
||||
braces := 0
|
||||
for {
|
||||
// '"' opening already consumed
|
||||
// read character after quote
|
||||
ch := s.next()
|
||||
|
||||
if (ch == '\n' && braces == 0) || ch < 0 || ch == eof {
|
||||
s.err("literal not terminated")
|
||||
return
|
||||
}
|
||||
|
||||
if ch == '"' && braces == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// If we're going into a ${} then we can ignore quotes for awhile
|
||||
if braces == 0 && ch == '$' && s.peek() == '{' {
|
||||
braces++
|
||||
s.next()
|
||||
} else if braces > 0 && ch == '{' {
|
||||
braces++
|
||||
}
|
||||
if braces > 0 && ch == '}' {
|
||||
braces--
|
||||
}
|
||||
|
||||
if ch == '\\' {
|
||||
s.scanEscape()
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// scanEscape scans an escape sequence
|
||||
func (s *Scanner) scanEscape() rune {
|
||||
// http://en.cppreference.com/w/cpp/language/escape
|
||||
ch := s.next() // read character after '/'
|
||||
switch ch {
|
||||
case 'a', 'b', 'f', 'n', 'r', 't', 'v', '\\', '"':
|
||||
// nothing to do
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7':
|
||||
// octal notation
|
||||
ch = s.scanDigits(ch, 8, 3)
|
||||
case 'x':
|
||||
// hexademical notation
|
||||
ch = s.scanDigits(s.next(), 16, 2)
|
||||
case 'u':
|
||||
// universal character name
|
||||
ch = s.scanDigits(s.next(), 16, 4)
|
||||
case 'U':
|
||||
// universal character name
|
||||
ch = s.scanDigits(s.next(), 16, 8)
|
||||
default:
|
||||
s.err("illegal char escape")
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
// scanDigits scans a rune with the given base for n times. For example an
|
||||
// octal notation \184 would yield in scanDigits(ch, 8, 3)
|
||||
func (s *Scanner) scanDigits(ch rune, base, n int) rune {
|
||||
start := n
|
||||
for n > 0 && digitVal(ch) < base {
|
||||
ch = s.next()
|
||||
if ch == eof {
|
||||
// If we see an EOF, we halt any more scanning of digits
|
||||
// immediately.
|
||||
break
|
||||
}
|
||||
|
||||
n--
|
||||
}
|
||||
if n > 0 {
|
||||
s.err("illegal char escape")
|
||||
}
|
||||
|
||||
if n != start && ch != eof {
|
||||
// we scanned all digits, put the last non digit char back,
|
||||
// only if we read anything at all
|
||||
s.unread()
|
||||
}
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
// scanIdentifier scans an identifier and returns the literal string
|
||||
func (s *Scanner) scanIdentifier() string {
|
||||
offs := s.srcPos.Offset - s.lastCharLen
|
||||
ch := s.next()
|
||||
for isLetter(ch) || isDigit(ch) || ch == '-' || ch == '.' {
|
||||
ch = s.next()
|
||||
}
|
||||
|
||||
if ch != eof {
|
||||
s.unread() // we got identifier, put back latest char
|
||||
}
|
||||
|
||||
return string(s.src[offs:s.srcPos.Offset])
|
||||
}
|
||||
|
||||
// recentPosition returns the position of the character immediately after the
|
||||
// character or token returned by the last call to Scan.
|
||||
func (s *Scanner) recentPosition() (pos token.Pos) {
|
||||
pos.Offset = s.srcPos.Offset - s.lastCharLen
|
||||
switch {
|
||||
case s.srcPos.Column > 0:
|
||||
// common case: last character was not a '\n'
|
||||
pos.Line = s.srcPos.Line
|
||||
pos.Column = s.srcPos.Column
|
||||
case s.lastLineLen > 0:
|
||||
// last character was a '\n'
|
||||
// (we cannot be at the beginning of the source
|
||||
// since we have called next() at least once)
|
||||
pos.Line = s.srcPos.Line - 1
|
||||
pos.Column = s.lastLineLen
|
||||
default:
|
||||
// at the beginning of the source
|
||||
pos.Line = 1
|
||||
pos.Column = 1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// err prints the error of any scanning to s.Error function. If the function is
|
||||
// not defined, by default it prints them to os.Stderr
|
||||
func (s *Scanner) err(msg string) {
|
||||
s.ErrorCount++
|
||||
pos := s.recentPosition()
|
||||
|
||||
if s.Error != nil {
|
||||
s.Error(pos, msg)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "%s: %s\n", pos, msg)
|
||||
}
|
||||
|
||||
// isHexadecimal returns true if the given rune is a letter
|
||||
func isLetter(ch rune) bool {
|
||||
return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= 0x80 && unicode.IsLetter(ch)
|
||||
}
|
||||
|
||||
// isDigit returns true if the given rune is a decimal digit
|
||||
func isDigit(ch rune) bool {
|
||||
return '0' <= ch && ch <= '9' || ch >= 0x80 && unicode.IsDigit(ch)
|
||||
}
|
||||
|
||||
// isDecimal returns true if the given rune is a decimal number
|
||||
func isDecimal(ch rune) bool {
|
||||
return '0' <= ch && ch <= '9'
|
||||
}
|
||||
|
||||
// isHexadecimal returns true if the given rune is an hexadecimal number
|
||||
func isHexadecimal(ch rune) bool {
|
||||
return '0' <= ch && ch <= '9' || 'a' <= ch && ch <= 'f' || 'A' <= ch && ch <= 'F'
|
||||
}
|
||||
|
||||
// isWhitespace returns true if the rune is a space, tab, newline or carriage return
|
||||
func isWhitespace(ch rune) bool {
|
||||
return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'
|
||||
}
|
||||
|
||||
// digitVal returns the integer value of a given octal,decimal or hexadecimal rune
|
||||
func digitVal(ch rune) int {
|
||||
switch {
|
||||
case '0' <= ch && ch <= '9':
|
||||
return int(ch - '0')
|
||||
case 'a' <= ch && ch <= 'f':
|
||||
return int(ch - 'a' + 10)
|
||||
case 'A' <= ch && ch <= 'F':
|
||||
return int(ch - 'A' + 10)
|
||||
}
|
||||
return 16 // larger than any legal digit val
|
||||
}
|
@ -1,241 +0,0 @@
|
||||
package strconv
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// ErrSyntax indicates that a value does not have the right syntax for the target type.
|
||||
var ErrSyntax = errors.New("invalid syntax")
|
||||
|
||||
// Unquote interprets s as a single-quoted, double-quoted,
|
||||
// or backquoted Go string literal, returning the string value
|
||||
// that s quotes. (If s is single-quoted, it would be a Go
|
||||
// character literal; Unquote returns the corresponding
|
||||
// one-character string.)
|
||||
func Unquote(s string) (t string, err error) {
|
||||
n := len(s)
|
||||
if n < 2 {
|
||||
return "", ErrSyntax
|
||||
}
|
||||
quote := s[0]
|
||||
if quote != s[n-1] {
|
||||
return "", ErrSyntax
|
||||
}
|
||||
s = s[1 : n-1]
|
||||
|
||||
if quote != '"' {
|
||||
return "", ErrSyntax
|
||||
}
|
||||
if !contains(s, '$') && !contains(s, '{') && contains(s, '\n') {
|
||||
return "", ErrSyntax
|
||||
}
|
||||
|
||||
// Is it trivial? Avoid allocation.
|
||||
if !contains(s, '\\') && !contains(s, quote) && !contains(s, '$') {
|
||||
switch quote {
|
||||
case '"':
|
||||
return s, nil
|
||||
case '\'':
|
||||
r, size := utf8.DecodeRuneInString(s)
|
||||
if size == len(s) && (r != utf8.RuneError || size != 1) {
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var runeTmp [utf8.UTFMax]byte
|
||||
buf := make([]byte, 0, 3*len(s)/2) // Try to avoid more allocations.
|
||||
for len(s) > 0 {
|
||||
// If we're starting a '${}' then let it through un-unquoted.
|
||||
// Specifically: we don't unquote any characters within the `${}`
|
||||
// section.
|
||||
if s[0] == '$' && len(s) > 1 && s[1] == '{' {
|
||||
buf = append(buf, '$', '{')
|
||||
s = s[2:]
|
||||
|
||||
// Continue reading until we find the closing brace, copying as-is
|
||||
braces := 1
|
||||
for len(s) > 0 && braces > 0 {
|
||||
r, size := utf8.DecodeRuneInString(s)
|
||||
if r == utf8.RuneError {
|
||||
return "", ErrSyntax
|
||||
}
|
||||
|
||||
s = s[size:]
|
||||
|
||||
n := utf8.EncodeRune(runeTmp[:], r)
|
||||
buf = append(buf, runeTmp[:n]...)
|
||||
|
||||
switch r {
|
||||
case '{':
|
||||
braces++
|
||||
case '}':
|
||||
braces--
|
||||
}
|
||||
}
|
||||
if braces != 0 {
|
||||
return "", ErrSyntax
|
||||
}
|
||||
if len(s) == 0 {
|
||||
// If there's no string left, we're done!
|
||||
break
|
||||
} else {
|
||||
// If there's more left, we need to pop back up to the top of the loop
|
||||
// in case there's another interpolation in this string.
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if s[0] == '\n' {
|
||||
return "", ErrSyntax
|
||||
}
|
||||
|
||||
c, multibyte, ss, err := unquoteChar(s, quote)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
s = ss
|
||||
if c < utf8.RuneSelf || !multibyte {
|
||||
buf = append(buf, byte(c))
|
||||
} else {
|
||||
n := utf8.EncodeRune(runeTmp[:], c)
|
||||
buf = append(buf, runeTmp[:n]...)
|
||||
}
|
||||
if quote == '\'' && len(s) != 0 {
|
||||
// single-quoted must be single character
|
||||
return "", ErrSyntax
|
||||
}
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
// contains reports whether the string contains the byte c.
|
||||
func contains(s string, c byte) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == c {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func unhex(b byte) (v rune, ok bool) {
|
||||
c := rune(b)
|
||||
switch {
|
||||
case '0' <= c && c <= '9':
|
||||
return c - '0', true
|
||||
case 'a' <= c && c <= 'f':
|
||||
return c - 'a' + 10, true
|
||||
case 'A' <= c && c <= 'F':
|
||||
return c - 'A' + 10, true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func unquoteChar(s string, quote byte) (value rune, multibyte bool, tail string, err error) {
|
||||
// easy cases
|
||||
switch c := s[0]; {
|
||||
case c == quote && (quote == '\'' || quote == '"'):
|
||||
err = ErrSyntax
|
||||
return
|
||||
case c >= utf8.RuneSelf:
|
||||
r, size := utf8.DecodeRuneInString(s)
|
||||
return r, true, s[size:], nil
|
||||
case c != '\\':
|
||||
return rune(s[0]), false, s[1:], nil
|
||||
}
|
||||
|
||||
// hard case: c is backslash
|
||||
if len(s) <= 1 {
|
||||
err = ErrSyntax
|
||||
return
|
||||
}
|
||||
c := s[1]
|
||||
s = s[2:]
|
||||
|
||||
switch c {
|
||||
case 'a':
|
||||
value = '\a'
|
||||
case 'b':
|
||||
value = '\b'
|
||||
case 'f':
|
||||
value = '\f'
|
||||
case 'n':
|
||||
value = '\n'
|
||||
case 'r':
|
||||
value = '\r'
|
||||
case 't':
|
||||
value = '\t'
|
||||
case 'v':
|
||||
value = '\v'
|
||||
case 'x', 'u', 'U':
|
||||
n := 0
|
||||
switch c {
|
||||
case 'x':
|
||||
n = 2
|
||||
case 'u':
|
||||
n = 4
|
||||
case 'U':
|
||||
n = 8
|
||||
}
|
||||
var v rune
|
||||
if len(s) < n {
|
||||
err = ErrSyntax
|
||||
return
|
||||
}
|
||||
for j := 0; j < n; j++ {
|
||||
x, ok := unhex(s[j])
|
||||
if !ok {
|
||||
err = ErrSyntax
|
||||
return
|
||||
}
|
||||
v = v<<4 | x
|
||||
}
|
||||
s = s[n:]
|
||||
if c == 'x' {
|
||||
// single-byte string, possibly not UTF-8
|
||||
value = v
|
||||
break
|
||||
}
|
||||
if v > utf8.MaxRune {
|
||||
err = ErrSyntax
|
||||
return
|
||||
}
|
||||
value = v
|
||||
multibyte = true
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7':
|
||||
v := rune(c) - '0'
|
||||
if len(s) < 2 {
|
||||
err = ErrSyntax
|
||||
return
|
||||
}
|
||||
for j := 0; j < 2; j++ { // one digit already; two more
|
||||
x := rune(s[j]) - '0'
|
||||
if x < 0 || x > 7 {
|
||||
err = ErrSyntax
|
||||
return
|
||||
}
|
||||
v = (v << 3) | x
|
||||
}
|
||||
s = s[2:]
|
||||
if v > 255 {
|
||||
err = ErrSyntax
|
||||
return
|
||||
}
|
||||
value = v
|
||||
case '\\':
|
||||
value = '\\'
|
||||
case '\'', '"':
|
||||
if c != quote {
|
||||
err = ErrSyntax
|
||||
return
|
||||
}
|
||||
value = rune(c)
|
||||
default:
|
||||
err = ErrSyntax
|
||||
return
|
||||
}
|
||||
tail = s
|
||||
return
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package token
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Pos describes an arbitrary source position
|
||||
// including the file, line, and column location.
|
||||
// A Position is valid if the line number is > 0.
|
||||
type Pos struct {
|
||||
Filename string // filename, if any
|
||||
Offset int // offset, starting at 0
|
||||
Line int // line number, starting at 1
|
||||
Column int // column number, starting at 1 (character count)
|
||||
}
|
||||
|
||||
// IsValid returns true if the position is valid.
|
||||
func (p *Pos) IsValid() bool { return p.Line > 0 }
|
||||
|
||||
// String returns a string in one of several forms:
|
||||
//
|
||||
// file:line:column valid position with file name
|
||||
// line:column valid position without file name
|
||||
// file invalid position with file name
|
||||
// - invalid position without file name
|
||||
func (p Pos) String() string {
|
||||
s := p.Filename
|
||||
if p.IsValid() {
|
||||
if s != "" {
|
||||
s += ":"
|
||||
}
|
||||
s += fmt.Sprintf("%d:%d", p.Line, p.Column)
|
||||
}
|
||||
if s == "" {
|
||||
s = "-"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Before reports whether the position p is before u.
|
||||
func (p Pos) Before(u Pos) bool {
|
||||
return u.Offset > p.Offset || u.Line > p.Line
|
||||
}
|
||||
|
||||
// After reports whether the position p is after u.
|
||||
func (p Pos) After(u Pos) bool {
|
||||
return u.Offset < p.Offset || u.Line < p.Line
|
||||
}
|
@ -1,219 +0,0 @@
|
||||
// Package token defines constants representing the lexical tokens for HCL
|
||||
// (HashiCorp Configuration Language)
|
||||
package token
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
hclstrconv "github.com/hashicorp/hcl/hcl/strconv"
|
||||
)
|
||||
|
||||
// Token defines a single HCL token which can be obtained via the Scanner
|
||||
type Token struct {
|
||||
Type Type
|
||||
Pos Pos
|
||||
Text string
|
||||
JSON bool
|
||||
}
|
||||
|
||||
// Type is the set of lexical tokens of the HCL (HashiCorp Configuration Language)
|
||||
type Type int
|
||||
|
||||
const (
|
||||
// Special tokens
|
||||
ILLEGAL Type = iota
|
||||
EOF
|
||||
COMMENT
|
||||
|
||||
identifier_beg
|
||||
IDENT // literals
|
||||
literal_beg
|
||||
NUMBER // 12345
|
||||
FLOAT // 123.45
|
||||
BOOL // true,false
|
||||
STRING // "abc"
|
||||
HEREDOC // <<FOO\nbar\nFOO
|
||||
literal_end
|
||||
identifier_end
|
||||
|
||||
operator_beg
|
||||
LBRACK // [
|
||||
LBRACE // {
|
||||
COMMA // ,
|
||||
PERIOD // .
|
||||
|
||||
RBRACK // ]
|
||||
RBRACE // }
|
||||
|
||||
ASSIGN // =
|
||||
ADD // +
|
||||
SUB // -
|
||||
operator_end
|
||||
)
|
||||
|
||||
var tokens = [...]string{
|
||||
ILLEGAL: "ILLEGAL",
|
||||
|
||||
EOF: "EOF",
|
||||
COMMENT: "COMMENT",
|
||||
|
||||
IDENT: "IDENT",
|
||||
NUMBER: "NUMBER",
|
||||
FLOAT: "FLOAT",
|
||||
BOOL: "BOOL",
|
||||
STRING: "STRING",
|
||||
|
||||
LBRACK: "LBRACK",
|
||||
LBRACE: "LBRACE",
|
||||
COMMA: "COMMA",
|
||||
PERIOD: "PERIOD",
|
||||
HEREDOC: "HEREDOC",
|
||||
|
||||
RBRACK: "RBRACK",
|
||||
RBRACE: "RBRACE",
|
||||
|
||||
ASSIGN: "ASSIGN",
|
||||
ADD: "ADD",
|
||||
SUB: "SUB",
|
||||
}
|
||||
|
||||
// String returns the string corresponding to the token tok.
|
||||
func (t Type) String() string {
|
||||
s := ""
|
||||
if 0 <= t && t < Type(len(tokens)) {
|
||||
s = tokens[t]
|
||||
}
|
||||
if s == "" {
|
||||
s = "token(" + strconv.Itoa(int(t)) + ")"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// IsIdentifier returns true for tokens corresponding to identifiers and basic
|
||||
// type literals; it returns false otherwise.
|
||||
func (t Type) IsIdentifier() bool { return identifier_beg < t && t < identifier_end }
|
||||
|
||||
// IsLiteral returns true for tokens corresponding to basic type literals; it
|
||||
// returns false otherwise.
|
||||
func (t Type) IsLiteral() bool { return literal_beg < t && t < literal_end }
|
||||
|
||||
// IsOperator returns true for tokens corresponding to operators and
|
||||
// delimiters; it returns false otherwise.
|
||||
func (t Type) IsOperator() bool { return operator_beg < t && t < operator_end }
|
||||
|
||||
// String returns the token's literal text. Note that this is only
|
||||
// applicable for certain token types, such as token.IDENT,
|
||||
// token.STRING, etc..
|
||||
func (t Token) String() string {
|
||||
return fmt.Sprintf("%s %s %s", t.Pos.String(), t.Type.String(), t.Text)
|
||||
}
|
||||
|
||||
// Value returns the properly typed value for this token. The type of
|
||||
// the returned interface{} is guaranteed based on the Type field.
|
||||
//
|
||||
// This can only be called for literal types. If it is called for any other
|
||||
// type, this will panic.
|
||||
func (t Token) Value() interface{} {
|
||||
switch t.Type {
|
||||
case BOOL:
|
||||
if t.Text == "true" {
|
||||
return true
|
||||
} else if t.Text == "false" {
|
||||
return false
|
||||
}
|
||||
|
||||
panic("unknown bool value: " + t.Text)
|
||||
case FLOAT:
|
||||
v, err := strconv.ParseFloat(t.Text, 64)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return float64(v)
|
||||
case NUMBER:
|
||||
v, err := strconv.ParseInt(t.Text, 0, 64)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return int64(v)
|
||||
case IDENT:
|
||||
return t.Text
|
||||
case HEREDOC:
|
||||
return unindentHeredoc(t.Text)
|
||||
case STRING:
|
||||
// Determine the Unquote method to use. If it came from JSON,
|
||||
// then we need to use the built-in unquote since we have to
|
||||
// escape interpolations there.
|
||||
f := hclstrconv.Unquote
|
||||
if t.JSON {
|
||||
f = strconv.Unquote
|
||||
}
|
||||
|
||||
// This case occurs if json null is used
|
||||
if t.Text == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
v, err := f(t.Text)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("unquote %s err: %s", t.Text, err))
|
||||
}
|
||||
|
||||
return v
|
||||
default:
|
||||
panic(fmt.Sprintf("unimplemented Value for type: %s", t.Type))
|
||||
}
|
||||
}
|
||||
|
||||
// unindentHeredoc returns the string content of a HEREDOC if it is started with <<
|
||||
// and the content of a HEREDOC with the hanging indent removed if it is started with
|
||||
// a <<-, and the terminating line is at least as indented as the least indented line.
|
||||
func unindentHeredoc(heredoc string) string {
|
||||
// We need to find the end of the marker
|
||||
idx := strings.IndexByte(heredoc, '\n')
|
||||
if idx == -1 {
|
||||
panic("heredoc doesn't contain newline")
|
||||
}
|
||||
|
||||
unindent := heredoc[2] == '-'
|
||||
|
||||
// We can optimize if the heredoc isn't marked for indentation
|
||||
if !unindent {
|
||||
return string(heredoc[idx+1 : len(heredoc)-idx+1])
|
||||
}
|
||||
|
||||
// We need to unindent each line based on the indentation level of the marker
|
||||
lines := strings.Split(string(heredoc[idx+1:len(heredoc)-idx+2]), "\n")
|
||||
whitespacePrefix := lines[len(lines)-1]
|
||||
|
||||
isIndented := true
|
||||
for _, v := range lines {
|
||||
if strings.HasPrefix(v, whitespacePrefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
isIndented = false
|
||||
break
|
||||
}
|
||||
|
||||
// If all lines are not at least as indented as the terminating mark, return the
|
||||
// heredoc as is, but trim the leading space from the marker on the final line.
|
||||
if !isIndented {
|
||||
return strings.TrimRight(string(heredoc[idx+1:len(heredoc)-idx+1]), " \t")
|
||||
}
|
||||
|
||||
unindentedLines := make([]string, len(lines))
|
||||
for k, v := range lines {
|
||||
if k == len(lines)-1 {
|
||||
unindentedLines[k] = ""
|
||||
break
|
||||
}
|
||||
|
||||
unindentedLines[k] = strings.TrimPrefix(v, whitespacePrefix)
|
||||
}
|
||||
|
||||
return strings.Join(unindentedLines, "\n")
|
||||
}
|
@ -1,117 +0,0 @@
|
||||
package parser
|
||||
|
||||
import "github.com/hashicorp/hcl/hcl/ast"
|
||||
|
||||
// flattenObjects takes an AST node, walks it, and flattens
|
||||
func flattenObjects(node ast.Node) {
|
||||
ast.Walk(node, func(n ast.Node) (ast.Node, bool) {
|
||||
// We only care about lists, because this is what we modify
|
||||
list, ok := n.(*ast.ObjectList)
|
||||
if !ok {
|
||||
return n, true
|
||||
}
|
||||
|
||||
// Rebuild the item list
|
||||
items := make([]*ast.ObjectItem, 0, len(list.Items))
|
||||
frontier := make([]*ast.ObjectItem, len(list.Items))
|
||||
copy(frontier, list.Items)
|
||||
for len(frontier) > 0 {
|
||||
// Pop the current item
|
||||
n := len(frontier)
|
||||
item := frontier[n-1]
|
||||
frontier = frontier[:n-1]
|
||||
|
||||
switch v := item.Val.(type) {
|
||||
case *ast.ObjectType:
|
||||
items, frontier = flattenObjectType(v, item, items, frontier)
|
||||
case *ast.ListType:
|
||||
items, frontier = flattenListType(v, item, items, frontier)
|
||||
default:
|
||||
items = append(items, item)
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse the list since the frontier model runs things backwards
|
||||
for i := len(items)/2 - 1; i >= 0; i-- {
|
||||
opp := len(items) - 1 - i
|
||||
items[i], items[opp] = items[opp], items[i]
|
||||
}
|
||||
|
||||
// Done! Set the original items
|
||||
list.Items = items
|
||||
return n, true
|
||||
})
|
||||
}
|
||||
|
||||
func flattenListType(
|
||||
ot *ast.ListType,
|
||||
item *ast.ObjectItem,
|
||||
items []*ast.ObjectItem,
|
||||
frontier []*ast.ObjectItem) ([]*ast.ObjectItem, []*ast.ObjectItem) {
|
||||
// If the list is empty, keep the original list
|
||||
if len(ot.List) == 0 {
|
||||
items = append(items, item)
|
||||
return items, frontier
|
||||
}
|
||||
|
||||
// All the elements of this object must also be objects!
|
||||
for _, subitem := range ot.List {
|
||||
if _, ok := subitem.(*ast.ObjectType); !ok {
|
||||
items = append(items, item)
|
||||
return items, frontier
|
||||
}
|
||||
}
|
||||
|
||||
// Great! We have a match go through all the items and flatten
|
||||
for _, elem := range ot.List {
|
||||
// Add it to the frontier so that we can recurse
|
||||
frontier = append(frontier, &ast.ObjectItem{
|
||||
Keys: item.Keys,
|
||||
Assign: item.Assign,
|
||||
Val: elem,
|
||||
LeadComment: item.LeadComment,
|
||||
LineComment: item.LineComment,
|
||||
})
|
||||
}
|
||||
|
||||
return items, frontier
|
||||
}
|
||||
|
||||
func flattenObjectType(
|
||||
ot *ast.ObjectType,
|
||||
item *ast.ObjectItem,
|
||||
items []*ast.ObjectItem,
|
||||
frontier []*ast.ObjectItem) ([]*ast.ObjectItem, []*ast.ObjectItem) {
|
||||
// If the list has no items we do not have to flatten anything
|
||||
if ot.List.Items == nil {
|
||||
items = append(items, item)
|
||||
return items, frontier
|
||||
}
|
||||
|
||||
// All the elements of this object must also be objects!
|
||||
for _, subitem := range ot.List.Items {
|
||||
if _, ok := subitem.Val.(*ast.ObjectType); !ok {
|
||||
items = append(items, item)
|
||||
return items, frontier
|
||||
}
|
||||
}
|
||||
|
||||
// Great! We have a match go through all the items and flatten
|
||||
for _, subitem := range ot.List.Items {
|
||||
// Copy the new key
|
||||
keys := make([]*ast.ObjectKey, len(item.Keys)+len(subitem.Keys))
|
||||
copy(keys, item.Keys)
|
||||
copy(keys[len(item.Keys):], subitem.Keys)
|
||||
|
||||
// Add it to the frontier so that we can recurse
|
||||
frontier = append(frontier, &ast.ObjectItem{
|
||||
Keys: keys,
|
||||
Assign: item.Assign,
|
||||
Val: subitem.Val,
|
||||
LeadComment: item.LeadComment,
|
||||
LineComment: item.LineComment,
|
||||
})
|
||||
}
|
||||
|
||||
return items, frontier
|
||||
}
|
@ -1,313 +0,0 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl/hcl/ast"
|
||||
hcltoken "github.com/hashicorp/hcl/hcl/token"
|
||||
"github.com/hashicorp/hcl/json/scanner"
|
||||
"github.com/hashicorp/hcl/json/token"
|
||||
)
|
||||
|
||||
type Parser struct {
|
||||
sc *scanner.Scanner
|
||||
|
||||
// Last read token
|
||||
tok token.Token
|
||||
commaPrev token.Token
|
||||
|
||||
enableTrace bool
|
||||
indent int
|
||||
n int // buffer size (max = 1)
|
||||
}
|
||||
|
||||
func newParser(src []byte) *Parser {
|
||||
return &Parser{
|
||||
sc: scanner.New(src),
|
||||
}
|
||||
}
|
||||
|
||||
// Parse returns the fully parsed source and returns the abstract syntax tree.
|
||||
func Parse(src []byte) (*ast.File, error) {
|
||||
p := newParser(src)
|
||||
return p.Parse()
|
||||
}
|
||||
|
||||
var errEofToken = errors.New("EOF token found")
|
||||
|
||||
// Parse returns the fully parsed source and returns the abstract syntax tree.
|
||||
func (p *Parser) Parse() (*ast.File, error) {
|
||||
f := &ast.File{}
|
||||
var err, scerr error
|
||||
p.sc.Error = func(pos token.Pos, msg string) {
|
||||
scerr = fmt.Errorf("%s: %s", pos, msg)
|
||||
}
|
||||
|
||||
// The root must be an object in JSON
|
||||
object, err := p.object()
|
||||
if scerr != nil {
|
||||
return nil, scerr
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We make our final node an object list so it is more HCL compatible
|
||||
f.Node = object.List
|
||||
|
||||
// Flatten it, which finds patterns and turns them into more HCL-like
|
||||
// AST trees.
|
||||
flattenObjects(f.Node)
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (p *Parser) objectList() (*ast.ObjectList, error) {
|
||||
defer un(trace(p, "ParseObjectList"))
|
||||
node := &ast.ObjectList{}
|
||||
|
||||
for {
|
||||
n, err := p.objectItem()
|
||||
if err == errEofToken {
|
||||
break // we are finished
|
||||
}
|
||||
|
||||
// we don't return a nil node, because might want to use already
|
||||
// collected items.
|
||||
if err != nil {
|
||||
return node, err
|
||||
}
|
||||
|
||||
node.Add(n)
|
||||
|
||||
// Check for a followup comma. If it isn't a comma, then we're done
|
||||
if tok := p.scan(); tok.Type != token.COMMA {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return node, nil
|
||||
}
|
||||
|
||||
// objectItem parses a single object item
|
||||
func (p *Parser) objectItem() (*ast.ObjectItem, error) {
|
||||
defer un(trace(p, "ParseObjectItem"))
|
||||
|
||||
keys, err := p.objectKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
o := &ast.ObjectItem{
|
||||
Keys: keys,
|
||||
}
|
||||
|
||||
switch p.tok.Type {
|
||||
case token.COLON:
|
||||
pos := p.tok.Pos
|
||||
o.Assign = hcltoken.Pos{
|
||||
Filename: pos.Filename,
|
||||
Offset: pos.Offset,
|
||||
Line: pos.Line,
|
||||
Column: pos.Column,
|
||||
}
|
||||
|
||||
o.Val, err = p.objectValue()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// objectKey parses an object key and returns a ObjectKey AST
|
||||
func (p *Parser) objectKey() ([]*ast.ObjectKey, error) {
|
||||
keyCount := 0
|
||||
keys := make([]*ast.ObjectKey, 0)
|
||||
|
||||
for {
|
||||
tok := p.scan()
|
||||
switch tok.Type {
|
||||
case token.EOF:
|
||||
return nil, errEofToken
|
||||
case token.STRING:
|
||||
keyCount++
|
||||
keys = append(keys, &ast.ObjectKey{
|
||||
Token: p.tok.HCLToken(),
|
||||
})
|
||||
case token.COLON:
|
||||
// If we have a zero keycount it means that we never got
|
||||
// an object key, i.e. `{ :`. This is a syntax error.
|
||||
if keyCount == 0 {
|
||||
return nil, fmt.Errorf("expected: STRING got: %s", p.tok.Type)
|
||||
}
|
||||
|
||||
// Done
|
||||
return keys, nil
|
||||
case token.ILLEGAL:
|
||||
return nil, errors.New("illegal")
|
||||
default:
|
||||
return nil, fmt.Errorf("expected: STRING got: %s", p.tok.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// object parses any type of object, such as number, bool, string, object or
|
||||
// list.
|
||||
func (p *Parser) objectValue() (ast.Node, error) {
|
||||
defer un(trace(p, "ParseObjectValue"))
|
||||
tok := p.scan()
|
||||
|
||||
switch tok.Type {
|
||||
case token.NUMBER, token.FLOAT, token.BOOL, token.NULL, token.STRING:
|
||||
return p.literalType()
|
||||
case token.LBRACE:
|
||||
return p.objectType()
|
||||
case token.LBRACK:
|
||||
return p.listType()
|
||||
case token.EOF:
|
||||
return nil, errEofToken
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Expected object value, got unknown token: %+v", tok)
|
||||
}
|
||||
|
||||
// object parses any type of object, such as number, bool, string, object or
|
||||
// list.
|
||||
func (p *Parser) object() (*ast.ObjectType, error) {
|
||||
defer un(trace(p, "ParseType"))
|
||||
tok := p.scan()
|
||||
|
||||
switch tok.Type {
|
||||
case token.LBRACE:
|
||||
return p.objectType()
|
||||
case token.EOF:
|
||||
return nil, errEofToken
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Expected object, got unknown token: %+v", tok)
|
||||
}
|
||||
|
||||
// objectType parses an object type and returns a ObjectType AST
|
||||
func (p *Parser) objectType() (*ast.ObjectType, error) {
|
||||
defer un(trace(p, "ParseObjectType"))
|
||||
|
||||
// we assume that the currently scanned token is a LBRACE
|
||||
o := &ast.ObjectType{}
|
||||
|
||||
l, err := p.objectList()
|
||||
|
||||
// if we hit RBRACE, we are good to go (means we parsed all Items), if it's
|
||||
// not a RBRACE, it's an syntax error and we just return it.
|
||||
if err != nil && p.tok.Type != token.RBRACE {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
o.List = l
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// listType parses a list type and returns a ListType AST
|
||||
func (p *Parser) listType() (*ast.ListType, error) {
|
||||
defer un(trace(p, "ParseListType"))
|
||||
|
||||
// we assume that the currently scanned token is a LBRACK
|
||||
l := &ast.ListType{}
|
||||
|
||||
for {
|
||||
tok := p.scan()
|
||||
switch tok.Type {
|
||||
case token.NUMBER, token.FLOAT, token.STRING:
|
||||
node, err := p.literalType()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l.Add(node)
|
||||
case token.COMMA:
|
||||
continue
|
||||
case token.LBRACE:
|
||||
node, err := p.objectType()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l.Add(node)
|
||||
case token.BOOL:
|
||||
// TODO(arslan) should we support? not supported by HCL yet
|
||||
case token.LBRACK:
|
||||
// TODO(arslan) should we support nested lists? Even though it's
|
||||
// written in README of HCL, it's not a part of the grammar
|
||||
// (not defined in parse.y)
|
||||
case token.RBRACK:
|
||||
// finished
|
||||
return l, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected token while parsing list: %s", tok.Type)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// literalType parses a literal type and returns a LiteralType AST
|
||||
func (p *Parser) literalType() (*ast.LiteralType, error) {
|
||||
defer un(trace(p, "ParseLiteral"))
|
||||
|
||||
return &ast.LiteralType{
|
||||
Token: p.tok.HCLToken(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// scan returns the next token from the underlying scanner. If a token has
|
||||
// been unscanned then read that instead.
|
||||
func (p *Parser) scan() token.Token {
|
||||
// If we have a token on the buffer, then return it.
|
||||
if p.n != 0 {
|
||||
p.n = 0
|
||||
return p.tok
|
||||
}
|
||||
|
||||
p.tok = p.sc.Scan()
|
||||
return p.tok
|
||||
}
|
||||
|
||||
// unscan pushes the previously read token back onto the buffer.
|
||||
func (p *Parser) unscan() {
|
||||
p.n = 1
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Parsing support
|
||||
|
||||
func (p *Parser) printTrace(a ...interface{}) {
|
||||
if !p.enableTrace {
|
||||
return
|
||||
}
|
||||
|
||||
const dots = ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . "
|
||||
const n = len(dots)
|
||||
fmt.Printf("%5d:%3d: ", p.tok.Pos.Line, p.tok.Pos.Column)
|
||||
|
||||
i := 2 * p.indent
|
||||
for i > n {
|
||||
fmt.Print(dots)
|
||||
i -= n
|
||||
}
|
||||
// i <= n
|
||||
fmt.Print(dots[0:i])
|
||||
fmt.Println(a...)
|
||||
}
|
||||
|
||||
func trace(p *Parser, msg string) *Parser {
|
||||
p.printTrace(msg, "(")
|
||||
p.indent++
|
||||
return p
|
||||
}
|
||||
|
||||
// Usage pattern: defer un(trace(p, "..."))
|
||||
func un(p *Parser) {
|
||||
p.indent--
|
||||
p.printTrace(")")
|
||||
}
|
@ -1,451 +0,0 @@
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/hashicorp/hcl/json/token"
|
||||
)
|
||||
|
||||
// eof represents a marker rune for the end of the reader.
|
||||
const eof = rune(0)
|
||||
|
||||
// Scanner defines a lexical scanner
|
||||
type Scanner struct {
|
||||
buf *bytes.Buffer // Source buffer for advancing and scanning
|
||||
src []byte // Source buffer for immutable access
|
||||
|
||||
// Source Position
|
||||
srcPos token.Pos // current position
|
||||
prevPos token.Pos // previous position, used for peek() method
|
||||
|
||||
lastCharLen int // length of last character in bytes
|
||||
lastLineLen int // length of last line in characters (for correct column reporting)
|
||||
|
||||
tokStart int // token text start position
|
||||
tokEnd int // token text end position
|
||||
|
||||
// Error is called for each error encountered. If no Error
|
||||
// function is set, the error is reported to os.Stderr.
|
||||
Error func(pos token.Pos, msg string)
|
||||
|
||||
// ErrorCount is incremented by one for each error encountered.
|
||||
ErrorCount int
|
||||
|
||||
// tokPos is the start position of most recently scanned token; set by
|
||||
// Scan. The Filename field is always left untouched by the Scanner. If
|
||||
// an error is reported (via Error) and Position is invalid, the scanner is
|
||||
// not inside a token.
|
||||
tokPos token.Pos
|
||||
}
|
||||
|
||||
// New creates and initializes a new instance of Scanner using src as
|
||||
// its source content.
|
||||
func New(src []byte) *Scanner {
|
||||
// even though we accept a src, we read from a io.Reader compatible type
|
||||
// (*bytes.Buffer). So in the future we might easily change it to streaming
|
||||
// read.
|
||||
b := bytes.NewBuffer(src)
|
||||
s := &Scanner{
|
||||
buf: b,
|
||||
src: src,
|
||||
}
|
||||
|
||||
// srcPosition always starts with 1
|
||||
s.srcPos.Line = 1
|
||||
return s
|
||||
}
|
||||
|
||||
// next reads the next rune from the bufferred reader. Returns the rune(0) if
|
||||
// an error occurs (or io.EOF is returned).
|
||||
func (s *Scanner) next() rune {
|
||||
ch, size, err := s.buf.ReadRune()
|
||||
if err != nil {
|
||||
// advance for error reporting
|
||||
s.srcPos.Column++
|
||||
s.srcPos.Offset += size
|
||||
s.lastCharLen = size
|
||||
return eof
|
||||
}
|
||||
|
||||
if ch == utf8.RuneError && size == 1 {
|
||||
s.srcPos.Column++
|
||||
s.srcPos.Offset += size
|
||||
s.lastCharLen = size
|
||||
s.err("illegal UTF-8 encoding")
|
||||
return ch
|
||||
}
|
||||
|
||||
// remember last position
|
||||
s.prevPos = s.srcPos
|
||||
|
||||
s.srcPos.Column++
|
||||
s.lastCharLen = size
|
||||
s.srcPos.Offset += size
|
||||
|
||||
if ch == '\n' {
|
||||
s.srcPos.Line++
|
||||
s.lastLineLen = s.srcPos.Column
|
||||
s.srcPos.Column = 0
|
||||
}
|
||||
|
||||
// debug
|
||||
// fmt.Printf("ch: %q, offset:column: %d:%d\n", ch, s.srcPos.Offset, s.srcPos.Column)
|
||||
return ch
|
||||
}
|
||||
|
||||
// unread unreads the previous read Rune and updates the source position
|
||||
func (s *Scanner) unread() {
|
||||
if err := s.buf.UnreadRune(); err != nil {
|
||||
panic(err) // this is user fault, we should catch it
|
||||
}
|
||||
s.srcPos = s.prevPos // put back last position
|
||||
}
|
||||
|
||||
// peek returns the next rune without advancing the reader.
|
||||
func (s *Scanner) peek() rune {
|
||||
peek, _, err := s.buf.ReadRune()
|
||||
if err != nil {
|
||||
return eof
|
||||
}
|
||||
|
||||
s.buf.UnreadRune()
|
||||
return peek
|
||||
}
|
||||
|
||||
// Scan scans the next token and returns the token.
|
||||
func (s *Scanner) Scan() token.Token {
|
||||
ch := s.next()
|
||||
|
||||
// skip white space
|
||||
for isWhitespace(ch) {
|
||||
ch = s.next()
|
||||
}
|
||||
|
||||
var tok token.Type
|
||||
|
||||
// token text markings
|
||||
s.tokStart = s.srcPos.Offset - s.lastCharLen
|
||||
|
||||
// token position, initial next() is moving the offset by one(size of rune
|
||||
// actually), though we are interested with the starting point
|
||||
s.tokPos.Offset = s.srcPos.Offset - s.lastCharLen
|
||||
if s.srcPos.Column > 0 {
|
||||
// common case: last character was not a '\n'
|
||||
s.tokPos.Line = s.srcPos.Line
|
||||
s.tokPos.Column = s.srcPos.Column
|
||||
} else {
|
||||
// last character was a '\n'
|
||||
// (we cannot be at the beginning of the source
|
||||
// since we have called next() at least once)
|
||||
s.tokPos.Line = s.srcPos.Line - 1
|
||||
s.tokPos.Column = s.lastLineLen
|
||||
}
|
||||
|
||||
switch {
|
||||
case isLetter(ch):
|
||||
lit := s.scanIdentifier()
|
||||
if lit == "true" || lit == "false" {
|
||||
tok = token.BOOL
|
||||
} else if lit == "null" {
|
||||
tok = token.NULL
|
||||
} else {
|
||||
s.err("illegal char")
|
||||
}
|
||||
case isDecimal(ch):
|
||||
tok = s.scanNumber(ch)
|
||||
default:
|
||||
switch ch {
|
||||
case eof:
|
||||
tok = token.EOF
|
||||
case '"':
|
||||
tok = token.STRING
|
||||
s.scanString()
|
||||
case '.':
|
||||
tok = token.PERIOD
|
||||
ch = s.peek()
|
||||
if isDecimal(ch) {
|
||||
tok = token.FLOAT
|
||||
ch = s.scanMantissa(ch)
|
||||
ch = s.scanExponent(ch)
|
||||
}
|
||||
case '[':
|
||||
tok = token.LBRACK
|
||||
case ']':
|
||||
tok = token.RBRACK
|
||||
case '{':
|
||||
tok = token.LBRACE
|
||||
case '}':
|
||||
tok = token.RBRACE
|
||||
case ',':
|
||||
tok = token.COMMA
|
||||
case ':':
|
||||
tok = token.COLON
|
||||
case '-':
|
||||
if isDecimal(s.peek()) {
|
||||
ch := s.next()
|
||||
tok = s.scanNumber(ch)
|
||||
} else {
|
||||
s.err("illegal char")
|
||||
}
|
||||
default:
|
||||
s.err("illegal char: " + string(ch))
|
||||
}
|
||||
}
|
||||
|
||||
// finish token ending
|
||||
s.tokEnd = s.srcPos.Offset
|
||||
|
||||
// create token literal
|
||||
var tokenText string
|
||||
if s.tokStart >= 0 {
|
||||
tokenText = string(s.src[s.tokStart:s.tokEnd])
|
||||
}
|
||||
s.tokStart = s.tokEnd // ensure idempotency of tokenText() call
|
||||
|
||||
return token.Token{
|
||||
Type: tok,
|
||||
Pos: s.tokPos,
|
||||
Text: tokenText,
|
||||
}
|
||||
}
|
||||
|
||||
// scanNumber scans a HCL number definition starting with the given rune
|
||||
func (s *Scanner) scanNumber(ch rune) token.Type {
|
||||
zero := ch == '0'
|
||||
pos := s.srcPos
|
||||
|
||||
s.scanMantissa(ch)
|
||||
ch = s.next() // seek forward
|
||||
if ch == 'e' || ch == 'E' {
|
||||
ch = s.scanExponent(ch)
|
||||
return token.FLOAT
|
||||
}
|
||||
|
||||
if ch == '.' {
|
||||
ch = s.scanFraction(ch)
|
||||
if ch == 'e' || ch == 'E' {
|
||||
ch = s.next()
|
||||
ch = s.scanExponent(ch)
|
||||
}
|
||||
return token.FLOAT
|
||||
}
|
||||
|
||||
if ch != eof {
|
||||
s.unread()
|
||||
}
|
||||
|
||||
// If we have a larger number and this is zero, error
|
||||
if zero && pos != s.srcPos {
|
||||
s.err("numbers cannot start with 0")
|
||||
}
|
||||
|
||||
return token.NUMBER
|
||||
}
|
||||
|
||||
// scanMantissa scans the mantissa beginning from the rune. It returns the next
|
||||
// non decimal rune. It's used to determine wheter it's a fraction or exponent.
|
||||
func (s *Scanner) scanMantissa(ch rune) rune {
|
||||
scanned := false
|
||||
for isDecimal(ch) {
|
||||
ch = s.next()
|
||||
scanned = true
|
||||
}
|
||||
|
||||
if scanned && ch != eof {
|
||||
s.unread()
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
// scanFraction scans the fraction after the '.' rune
|
||||
func (s *Scanner) scanFraction(ch rune) rune {
|
||||
if ch == '.' {
|
||||
ch = s.peek() // we peek just to see if we can move forward
|
||||
ch = s.scanMantissa(ch)
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
// scanExponent scans the remaining parts of an exponent after the 'e' or 'E'
|
||||
// rune.
|
||||
func (s *Scanner) scanExponent(ch rune) rune {
|
||||
if ch == 'e' || ch == 'E' {
|
||||
ch = s.next()
|
||||
if ch == '-' || ch == '+' {
|
||||
ch = s.next()
|
||||
}
|
||||
ch = s.scanMantissa(ch)
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
// scanString scans a quoted string
|
||||
func (s *Scanner) scanString() {
|
||||
braces := 0
|
||||
for {
|
||||
// '"' opening already consumed
|
||||
// read character after quote
|
||||
ch := s.next()
|
||||
|
||||
if ch == '\n' || ch < 0 || ch == eof {
|
||||
s.err("literal not terminated")
|
||||
return
|
||||
}
|
||||
|
||||
if ch == '"' {
|
||||
break
|
||||
}
|
||||
|
||||
// If we're going into a ${} then we can ignore quotes for awhile
|
||||
if braces == 0 && ch == '$' && s.peek() == '{' {
|
||||
braces++
|
||||
s.next()
|
||||
} else if braces > 0 && ch == '{' {
|
||||
braces++
|
||||
}
|
||||
if braces > 0 && ch == '}' {
|
||||
braces--
|
||||
}
|
||||
|
||||
if ch == '\\' {
|
||||
s.scanEscape()
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// scanEscape scans an escape sequence
|
||||
func (s *Scanner) scanEscape() rune {
|
||||
// http://en.cppreference.com/w/cpp/language/escape
|
||||
ch := s.next() // read character after '/'
|
||||
switch ch {
|
||||
case 'a', 'b', 'f', 'n', 'r', 't', 'v', '\\', '"':
|
||||
// nothing to do
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7':
|
||||
// octal notation
|
||||
ch = s.scanDigits(ch, 8, 3)
|
||||
case 'x':
|
||||
// hexademical notation
|
||||
ch = s.scanDigits(s.next(), 16, 2)
|
||||
case 'u':
|
||||
// universal character name
|
||||
ch = s.scanDigits(s.next(), 16, 4)
|
||||
case 'U':
|
||||
// universal character name
|
||||
ch = s.scanDigits(s.next(), 16, 8)
|
||||
default:
|
||||
s.err("illegal char escape")
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
// scanDigits scans a rune with the given base for n times. For example an
|
||||
// octal notation \184 would yield in scanDigits(ch, 8, 3)
|
||||
func (s *Scanner) scanDigits(ch rune, base, n int) rune {
|
||||
for n > 0 && digitVal(ch) < base {
|
||||
ch = s.next()
|
||||
n--
|
||||
}
|
||||
if n > 0 {
|
||||
s.err("illegal char escape")
|
||||
}
|
||||
|
||||
// we scanned all digits, put the last non digit char back
|
||||
s.unread()
|
||||
return ch
|
||||
}
|
||||
|
||||
// scanIdentifier scans an identifier and returns the literal string
|
||||
func (s *Scanner) scanIdentifier() string {
|
||||
offs := s.srcPos.Offset - s.lastCharLen
|
||||
ch := s.next()
|
||||
for isLetter(ch) || isDigit(ch) || ch == '-' {
|
||||
ch = s.next()
|
||||
}
|
||||
|
||||
if ch != eof {
|
||||
s.unread() // we got identifier, put back latest char
|
||||
}
|
||||
|
||||
return string(s.src[offs:s.srcPos.Offset])
|
||||
}
|
||||
|
||||
// recentPosition returns the position of the character immediately after the
|
||||
// character or token returned by the last call to Scan.
|
||||
func (s *Scanner) recentPosition() (pos token.Pos) {
|
||||
pos.Offset = s.srcPos.Offset - s.lastCharLen
|
||||
switch {
|
||||
case s.srcPos.Column > 0:
|
||||
// common case: last character was not a '\n'
|
||||
pos.Line = s.srcPos.Line
|
||||
pos.Column = s.srcPos.Column
|
||||
case s.lastLineLen > 0:
|
||||
// last character was a '\n'
|
||||
// (we cannot be at the beginning of the source
|
||||
// since we have called next() at least once)
|
||||
pos.Line = s.srcPos.Line - 1
|
||||
pos.Column = s.lastLineLen
|
||||
default:
|
||||
// at the beginning of the source
|
||||
pos.Line = 1
|
||||
pos.Column = 1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// err prints the error of any scanning to s.Error function. If the function is
|
||||
// not defined, by default it prints them to os.Stderr
|
||||
func (s *Scanner) err(msg string) {
|
||||
s.ErrorCount++
|
||||
pos := s.recentPosition()
|
||||
|
||||
if s.Error != nil {
|
||||
s.Error(pos, msg)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "%s: %s\n", pos, msg)
|
||||
}
|
||||
|
||||
// isHexadecimal returns true if the given rune is a letter
|
||||
func isLetter(ch rune) bool {
|
||||
return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= 0x80 && unicode.IsLetter(ch)
|
||||
}
|
||||
|
||||
// isHexadecimal returns true if the given rune is a decimal digit
|
||||
func isDigit(ch rune) bool {
|
||||
return '0' <= ch && ch <= '9' || ch >= 0x80 && unicode.IsDigit(ch)
|
||||
}
|
||||
|
||||
// isHexadecimal returns true if the given rune is a decimal number
|
||||
func isDecimal(ch rune) bool {
|
||||
return '0' <= ch && ch <= '9'
|
||||
}
|
||||
|
||||
// isHexadecimal returns true if the given rune is an hexadecimal number
|
||||
func isHexadecimal(ch rune) bool {
|
||||
return '0' <= ch && ch <= '9' || 'a' <= ch && ch <= 'f' || 'A' <= ch && ch <= 'F'
|
||||
}
|
||||
|
||||
// isWhitespace returns true if the rune is a space, tab, newline or carriage return
|
||||
func isWhitespace(ch rune) bool {
|
||||
return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'
|
||||
}
|
||||
|
||||
// digitVal returns the integer value of a given octal,decimal or hexadecimal rune
|
||||
func digitVal(ch rune) int {
|
||||
switch {
|
||||
case '0' <= ch && ch <= '9':
|
||||
return int(ch - '0')
|
||||
case 'a' <= ch && ch <= 'f':
|
||||
return int(ch - 'a' + 10)
|
||||
case 'A' <= ch && ch <= 'F':
|
||||
return int(ch - 'A' + 10)
|
||||
}
|
||||
return 16 // larger than any legal digit val
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package token
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Pos describes an arbitrary source position
|
||||
// including the file, line, and column location.
|
||||
// A Position is valid if the line number is > 0.
|
||||
type Pos struct {
|
||||
Filename string // filename, if any
|
||||
Offset int // offset, starting at 0
|
||||
Line int // line number, starting at 1
|
||||
Column int // column number, starting at 1 (character count)
|
||||
}
|
||||
|
||||
// IsValid returns true if the position is valid.
|
||||
func (p *Pos) IsValid() bool { return p.Line > 0 }
|
||||
|
||||
// String returns a string in one of several forms:
|
||||
//
|
||||
// file:line:column valid position with file name
|
||||
// line:column valid position without file name
|
||||
// file invalid position with file name
|
||||
// - invalid position without file name
|
||||
func (p Pos) String() string {
|
||||
s := p.Filename
|
||||
if p.IsValid() {
|
||||
if s != "" {
|
||||
s += ":"
|
||||
}
|
||||
s += fmt.Sprintf("%d:%d", p.Line, p.Column)
|
||||
}
|
||||
if s == "" {
|
||||
s = "-"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Before reports whether the position p is before u.
|
||||
func (p Pos) Before(u Pos) bool {
|
||||
return u.Offset > p.Offset || u.Line > p.Line
|
||||
}
|
||||
|
||||
// After reports whether the position p is after u.
|
||||
func (p Pos) After(u Pos) bool {
|
||||
return u.Offset < p.Offset || u.Line < p.Line
|
||||
}
|
@ -1,118 +0,0 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
hcltoken "github.com/hashicorp/hcl/hcl/token"
|
||||
)
|
||||
|
||||
// Token defines a single HCL token which can be obtained via the Scanner
|
||||
type Token struct {
|
||||
Type Type
|
||||
Pos Pos
|
||||
Text string
|
||||
}
|
||||
|
||||
// Type is the set of lexical tokens of the HCL (HashiCorp Configuration Language)
|
||||
type Type int
|
||||
|
||||
const (
|
||||
// Special tokens
|
||||
ILLEGAL Type = iota
|
||||
EOF
|
||||
|
||||
identifier_beg
|
||||
literal_beg
|
||||
NUMBER // 12345
|
||||
FLOAT // 123.45
|
||||
BOOL // true,false
|
||||
STRING // "abc"
|
||||
NULL // null
|
||||
literal_end
|
||||
identifier_end
|
||||
|
||||
operator_beg
|
||||
LBRACK // [
|
||||
LBRACE // {
|
||||
COMMA // ,
|
||||
PERIOD // .
|
||||
COLON // :
|
||||
|
||||
RBRACK // ]
|
||||
RBRACE // }
|
||||
|
||||
operator_end
|
||||
)
|
||||
|
||||
var tokens = [...]string{
|
||||
ILLEGAL: "ILLEGAL",
|
||||
|
||||
EOF: "EOF",
|
||||
|
||||
NUMBER: "NUMBER",
|
||||
FLOAT: "FLOAT",
|
||||
BOOL: "BOOL",
|
||||
STRING: "STRING",
|
||||
NULL: "NULL",
|
||||
|
||||
LBRACK: "LBRACK",
|
||||
LBRACE: "LBRACE",
|
||||
COMMA: "COMMA",
|
||||
PERIOD: "PERIOD",
|
||||
COLON: "COLON",
|
||||
|
||||
RBRACK: "RBRACK",
|
||||
RBRACE: "RBRACE",
|
||||
}
|
||||
|
||||
// String returns the string corresponding to the token tok.
|
||||
func (t Type) String() string {
|
||||
s := ""
|
||||
if 0 <= t && t < Type(len(tokens)) {
|
||||
s = tokens[t]
|
||||
}
|
||||
if s == "" {
|
||||
s = "token(" + strconv.Itoa(int(t)) + ")"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// IsIdentifier returns true for tokens corresponding to identifiers and basic
|
||||
// type literals; it returns false otherwise.
|
||||
func (t Type) IsIdentifier() bool { return identifier_beg < t && t < identifier_end }
|
||||
|
||||
// IsLiteral returns true for tokens corresponding to basic type literals; it
|
||||
// returns false otherwise.
|
||||
func (t Type) IsLiteral() bool { return literal_beg < t && t < literal_end }
|
||||
|
||||
// IsOperator returns true for tokens corresponding to operators and
|
||||
// delimiters; it returns false otherwise.
|
||||
func (t Type) IsOperator() bool { return operator_beg < t && t < operator_end }
|
||||
|
||||
// String returns the token's literal text. Note that this is only
|
||||
// applicable for certain token types, such as token.IDENT,
|
||||
// token.STRING, etc..
|
||||
func (t Token) String() string {
|
||||
return fmt.Sprintf("%s %s %s", t.Pos.String(), t.Type.String(), t.Text)
|
||||
}
|
||||
|
||||
// HCLToken converts this token to an HCL token.
|
||||
//
|
||||
// The token type must be a literal type or this will panic.
|
||||
func (t Token) HCLToken() hcltoken.Token {
|
||||
switch t.Type {
|
||||
case BOOL:
|
||||
return hcltoken.Token{Type: hcltoken.BOOL, Text: t.Text}
|
||||
case FLOAT:
|
||||
return hcltoken.Token{Type: hcltoken.FLOAT, Text: t.Text}
|
||||
case NULL:
|
||||
return hcltoken.Token{Type: hcltoken.STRING, Text: ""}
|
||||
case NUMBER:
|
||||
return hcltoken.Token{Type: hcltoken.NUMBER, Text: t.Text}
|
||||
case STRING:
|
||||
return hcltoken.Token{Type: hcltoken.STRING, Text: t.Text, JSON: true}
|
||||
default:
|
||||
panic(fmt.Sprintf("unimplemented HCLToken for type: %s", t.Type))
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
package hcl
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type lexModeValue byte
|
||||
|
||||
const (
|
||||
lexModeUnknown lexModeValue = iota
|
||||
lexModeHcl
|
||||
lexModeJson
|
||||
)
|
||||
|
||||
// lexMode returns whether we're going to be parsing in JSON
|
||||
// mode or HCL mode.
|
||||
func lexMode(v []byte) lexModeValue {
|
||||
var (
|
||||
r rune
|
||||
w int
|
||||
offset int
|
||||
)
|
||||
|
||||
for {
|
||||
r, w = utf8.DecodeRune(v[offset:])
|
||||
offset += w
|
||||
if unicode.IsSpace(r) {
|
||||
continue
|
||||
}
|
||||
if r == '{' {
|
||||
return lexModeJson
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return lexModeHcl
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package hcl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl/hcl/ast"
|
||||
hclParser "github.com/hashicorp/hcl/hcl/parser"
|
||||
jsonParser "github.com/hashicorp/hcl/json/parser"
|
||||
)
|
||||
|
||||
// ParseBytes accepts as input byte slice and returns ast tree.
|
||||
//
|
||||
// Input can be either JSON or HCL
|
||||
func ParseBytes(in []byte) (*ast.File, error) {
|
||||
return parse(in)
|
||||
}
|
||||
|
||||
// ParseString accepts input as a string and returns ast tree.
|
||||
func ParseString(input string) (*ast.File, error) {
|
||||
return parse([]byte(input))
|
||||
}
|
||||
|
||||
func parse(in []byte) (*ast.File, error) {
|
||||
switch lexMode(in) {
|
||||
case lexModeHcl:
|
||||
return hclParser.Parse(in)
|
||||
case lexModeJson:
|
||||
return jsonParser.Parse(in)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unknown config format")
|
||||
}
|
||||
|
||||
// Parse parses the given input and returns the root object.
|
||||
//
|
||||
// The input format can be either HCL or JSON.
|
||||
func Parse(input string) (*ast.File, error) {
|
||||
return parse([]byte(input))
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
# HCL Changelog
|
||||
|
||||
## v2.4.0 (Apr 13, 2020)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* The Unicode data tables that HCL uses to produce user-perceived "column" positions in diagnostics and other source ranges are now updated to Unicode 12.0.0, which will cause HCL to produce more accurate column numbers for combining characters introduced to Unicode since Unicode 9.0.0.
|
||||
|
||||
### Bugs Fixed
|
||||
|
||||
* json: Fix panic when parsing malformed JSON. ([#358](https://github.com/hashicorp/hcl/pull/358))
|
||||
|
||||
## v2.3.0 (Jan 3, 2020)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* ext/tryfunc: Optional functions `try` and `can` to include in your `hcl.EvalContext` when evaluating expressions, which allow users to make decisions based on the success of expressions. ([#330](https://github.com/hashicorp/hcl/pull/330))
|
||||
* ext/typeexpr: Now has an optional function `convert` which you can include in your `hcl.EvalContext` when evaluating expressions, allowing users to convert values to specific type constraints using the type constraint expression syntax. ([#330](https://github.com/hashicorp/hcl/pull/330))
|
||||
* ext/typeexpr: A new `cty` capsule type `typeexpr.TypeConstraintType` which, when used as either a type constraint for a function parameter or as a type constraint for a `hcldec` attribute specification will cause the given expression to be interpreted as a type constraint expression rather than a value expression. ([#330](https://github.com/hashicorp/hcl/pull/330))
|
||||
* ext/customdecode: An optional extension that allows overriding the static decoding behavior for expressions either in function arguments or `hcldec` attribute specifications. ([#330](https://github.com/hashicorp/hcl/pull/330))
|
||||
* ext/customdecode: New `cty` capsuletypes `customdecode.ExpressionType` and `customdecode.ExpressionClosureType` which, when used as either a type constraint for a function parameter or as a type constraint for a `hcldec` attribute specification will cause the given expression (and, for the closure type, also the `hcl.EvalContext` it was evaluated in) to be captured for later analysis, rather than immediately evaluated. ([#330](https://github.com/hashicorp/hcl/pull/330))
|
||||
|
||||
## v2.2.0 (Dec 11, 2019)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* hcldec: Attribute evaluation (as part of `AttrSpec` or `BlockAttrsSpec`) now captures expression evaluation metadata in any errors it produces during type conversions, allowing for better feedback in calling applications that are able to make use of this metadata when printing diagnostic messages. ([#329](https://github.com/hashicorp/hcl/pull/329))
|
||||
|
||||
### Bugs Fixed
|
||||
|
||||
* hclsyntax: `IndexExpr`, `SplatExpr`, and `RelativeTraversalExpr` will now report a source range that covers all of their child expression nodes. Previously they would report only the operator part, such as `["foo"]`, `[*]`, or `.foo`, which was problematic for callers using source ranges for code analysis. ([#328](https://github.com/hashicorp/hcl/pull/328))
|
||||
* hclwrite: Parser will no longer panic when the input includes index, splat, or relative traversal syntax. ([#328](https://github.com/hashicorp/hcl/pull/328))
|
||||
|
||||
## v2.1.0 (Nov 19, 2019)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* gohcl: When decoding into a struct value with some fields already populated, those values will be retained if not explicitly overwritten in the given HCL body, with similar overriding/merging behavior as `json.Unmarshal` in the Go standard library.
|
||||
* hclwrite: New interface to set the expression for an attribute to be a raw token sequence, with no special processing. This has some caveats, so if you intend to use it please refer to the godoc comments. ([#320](https://github.com/hashicorp/hcl/pull/320))
|
||||
|
||||
### Bugs Fixed
|
||||
|
||||
* hclwrite: The `Body.Blocks` method was returing the blocks in an indefined order, rather than preserving the order of declaration in the source input. ([#313](https://github.com/hashicorp/hcl/pull/313))
|
||||
* hclwrite: The `TokensForTraversal` function (and thus in turn the `Body.SetAttributeTraversal` method) was not correctly handling index steps in traversals, and thus producing invalid results. ([#319](https://github.com/hashicorp/hcl/pull/319))
|
||||
|
||||
## v2.0.0 (Oct 2, 2019)
|
||||
|
||||
Initial release of HCL 2, which is a new implementating combining the HCL 1
|
||||
language with the HIL expression language to produce a single language
|
||||
supporting both nested configuration structures and arbitrary expressions.
|
||||
|
||||
HCL 2 has an entirely new Go library API and so is _not_ a drop-in upgrade
|
||||
relative to HCL 1. It's possible to import both versions of HCL into a single
|
||||
program using Go's _semantic import versioning_ mechanism:
|
||||
|
||||
```
|
||||
import (
|
||||
hcl1 "github.com/hashicorp/hcl"
|
||||
hcl2 "github.com/hashicorp/hcl/v2"
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Prior to v2.0.0 there was not a curated changelog. Consult the git history
|
||||
from the latest v1.x.x tag for information on the changes to HCL 1.
|
1
vendor/github.com/hashicorp/hcl/LICENSE → vendor/github.com/hashicorp/hcl/v2/LICENSE
generated
vendored
1
vendor/github.com/hashicorp/hcl/LICENSE → vendor/github.com/hashicorp/hcl/v2/LICENSE
generated
vendored
@ -0,0 +1,205 @@
|
||||
# HCL
|
||||
|
||||
HCL is a toolkit for creating structured configuration languages that are
|
||||
both human- and machine-friendly, for use with command-line tools.
|
||||
Although intended to be generally useful, it is primarily targeted
|
||||
towards devops tools, servers, etc.
|
||||
|
||||
> **NOTE:** This is major version 2 of HCL, whose Go API is incompatible with
|
||||
> major version 1. Both versions are available for selection in Go Modules
|
||||
> projects. HCL 2 _cannot_ be imported from Go projects that are not using Go Modules. For more information, see
|
||||
> [our version selection guide](https://github.com/hashicorp/hcl/wiki/Version-Selection).
|
||||
|
||||
HCL has both a _native syntax_, intended to be pleasant to read and write for
|
||||
humans, and a JSON-based variant that is easier for machines to generate
|
||||
and parse.
|
||||
|
||||
The HCL native syntax is inspired by [libucl](https://github.com/vstakhov/libucl),
|
||||
[nginx configuration](http://nginx.org/en/docs/beginners_guide.html#conf_structure),
|
||||
and others.
|
||||
|
||||
It includes an expression syntax that allows basic inline computation and,
|
||||
with support from the calling application, use of variables and functions
|
||||
for more dynamic configuration languages.
|
||||
|
||||
HCL provides a set of constructs that can be used by a calling application to
|
||||
construct a configuration language. The application defines which attribute
|
||||
names and nested block types are expected, and HCL parses the configuration
|
||||
file, verifies that it conforms to the expected structure, and returns
|
||||
high-level objects that the application can use for further processing.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"github.com/hashicorp/hcl/v2/hclsimple"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
LogLevel string `hcl:"log_level"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
var config Config
|
||||
err := hclsimple.DecodeFile("config.hcl", nil, &config)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load configuration: %s", err)
|
||||
}
|
||||
log.Printf("Configuration is %#v", config)
|
||||
}
|
||||
```
|
||||
|
||||
A lower-level API is available for applications that need more control over
|
||||
the parsing, decoding, and evaluation of configuration. For more information,
|
||||
see [the package documentation](https://pkg.go.dev/github.com/hashicorp/hcl/v2).
|
||||
|
||||
## Why?
|
||||
|
||||
Newcomers to HCL often ask: why not JSON, YAML, etc?
|
||||
|
||||
Whereas JSON and YAML are formats for serializing data structures, HCL is
|
||||
a syntax and API specifically designed for building structured configuration
|
||||
formats.
|
||||
|
||||
HCL attempts to strike a compromise between generic serialization formats
|
||||
such as JSON and configuration formats built around full programming languages
|
||||
such as Ruby. HCL syntax is designed to be easily read and written by humans,
|
||||
and allows _declarative_ logic to permit its use in more complex applications.
|
||||
|
||||
HCL is intended as a base syntax for configuration formats built
|
||||
around key-value pairs and hierarchical blocks whose structure is well-defined
|
||||
by the calling application, and this definition of the configuration structure
|
||||
allows for better error messages and more convenient definition within the
|
||||
calling application.
|
||||
|
||||
It can't be denied that JSON is very convenient as a _lingua franca_
|
||||
for interoperability between different pieces of software. Because of this,
|
||||
HCL defines a common configuration model that can be parsed from either its
|
||||
native syntax or from a well-defined equivalent JSON structure. This allows
|
||||
configuration to be provided as a mixture of human-authored configuration
|
||||
files in the native syntax and machine-generated files in JSON.
|
||||
|
||||
## Information Model and Syntax
|
||||
|
||||
HCL is built around two primary concepts: _attributes_ and _blocks_. In
|
||||
native syntax, a configuration file for a hypothetical application might look
|
||||
something like this:
|
||||
|
||||
```hcl
|
||||
io_mode = "async"
|
||||
|
||||
service "http" "web_proxy" {
|
||||
listen_addr = "127.0.0.1:8080"
|
||||
|
||||
process "main" {
|
||||
command = ["/usr/local/bin/awesome-app", "server"]
|
||||
}
|
||||
|
||||
process "mgmt" {
|
||||
command = ["/usr/local/bin/awesome-app", "mgmt"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The JSON equivalent of this configuration is the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"io_mode": "async",
|
||||
"service": {
|
||||
"http": {
|
||||
"web_proxy": {
|
||||
"listen_addr": "127.0.0.1:8080",
|
||||
"process": {
|
||||
"main": {
|
||||
"command": ["/usr/local/bin/awesome-app", "server"]
|
||||
},
|
||||
"mgmt": {
|
||||
"command": ["/usr/local/bin/awesome-app", "mgmt"]
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Regardless of which syntax is used, the API within the calling application
|
||||
is the same. It can either work directly with the low-level attributes and
|
||||
blocks, for more advanced use-cases, or it can use one of the _decoder_
|
||||
packages to declaratively extract into either Go structs or dynamic value
|
||||
structures.
|
||||
|
||||
Attribute values can be expressions as well as just literal values:
|
||||
|
||||
```hcl
|
||||
# Arithmetic with literals and application-provided variables
|
||||
sum = 1 + addend
|
||||
|
||||
# String interpolation and templates
|
||||
message = "Hello, ${name}!"
|
||||
|
||||
# Application-provided functions
|
||||
shouty_message = upper(message)
|
||||
```
|
||||
|
||||
Although JSON syntax doesn't permit direct use of expressions, the interpolation
|
||||
syntax allows use of arbitrary expressions within JSON strings:
|
||||
|
||||
```json
|
||||
{
|
||||
"sum": "${1 + addend}",
|
||||
"message": "Hello, ${name}!",
|
||||
"shouty_message": "${upper(message)}"
|
||||
}
|
||||
```
|
||||
|
||||
For more information, see the detailed specifications:
|
||||
|
||||
* [Syntax-agnostic Information Model](spec.md)
|
||||
* [HCL Native Syntax](hclsyntax/spec.md)
|
||||
* [JSON Representation](json/spec.md)
|
||||
|
||||
## Changes in 2.0
|
||||
|
||||
Version 2.0 of HCL combines the features of HCL 1.0 with those of the
|
||||
interpolation language HIL to produce a single configuration language that
|
||||
supports arbitrary expressions.
|
||||
|
||||
This new version has a completely new parser and Go API, with no direct
|
||||
migration path. Although the syntax is similar, the implementation takes some
|
||||
very different approaches to improve on some "rough edges" that existed with
|
||||
the original implementation and to allow for more robust error handling.
|
||||
|
||||
It's possible to import both HCL 1 and HCL 2 into the same program using Go's
|
||||
_semantic import versioning_ mechanism:
|
||||
|
||||
```go
|
||||
import (
|
||||
hcl1 "github.com/hashicorp/hcl"
|
||||
hcl2 "github.com/hashicorp/hcl/v2"
|
||||
)
|
||||
```
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
HCL was heavily inspired by [libucl](https://github.com/vstakhov/libucl),
|
||||
by [Vsevolod Stakhov](https://github.com/vstakhov).
|
||||
|
||||
HCL and HIL originate in [HashiCorp Terraform](https://terraform.io/),
|
||||
with the original parsers for each written by
|
||||
[Mitchell Hashimoto](https://github.com/mitchellh).
|
||||
|
||||
The original HCL parser was ported to pure Go (from yacc) by
|
||||
[Fatih Arslan](https://github.com/fatih). The structure-related portions of
|
||||
the new native syntax parser build on that work.
|
||||
|
||||
The original HIL parser was ported to pure Go (from yacc) by
|
||||
[Martin Atkins](https://github.com/apparentlymart). The expression-related
|
||||
portions of the new native syntax parser build on that work.
|
||||
|
||||
HCL 2, which merged the original HCL and HIL languages into this single new
|
||||
language, builds on design and prototyping work by
|
||||
[Martin Atkins](https://github.com/apparentlymart) in
|
||||
[zcl](https://github.com/zclconf/go-zcl).
|
@ -0,0 +1,13 @@
|
||||
build: off
|
||||
|
||||
clone_folder: c:\gopath\src\github.com\hashicorp\hcl
|
||||
|
||||
environment:
|
||||
GOPATH: c:\gopath
|
||||
GO111MODULE: on
|
||||
GOPROXY: https://goproxy.io
|
||||
|
||||
stack: go 1.12
|
||||
|
||||
test_script:
|
||||
- go test ./...
|
@ -0,0 +1,143 @@
|
||||
package hcl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// DiagnosticSeverity represents the severity of a diagnostic.
|
||||
type DiagnosticSeverity int
|
||||
|
||||
const (
|
||||
// DiagInvalid is the invalid zero value of DiagnosticSeverity
|
||||
DiagInvalid DiagnosticSeverity = iota
|
||||
|
||||
// DiagError indicates that the problem reported by a diagnostic prevents
|
||||
// further progress in parsing and/or evaluating the subject.
|
||||
DiagError
|
||||
|
||||
// DiagWarning indicates that the problem reported by a diagnostic warrants
|
||||
// user attention but does not prevent further progress. It is most
|
||||
// commonly used for showing deprecation notices.
|
||||
DiagWarning
|
||||
)
|
||||
|
||||
// Diagnostic represents information to be presented to a user about an
|
||||
// error or anomoly in parsing or evaluating configuration.
|
||||
type Diagnostic struct {
|
||||
Severity DiagnosticSeverity
|
||||
|
||||
// Summary and Detail contain the English-language description of the
|
||||
// problem. Summary is a terse description of the general problem and
|
||||
// detail is a more elaborate, often-multi-sentence description of
|
||||
// the probem and what might be done to solve it.
|
||||
Summary string
|
||||
Detail string
|
||||
|
||||
// Subject and Context are both source ranges relating to the diagnostic.
|
||||
//
|
||||
// Subject is a tight range referring to exactly the construct that
|
||||
// is problematic, while Context is an optional broader range (which should
|
||||
// fully contain Subject) that ought to be shown around Subject when
|
||||
// generating isolated source-code snippets in diagnostic messages.
|
||||
// If Context is nil, the Subject is also the Context.
|
||||
//
|
||||
// Some diagnostics have no source ranges at all. If Context is set then
|
||||
// Subject should always also be set.
|
||||
Subject *Range
|
||||
Context *Range
|
||||
|
||||
// For diagnostics that occur when evaluating an expression, Expression
|
||||
// may refer to that expression and EvalContext may point to the
|
||||
// EvalContext that was active when evaluating it. This may allow for the
|
||||
// inclusion of additional useful information when rendering a diagnostic
|
||||
// message to the user.
|
||||
//
|
||||
// It is not always possible to select a single EvalContext for a
|
||||
// diagnostic, and so in some cases this field may be nil even when an
|
||||
// expression causes a problem.
|
||||
//
|
||||
// EvalContexts form a tree, so the given EvalContext may refer to a parent
|
||||
// which in turn refers to another parent, etc. For a full picture of all
|
||||
// of the active variables and functions the caller must walk up this
|
||||
// chain, preferring definitions that are "closer" to the expression in
|
||||
// case of colliding names.
|
||||
Expression Expression
|
||||
EvalContext *EvalContext
|
||||
}
|
||||
|
||||
// Diagnostics is a list of Diagnostic instances.
|
||||
type Diagnostics []*Diagnostic
|
||||
|
||||
// error implementation, so that diagnostics can be returned via APIs
|
||||
// that normally deal in vanilla Go errors.
|
||||
//
|
||||
// This presents only minimal context about the error, for compatibility
|
||||
// with usual expectations about how errors will present as strings.
|
||||
func (d *Diagnostic) Error() string {
|
||||
return fmt.Sprintf("%s: %s; %s", d.Subject, d.Summary, d.Detail)
|
||||
}
|
||||
|
||||
// error implementation, so that sets of diagnostics can be returned via
|
||||
// APIs that normally deal in vanilla Go errors.
|
||||
func (d Diagnostics) Error() string {
|
||||
count := len(d)
|
||||
switch {
|
||||
case count == 0:
|
||||
return "no diagnostics"
|
||||
case count == 1:
|
||||
return d[0].Error()
|
||||
default:
|
||||
return fmt.Sprintf("%s, and %d other diagnostic(s)", d[0].Error(), count-1)
|
||||
}
|
||||
}
|
||||
|
||||
// Append appends a new error to a Diagnostics and return the whole Diagnostics.
|
||||
//
|
||||
// This is provided as a convenience for returning from a function that
|
||||
// collects and then returns a set of diagnostics:
|
||||
//
|
||||
// return nil, diags.Append(&hcl.Diagnostic{ ... })
|
||||
//
|
||||
// Note that this modifies the array underlying the diagnostics slice, so
|
||||
// must be used carefully within a single codepath. It is incorrect (and rude)
|
||||
// to extend a diagnostics created by a different subsystem.
|
||||
func (d Diagnostics) Append(diag *Diagnostic) Diagnostics {
|
||||
return append(d, diag)
|
||||
}
|
||||
|
||||
// Extend concatenates the given Diagnostics with the receiver and returns
|
||||
// the whole new Diagnostics.
|
||||
//
|
||||
// This is similar to Append but accepts multiple diagnostics to add. It has
|
||||
// all the same caveats and constraints.
|
||||
func (d Diagnostics) Extend(diags Diagnostics) Diagnostics {
|
||||
return append(d, diags...)
|
||||
}
|
||||
|
||||
// HasErrors returns true if the receiver contains any diagnostics of
|
||||
// severity DiagError.
|
||||
func (d Diagnostics) HasErrors() bool {
|
||||
for _, diag := range d {
|
||||
if diag.Severity == DiagError {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d Diagnostics) Errs() []error {
|
||||
var errs []error
|
||||
for _, diag := range d {
|
||||
if diag.Severity == DiagError {
|
||||
errs = append(errs, diag)
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
// A DiagnosticWriter emits diagnostics somehow.
|
||||
type DiagnosticWriter interface {
|
||||
WriteDiagnostic(*Diagnostic) error
|
||||
WriteDiagnostics(Diagnostics) error
|
||||
}
|
@ -0,0 +1,311 @@
|
||||
package hcl
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
|
||||
wordwrap "github.com/mitchellh/go-wordwrap"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
type diagnosticTextWriter struct {
|
||||
files map[string]*File
|
||||
wr io.Writer
|
||||
width uint
|
||||
color bool
|
||||
}
|
||||
|
||||
// NewDiagnosticTextWriter creates a DiagnosticWriter that writes diagnostics
|
||||
// to the given writer as formatted text.
|
||||
//
|
||||
// It is designed to produce text appropriate to print in a monospaced font
|
||||
// in a terminal of a particular width, or optionally with no width limit.
|
||||
//
|
||||
// The given width may be zero to disable word-wrapping of the detail text
|
||||
// and truncation of source code snippets.
|
||||
//
|
||||
// If color is set to true, the output will include VT100 escape sequences to
|
||||
// color-code the severity indicators. It is suggested to turn this off if
|
||||
// the target writer is not a terminal.
|
||||
func NewDiagnosticTextWriter(wr io.Writer, files map[string]*File, width uint, color bool) DiagnosticWriter {
|
||||
return &diagnosticTextWriter{
|
||||
files: files,
|
||||
wr: wr,
|
||||
width: width,
|
||||
color: color,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *diagnosticTextWriter) WriteDiagnostic(diag *Diagnostic) error {
|
||||
if diag == nil {
|
||||
return errors.New("nil diagnostic")
|
||||
}
|
||||
|
||||
var colorCode, highlightCode, resetCode string
|
||||
if w.color {
|
||||
switch diag.Severity {
|
||||
case DiagError:
|
||||
colorCode = "\x1b[31m"
|
||||
case DiagWarning:
|
||||
colorCode = "\x1b[33m"
|
||||
}
|
||||
resetCode = "\x1b[0m"
|
||||
highlightCode = "\x1b[1;4m"
|
||||
}
|
||||
|
||||
var severityStr string
|
||||
switch diag.Severity {
|
||||
case DiagError:
|
||||
severityStr = "Error"
|
||||
case DiagWarning:
|
||||
severityStr = "Warning"
|
||||
default:
|
||||
// should never happen
|
||||
severityStr = "???????"
|
||||
}
|
||||
|
||||
fmt.Fprintf(w.wr, "%s%s%s: %s\n\n", colorCode, severityStr, resetCode, diag.Summary)
|
||||
|
||||
if diag.Subject != nil {
|
||||
snipRange := *diag.Subject
|
||||
highlightRange := snipRange
|
||||
if diag.Context != nil {
|
||||
// Show enough of the source code to include both the subject
|
||||
// and context ranges, which overlap in all reasonable
|
||||
// situations.
|
||||
snipRange = RangeOver(snipRange, *diag.Context)
|
||||
}
|
||||
// We can't illustrate an empty range, so we'll turn such ranges into
|
||||
// single-character ranges, which might not be totally valid (may point
|
||||
// off the end of a line, or off the end of the file) but are good
|
||||
// enough for the bounds checks we do below.
|
||||
if snipRange.Empty() {
|
||||
snipRange.End.Byte++
|
||||
snipRange.End.Column++
|
||||
}
|
||||
if highlightRange.Empty() {
|
||||
highlightRange.End.Byte++
|
||||
highlightRange.End.Column++
|
||||
}
|
||||
|
||||
file := w.files[diag.Subject.Filename]
|
||||
if file == nil || file.Bytes == nil {
|
||||
fmt.Fprintf(w.wr, " on %s line %d:\n (source code not available)\n\n", diag.Subject.Filename, diag.Subject.Start.Line)
|
||||
} else {
|
||||
|
||||
var contextLine string
|
||||
if diag.Subject != nil {
|
||||
contextLine = contextString(file, diag.Subject.Start.Byte)
|
||||
if contextLine != "" {
|
||||
contextLine = ", in " + contextLine
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(w.wr, " on %s line %d%s:\n", diag.Subject.Filename, diag.Subject.Start.Line, contextLine)
|
||||
|
||||
src := file.Bytes
|
||||
sc := NewRangeScanner(src, diag.Subject.Filename, bufio.ScanLines)
|
||||
|
||||
for sc.Scan() {
|
||||
lineRange := sc.Range()
|
||||
if !lineRange.Overlaps(snipRange) {
|
||||
continue
|
||||
}
|
||||
|
||||
beforeRange, highlightedRange, afterRange := lineRange.PartitionAround(highlightRange)
|
||||
if highlightedRange.Empty() {
|
||||
fmt.Fprintf(w.wr, "%4d: %s\n", lineRange.Start.Line, sc.Bytes())
|
||||
} else {
|
||||
before := beforeRange.SliceBytes(src)
|
||||
highlighted := highlightedRange.SliceBytes(src)
|
||||
after := afterRange.SliceBytes(src)
|
||||
fmt.Fprintf(
|
||||
w.wr, "%4d: %s%s%s%s%s\n",
|
||||
lineRange.Start.Line,
|
||||
before,
|
||||
highlightCode, highlighted, resetCode,
|
||||
after,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
w.wr.Write([]byte{'\n'})
|
||||
}
|
||||
|
||||
if diag.Expression != nil && diag.EvalContext != nil {
|
||||
// We will attempt to render the values for any variables
|
||||
// referenced in the given expression as additional context, for
|
||||
// situations where the same expression is evaluated multiple
|
||||
// times in different scopes.
|
||||
expr := diag.Expression
|
||||
ctx := diag.EvalContext
|
||||
|
||||
vars := expr.Variables()
|
||||
stmts := make([]string, 0, len(vars))
|
||||
seen := make(map[string]struct{}, len(vars))
|
||||
for _, traversal := range vars {
|
||||
val, diags := traversal.TraverseAbs(ctx)
|
||||
if diags.HasErrors() {
|
||||
// Skip anything that generates errors, since we probably
|
||||
// already have the same error in our diagnostics set
|
||||
// already.
|
||||
continue
|
||||
}
|
||||
|
||||
traversalStr := w.traversalStr(traversal)
|
||||
if _, exists := seen[traversalStr]; exists {
|
||||
continue // don't show duplicates when the same variable is referenced multiple times
|
||||
}
|
||||
switch {
|
||||
case !val.IsKnown():
|
||||
// Can't say anything about this yet, then.
|
||||
continue
|
||||
case val.IsNull():
|
||||
stmts = append(stmts, fmt.Sprintf("%s set to null", traversalStr))
|
||||
default:
|
||||
stmts = append(stmts, fmt.Sprintf("%s as %s", traversalStr, w.valueStr(val)))
|
||||
}
|
||||
seen[traversalStr] = struct{}{}
|
||||
}
|
||||
|
||||
sort.Strings(stmts) // FIXME: Should maybe use a traversal-aware sort that can sort numeric indexes properly?
|
||||
last := len(stmts) - 1
|
||||
|
||||
for i, stmt := range stmts {
|
||||
switch i {
|
||||
case 0:
|
||||
w.wr.Write([]byte{'w', 'i', 't', 'h', ' '})
|
||||
default:
|
||||
w.wr.Write([]byte{' ', ' ', ' ', ' ', ' '})
|
||||
}
|
||||
w.wr.Write([]byte(stmt))
|
||||
switch i {
|
||||
case last:
|
||||
w.wr.Write([]byte{'.', '\n', '\n'})
|
||||
default:
|
||||
w.wr.Write([]byte{',', '\n'})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if diag.Detail != "" {
|
||||
detail := diag.Detail
|
||||
if w.width != 0 {
|
||||
detail = wordwrap.WrapString(detail, w.width)
|
||||
}
|
||||
fmt.Fprintf(w.wr, "%s\n\n", detail)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *diagnosticTextWriter) WriteDiagnostics(diags Diagnostics) error {
|
||||
for _, diag := range diags {
|
||||
err := w.WriteDiagnostic(diag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *diagnosticTextWriter) traversalStr(traversal Traversal) string {
|
||||
// This is a specialized subset of traversal rendering tailored to
|
||||
// producing helpful contextual messages in diagnostics. It is not
|
||||
// comprehensive nor intended to be used for other purposes.
|
||||
|
||||
var buf bytes.Buffer
|
||||
for _, step := range traversal {
|
||||
switch tStep := step.(type) {
|
||||
case TraverseRoot:
|
||||
buf.WriteString(tStep.Name)
|
||||
case TraverseAttr:
|
||||
buf.WriteByte('.')
|
||||
buf.WriteString(tStep.Name)
|
||||
case TraverseIndex:
|
||||
buf.WriteByte('[')
|
||||
if keyTy := tStep.Key.Type(); keyTy.IsPrimitiveType() {
|
||||
buf.WriteString(w.valueStr(tStep.Key))
|
||||
} else {
|
||||
// We'll just use a placeholder for more complex values,
|
||||
// since otherwise our result could grow ridiculously long.
|
||||
buf.WriteString("...")
|
||||
}
|
||||
buf.WriteByte(']')
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (w *diagnosticTextWriter) valueStr(val cty.Value) string {
|
||||
// This is a specialized subset of value rendering tailored to producing
|
||||
// helpful but concise messages in diagnostics. It is not comprehensive
|
||||
// nor intended to be used for other purposes.
|
||||
|
||||
ty := val.Type()
|
||||
switch {
|
||||
case val.IsNull():
|
||||
return "null"
|
||||
case !val.IsKnown():
|
||||
// Should never happen here because we should filter before we get
|
||||
// in here, but we'll do something reasonable rather than panic.
|
||||
return "(not yet known)"
|
||||
case ty == cty.Bool:
|
||||
if val.True() {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
case ty == cty.Number:
|
||||
bf := val.AsBigFloat()
|
||||
return bf.Text('g', 10)
|
||||
case ty == cty.String:
|
||||
// Go string syntax is not exactly the same as HCL native string syntax,
|
||||
// but we'll accept the minor edge-cases where this is different here
|
||||
// for now, just to get something reasonable here.
|
||||
return fmt.Sprintf("%q", val.AsString())
|
||||
case ty.IsCollectionType() || ty.IsTupleType():
|
||||
l := val.LengthInt()
|
||||
switch l {
|
||||
case 0:
|
||||
return "empty " + ty.FriendlyName()
|
||||
case 1:
|
||||
return ty.FriendlyName() + " with 1 element"
|
||||
default:
|
||||
return fmt.Sprintf("%s with %d elements", ty.FriendlyName(), l)
|
||||
}
|
||||
case ty.IsObjectType():
|
||||
atys := ty.AttributeTypes()
|
||||
l := len(atys)
|
||||
switch l {
|
||||
case 0:
|
||||
return "object with no attributes"
|
||||
case 1:
|
||||
var name string
|
||||
for k := range atys {
|
||||
name = k
|
||||
}
|
||||
return fmt.Sprintf("object with 1 attribute %q", name)
|
||||
default:
|
||||
return fmt.Sprintf("object with %d attributes", l)
|
||||
}
|
||||
default:
|
||||
return ty.FriendlyName()
|
||||
}
|
||||
}
|
||||
|
||||
func contextString(file *File, offset int) string {
|
||||
type contextStringer interface {
|
||||
ContextString(offset int) string
|
||||
}
|
||||
|
||||
if cser, ok := file.Nav.(contextStringer); ok {
|
||||
return cser.ContextString(offset)
|
||||
}
|
||||
return ""
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package hcl
|
||||
|
||||
import (
|
||||
"github.com/agext/levenshtein"
|
||||
)
|
||||
|
||||
// nameSuggestion tries to find a name from the given slice of suggested names
|
||||
// that is close to the given name and returns it if found. If no suggestion
|
||||
// is close enough, returns the empty string.
|
||||
//
|
||||
// The suggestions are tried in order, so earlier suggestions take precedence
|
||||
// if the given string is similar to two or more suggestions.
|
||||
//
|
||||
// This function is intended to be used with a relatively-small number of
|
||||
// suggestions. It's not optimized for hundreds or thousands of them.
|
||||
func nameSuggestion(given string, suggestions []string) string {
|
||||
for _, suggestion := range suggestions {
|
||||
dist := levenshtein.Distance(given, suggestion, nil)
|
||||
if dist < 3 { // threshold determined experimentally
|
||||
return suggestion
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
// Package hcl contains the main modelling types and general utility functions
|
||||
// for HCL.
|
||||
//
|
||||
// For a simple entry point into HCL, see the package in the subdirectory
|
||||
// "hclsimple", which has an opinionated function Decode that can decode HCL
|
||||
// configurations in either native HCL syntax or JSON syntax into a Go struct
|
||||
// type:
|
||||
//
|
||||
// package main
|
||||
//
|
||||
// import (
|
||||
// "log"
|
||||
// "github.com/hashicorp/hcl/v2/hclsimple"
|
||||
// )
|
||||
//
|
||||
// type Config struct {
|
||||
// LogLevel string `hcl:"log_level"`
|
||||
// }
|
||||
//
|
||||
// func main() {
|
||||
// var config Config
|
||||
// err := hclsimple.DecodeFile("config.hcl", nil, &config)
|
||||
// if err != nil {
|
||||
// log.Fatalf("Failed to load configuration: %s", err)
|
||||
// }
|
||||
// log.Printf("Configuration is %#v", config)
|
||||
// }
|
||||
//
|
||||
// If your application needs more control over the evaluation of the
|
||||
// configuration, you can use the functions in the subdirectories hclparse,
|
||||
// gohcl, hcldec, etc. Splitting the handling of configuration into multiple
|
||||
// phases allows for advanced patterns such as allowing expressions in one
|
||||
// part of the configuration to refer to data defined in another part.
|
||||
package hcl
|
@ -0,0 +1,25 @@
|
||||
package hcl
|
||||
|
||||
import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
)
|
||||
|
||||
// An EvalContext provides the variables and functions that should be used
|
||||
// to evaluate an expression.
|
||||
type EvalContext struct {
|
||||
Variables map[string]cty.Value
|
||||
Functions map[string]function.Function
|
||||
parent *EvalContext
|
||||
}
|
||||
|
||||
// NewChild returns a new EvalContext that is a child of the receiver.
|
||||
func (ctx *EvalContext) NewChild() *EvalContext {
|
||||
return &EvalContext{parent: ctx}
|
||||
}
|
||||
|
||||
// Parent returns the parent of the receiver, or nil if the receiver has
|
||||
// no parent.
|
||||
func (ctx *EvalContext) Parent() *EvalContext {
|
||||
return ctx.parent
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package hcl
|
||||
|
||||
// ExprCall tests if the given expression is a function call and,
|
||||
// if so, extracts the function name and the expressions that represent
|
||||
// the arguments. If the given expression is not statically a function call,
|
||||
// error diagnostics are returned.
|
||||
//
|
||||
// A particular Expression implementation can support this function by
|
||||
// offering a method called ExprCall that takes no arguments and returns
|
||||
// *StaticCall. This method should return nil if a static call cannot
|
||||
// be extracted. Alternatively, an implementation can support
|
||||
// UnwrapExpression to delegate handling of this function to a wrapped
|
||||
// Expression object.
|
||||
func ExprCall(expr Expression) (*StaticCall, Diagnostics) {
|
||||
type exprCall interface {
|
||||
ExprCall() *StaticCall
|
||||
}
|
||||
|
||||
physExpr := UnwrapExpressionUntil(expr, func(expr Expression) bool {
|
||||
_, supported := expr.(exprCall)
|
||||
return supported
|
||||
})
|
||||
|
||||
if exC, supported := physExpr.(exprCall); supported {
|
||||
if call := exC.ExprCall(); call != nil {
|
||||
return call, nil
|
||||
}
|
||||
}
|
||||
return nil, Diagnostics{
|
||||
&Diagnostic{
|
||||
Severity: DiagError,
|
||||
Summary: "Invalid expression",
|
||||
Detail: "A static function call is required.",
|
||||
Subject: expr.StartRange().Ptr(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// StaticCall represents a function call that was extracted statically from
|
||||
// an expression using ExprCall.
|
||||
type StaticCall struct {
|
||||
Name string
|
||||
NameRange Range
|
||||
Arguments []Expression
|
||||
ArgsRange Range
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package hcl
|
||||
|
||||
// ExprList tests if the given expression is a static list construct and,
|
||||
// if so, extracts the expressions that represent the list elements.
|
||||
// If the given expression is not a static list, error diagnostics are
|
||||
// returned.
|
||||
//
|
||||
// A particular Expression implementation can support this function by
|
||||
// offering a method called ExprList that takes no arguments and returns
|
||||
// []Expression. This method should return nil if a static list cannot
|
||||
// be extracted. Alternatively, an implementation can support
|
||||
// UnwrapExpression to delegate handling of this function to a wrapped
|
||||
// Expression object.
|
||||
func ExprList(expr Expression) ([]Expression, Diagnostics) {
|
||||
type exprList interface {
|
||||
ExprList() []Expression
|
||||
}
|
||||
|
||||
physExpr := UnwrapExpressionUntil(expr, func(expr Expression) bool {
|
||||
_, supported := expr.(exprList)
|
||||
return supported
|
||||
})
|
||||
|
||||
if exL, supported := physExpr.(exprList); supported {
|
||||
if list := exL.ExprList(); list != nil {
|
||||
return list, nil
|
||||
}
|
||||
}
|
||||
return nil, Diagnostics{
|
||||
&Diagnostic{
|
||||
Severity: DiagError,
|
||||
Summary: "Invalid expression",
|
||||
Detail: "A static list expression is required.",
|
||||
Subject: expr.StartRange().Ptr(),
|
||||
},
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package hcl
|
||||
|
||||
// ExprMap tests if the given expression is a static map construct and,
|
||||
// if so, extracts the expressions that represent the map elements.
|
||||
// If the given expression is not a static map, error diagnostics are
|
||||
// returned.
|
||||
//
|
||||
// A particular Expression implementation can support this function by
|
||||
// offering a method called ExprMap that takes no arguments and returns
|
||||
// []KeyValuePair. This method should return nil if a static map cannot
|
||||
// be extracted. Alternatively, an implementation can support
|
||||
// UnwrapExpression to delegate handling of this function to a wrapped
|
||||
// Expression object.
|
||||
func ExprMap(expr Expression) ([]KeyValuePair, Diagnostics) {
|
||||
type exprMap interface {
|
||||
ExprMap() []KeyValuePair
|
||||
}
|
||||
|
||||
physExpr := UnwrapExpressionUntil(expr, func(expr Expression) bool {
|
||||
_, supported := expr.(exprMap)
|
||||
return supported
|
||||
})
|
||||
|
||||
if exM, supported := physExpr.(exprMap); supported {
|
||||
if pairs := exM.ExprMap(); pairs != nil {
|
||||
return pairs, nil
|
||||
}
|
||||
}
|
||||
return nil, Diagnostics{
|
||||
&Diagnostic{
|
||||
Severity: DiagError,
|
||||
Summary: "Invalid expression",
|
||||
Detail: "A static map expression is required.",
|
||||
Subject: expr.StartRange().Ptr(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// KeyValuePair represents a pair of expressions that serve as a single item
|
||||
// within a map or object definition construct.
|
||||
type KeyValuePair struct {
|
||||
Key Expression
|
||||
Value Expression
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package hcl
|
||||
|
||||
type unwrapExpression interface {
|
||||
UnwrapExpression() Expression
|
||||
}
|
||||
|
||||
// UnwrapExpression removes any "wrapper" expressions from the given expression,
|
||||
// to recover the representation of the physical expression given in source
|
||||
// code.
|
||||
//
|
||||
// Sometimes wrapping expressions are used to modify expression behavior, e.g.
|
||||
// in extensions that need to make some local variables available to certain
|
||||
// sub-trees of the configuration. This can make it difficult to reliably
|
||||
// type-assert on the physical AST types used by the underlying syntax.
|
||||
//
|
||||
// Unwrapping an expression may modify its behavior by stripping away any
|
||||
// additional constraints or capabilities being applied to the Value and
|
||||
// Variables methods, so this function should generally only be used prior
|
||||
// to operations that concern themselves with the static syntax of the input
|
||||
// configuration, and not with the effective value of the expression.
|
||||
//
|
||||
// Wrapper expression types must support unwrapping by implementing a method
|
||||
// called UnwrapExpression that takes no arguments and returns the embedded
|
||||
// Expression. Implementations of this method should peel away only one level
|
||||
// of wrapping, if multiple are present. This method may return nil to
|
||||
// indicate _dynamically_ that no wrapped expression is available, for
|
||||
// expression types that might only behave as wrappers in certain cases.
|
||||
func UnwrapExpression(expr Expression) Expression {
|
||||
for {
|
||||
unwrap, wrapped := expr.(unwrapExpression)
|
||||
if !wrapped {
|
||||
return expr
|
||||
}
|
||||
innerExpr := unwrap.UnwrapExpression()
|
||||
if innerExpr == nil {
|
||||
return expr
|
||||
}
|
||||
expr = innerExpr
|
||||
}
|
||||
}
|
||||
|
||||
// UnwrapExpressionUntil is similar to UnwrapExpression except it gives the
|
||||
// caller an opportunity to test each level of unwrapping to see each a
|
||||
// particular expression is accepted.
|
||||
//
|
||||
// This could be used, for example, to unwrap until a particular other
|
||||
// interface is satisfied, regardless of wrap wrapping level it is satisfied
|
||||
// at.
|
||||
//
|
||||
// The given callback function must return false to continue wrapping, or
|
||||
// true to accept and return the proposed expression given. If the callback
|
||||
// function rejects even the final, physical expression then the result of
|
||||
// this function is nil.
|
||||
func UnwrapExpressionUntil(expr Expression, until func(Expression) bool) Expression {
|
||||
for {
|
||||
if until(expr) {
|
||||
return expr
|
||||
}
|
||||
unwrap, wrapped := expr.(unwrapExpression)
|
||||
if !wrapped {
|
||||
return nil
|
||||
}
|
||||
expr = unwrap.UnwrapExpression()
|
||||
if expr == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
module github.com/hashicorp/hcl/v2
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/agext/levenshtein v1.2.1
|
||||
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3
|
||||
github.com/apparentlymart/go-textseg/v12 v12.0.0
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/go-test/deep v1.0.3
|
||||
github.com/google/go-cmp v0.3.1
|
||||
github.com/kr/pretty v0.1.0
|
||||
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/sergi/go-diff v1.0.0
|
||||
github.com/spf13/pflag v1.0.2
|
||||
github.com/stretchr/testify v1.2.2 // indirect
|
||||
github.com/zclconf/go-cty v1.2.0
|
||||
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734
|
||||
golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82 // indirect
|
||||
golang.org/x/text v0.3.2 // indirect
|
||||
)
|
@ -0,0 +1,53 @@
|
||||
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
|
||||
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3 h1:ZSTrOEhiM5J5RFxEaFvMZVEAM1KvT1YzbEOwB2EAGjA=
|
||||
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
|
||||
github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0=
|
||||
github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
|
||||
github.com/apparentlymart/go-textseg/v12 v12.0.0 h1:bNEQyAGak9tojivJNkoqWErVCQbjdL7GzRt3F8NvfJ0=
|
||||
github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
|
||||
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/spf13/pflag v1.0.2 h1:Fy0orTDgHdbnzHcsOgfCN4LtHf0ec3wwtiwJqwvf3Gc=
|
||||
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
|
||||
github.com/zclconf/go-cty v1.2.0 h1:sPHsy7ADcIZQP3vILvTjrh74ZA175TFP5vqiNK1UmlI=
|
||||
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo=
|
||||
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82 h1:vsphBvatvfbhlb4PO1BYSr9dzugGxJ/SQHoNufZJq1w=
|
||||
golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
@ -0,0 +1,226 @@
|
||||
package hcl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// MergeFiles combines the given files to produce a single body that contains
|
||||
// configuration from all of the given files.
|
||||
//
|
||||
// The ordering of the given files decides the order in which contained
|
||||
// elements will be returned. If any top-level attributes are defined with
|
||||
// the same name across multiple files, a diagnostic will be produced from
|
||||
// the Content and PartialContent methods describing this error in a
|
||||
// user-friendly way.
|
||||
func MergeFiles(files []*File) Body {
|
||||
var bodies []Body
|
||||
for _, file := range files {
|
||||
bodies = append(bodies, file.Body)
|
||||
}
|
||||
return MergeBodies(bodies)
|
||||
}
|
||||
|
||||
// MergeBodies is like MergeFiles except it deals directly with bodies, rather
|
||||
// than with entire files.
|
||||
func MergeBodies(bodies []Body) Body {
|
||||
if len(bodies) == 0 {
|
||||
// Swap out for our singleton empty body, to reduce the number of
|
||||
// empty slices we have hanging around.
|
||||
return emptyBody
|
||||
}
|
||||
|
||||
// If any of the given bodies are already merged bodies, we'll unpack
|
||||
// to flatten to a single mergedBodies, since that's conceptually simpler.
|
||||
// This also, as a side-effect, eliminates any empty bodies, since
|
||||
// empties are merged bodies with no inner bodies.
|
||||
var newLen int
|
||||
var flatten bool
|
||||
for _, body := range bodies {
|
||||
if children, merged := body.(mergedBodies); merged {
|
||||
newLen += len(children)
|
||||
flatten = true
|
||||
} else {
|
||||
newLen++
|
||||
}
|
||||
}
|
||||
|
||||
if !flatten { // not just newLen == len, because we might have mergedBodies with single bodies inside
|
||||
return mergedBodies(bodies)
|
||||
}
|
||||
|
||||
if newLen == 0 {
|
||||
// Don't allocate a new empty when we already have one
|
||||
return emptyBody
|
||||
}
|
||||
|
||||
new := make([]Body, 0, newLen)
|
||||
for _, body := range bodies {
|
||||
if children, merged := body.(mergedBodies); merged {
|
||||
new = append(new, children...)
|
||||
} else {
|
||||
new = append(new, body)
|
||||
}
|
||||
}
|
||||
return mergedBodies(new)
|
||||
}
|
||||
|
||||
var emptyBody = mergedBodies([]Body{})
|
||||
|
||||
// EmptyBody returns a body with no content. This body can be used as a
|
||||
// placeholder when a body is required but no body content is available.
|
||||
func EmptyBody() Body {
|
||||
return emptyBody
|
||||
}
|
||||
|
||||
type mergedBodies []Body
|
||||
|
||||
// Content returns the content produced by applying the given schema to all
|
||||
// of the merged bodies and merging the result.
|
||||
//
|
||||
// Although required attributes _are_ supported, they should be used sparingly
|
||||
// with merged bodies since in this case there is no contextual information
|
||||
// with which to return good diagnostics. Applications working with merged
|
||||
// bodies may wish to mark all attributes as optional and then check for
|
||||
// required attributes afterwards, to produce better diagnostics.
|
||||
func (mb mergedBodies) Content(schema *BodySchema) (*BodyContent, Diagnostics) {
|
||||
// the returned body will always be empty in this case, because mergedContent
|
||||
// will only ever call Content on the child bodies.
|
||||
content, _, diags := mb.mergedContent(schema, false)
|
||||
return content, diags
|
||||
}
|
||||
|
||||
func (mb mergedBodies) PartialContent(schema *BodySchema) (*BodyContent, Body, Diagnostics) {
|
||||
return mb.mergedContent(schema, true)
|
||||
}
|
||||
|
||||
func (mb mergedBodies) JustAttributes() (Attributes, Diagnostics) {
|
||||
attrs := make(map[string]*Attribute)
|
||||
var diags Diagnostics
|
||||
|
||||
for _, body := range mb {
|
||||
thisAttrs, thisDiags := body.JustAttributes()
|
||||
|
||||
if len(thisDiags) != 0 {
|
||||
diags = append(diags, thisDiags...)
|
||||
}
|
||||
|
||||
if thisAttrs != nil {
|
||||
for name, attr := range thisAttrs {
|
||||
if existing := attrs[name]; existing != nil {
|
||||
diags = diags.Append(&Diagnostic{
|
||||
Severity: DiagError,
|
||||
Summary: "Duplicate argument",
|
||||
Detail: fmt.Sprintf(
|
||||
"Argument %q was already set at %s",
|
||||
name, existing.NameRange.String(),
|
||||
),
|
||||
Subject: &attr.NameRange,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
attrs[name] = attr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return attrs, diags
|
||||
}
|
||||
|
||||
func (mb mergedBodies) MissingItemRange() Range {
|
||||
if len(mb) == 0 {
|
||||
// Nothing useful to return here, so we'll return some garbage.
|
||||
return Range{
|
||||
Filename: "<empty>",
|
||||
}
|
||||
}
|
||||
|
||||
// arbitrarily use the first body's missing item range
|
||||
return mb[0].MissingItemRange()
|
||||
}
|
||||
|
||||
func (mb mergedBodies) mergedContent(schema *BodySchema, partial bool) (*BodyContent, Body, Diagnostics) {
|
||||
// We need to produce a new schema with none of the attributes marked as
|
||||
// required, since _any one_ of our bodies can contribute an attribute value.
|
||||
// We'll separately check that all required attributes are present at
|
||||
// the end.
|
||||
mergedSchema := &BodySchema{
|
||||
Blocks: schema.Blocks,
|
||||
}
|
||||
for _, attrS := range schema.Attributes {
|
||||
mergedAttrS := attrS
|
||||
mergedAttrS.Required = false
|
||||
mergedSchema.Attributes = append(mergedSchema.Attributes, mergedAttrS)
|
||||
}
|
||||
|
||||
var mergedLeftovers []Body
|
||||
content := &BodyContent{
|
||||
Attributes: map[string]*Attribute{},
|
||||
}
|
||||
|
||||
var diags Diagnostics
|
||||
for _, body := range mb {
|
||||
var thisContent *BodyContent
|
||||
var thisLeftovers Body
|
||||
var thisDiags Diagnostics
|
||||
|
||||
if partial {
|
||||
thisContent, thisLeftovers, thisDiags = body.PartialContent(mergedSchema)
|
||||
} else {
|
||||
thisContent, thisDiags = body.Content(mergedSchema)
|
||||
}
|
||||
|
||||
if thisLeftovers != nil {
|
||||
mergedLeftovers = append(mergedLeftovers, thisLeftovers)
|
||||
}
|
||||
if len(thisDiags) != 0 {
|
||||
diags = append(diags, thisDiags...)
|
||||
}
|
||||
|
||||
if thisContent.Attributes != nil {
|
||||
for name, attr := range thisContent.Attributes {
|
||||
if existing := content.Attributes[name]; existing != nil {
|
||||
diags = diags.Append(&Diagnostic{
|
||||
Severity: DiagError,
|
||||
Summary: "Duplicate argument",
|
||||
Detail: fmt.Sprintf(
|
||||
"Argument %q was already set at %s",
|
||||
name, existing.NameRange.String(),
|
||||
),
|
||||
Subject: &attr.NameRange,
|
||||
})
|
||||
continue
|
||||
}
|
||||
content.Attributes[name] = attr
|
||||
}
|
||||
}
|
||||
|
||||
if len(thisContent.Blocks) != 0 {
|
||||
content.Blocks = append(content.Blocks, thisContent.Blocks...)
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, we check for required attributes.
|
||||
for _, attrS := range schema.Attributes {
|
||||
if !attrS.Required {
|
||||
continue
|
||||
}
|
||||
|
||||
if content.Attributes[attrS.Name] == nil {
|
||||
// We don't have any context here to produce a good diagnostic,
|
||||
// which is why we warn in the Content docstring to minimize the
|
||||
// use of required attributes on merged bodies.
|
||||
diags = diags.Append(&Diagnostic{
|
||||
Severity: DiagError,
|
||||
Summary: "Missing required argument",
|
||||
Detail: fmt.Sprintf(
|
||||
"The argument %q is required, but was not set.",
|
||||
attrS.Name,
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
leftoverBody := MergeBodies(mergedLeftovers)
|
||||
return content, leftoverBody, diags
|
||||
}
|
@ -0,0 +1,288 @@
|
||||
package hcl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
)
|
||||
|
||||
// Index is a helper function that performs the same operation as the index
|
||||
// operator in the HCL expression language. That is, the result is the
|
||||
// same as it would be for collection[key] in a configuration expression.
|
||||
//
|
||||
// This is exported so that applications can perform indexing in a manner
|
||||
// consistent with how the language does it, including handling of null and
|
||||
// unknown values, etc.
|
||||
//
|
||||
// Diagnostics are produced if the given combination of values is not valid.
|
||||
// Therefore a pointer to a source range must be provided to use in diagnostics,
|
||||
// though nil can be provided if the calling application is going to
|
||||
// ignore the subject of the returned diagnostics anyway.
|
||||
func Index(collection, key cty.Value, srcRange *Range) (cty.Value, Diagnostics) {
|
||||
if collection.IsNull() {
|
||||
return cty.DynamicVal, Diagnostics{
|
||||
{
|
||||
Severity: DiagError,
|
||||
Summary: "Attempt to index null value",
|
||||
Detail: "This value is null, so it does not have any indices.",
|
||||
Subject: srcRange,
|
||||
},
|
||||
}
|
||||
}
|
||||
if key.IsNull() {
|
||||
return cty.DynamicVal, Diagnostics{
|
||||
{
|
||||
Severity: DiagError,
|
||||
Summary: "Invalid index",
|
||||
Detail: "Can't use a null value as an indexing key.",
|
||||
Subject: srcRange,
|
||||
},
|
||||
}
|
||||
}
|
||||
ty := collection.Type()
|
||||
kty := key.Type()
|
||||
if kty == cty.DynamicPseudoType || ty == cty.DynamicPseudoType {
|
||||
return cty.DynamicVal, nil
|
||||
}
|
||||
|
||||
switch {
|
||||
|
||||
case ty.IsListType() || ty.IsTupleType() || ty.IsMapType():
|
||||
var wantType cty.Type
|
||||
switch {
|
||||
case ty.IsListType() || ty.IsTupleType():
|
||||
wantType = cty.Number
|
||||
case ty.IsMapType():
|
||||
wantType = cty.String
|
||||
default:
|
||||
// should never happen
|
||||
panic("don't know what key type we want")
|
||||
}
|
||||
|
||||
key, keyErr := convert.Convert(key, wantType)
|
||||
if keyErr != nil {
|
||||
return cty.DynamicVal, Diagnostics{
|
||||
{
|
||||
Severity: DiagError,
|
||||
Summary: "Invalid index",
|
||||
Detail: fmt.Sprintf(
|
||||
"The given key does not identify an element in this collection value: %s.",
|
||||
keyErr.Error(),
|
||||
),
|
||||
Subject: srcRange,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
has := collection.HasIndex(key)
|
||||
if !has.IsKnown() {
|
||||
if ty.IsTupleType() {
|
||||
return cty.DynamicVal, nil
|
||||
} else {
|
||||
return cty.UnknownVal(ty.ElementType()), nil
|
||||
}
|
||||
}
|
||||
if has.False() {
|
||||
// We have a more specialized error message for the situation of
|
||||
// using a fractional number to index into a sequence, because
|
||||
// that will tend to happen if the user is trying to use division
|
||||
// to calculate an index and not realizing that HCL does float
|
||||
// division rather than integer division.
|
||||
if (ty.IsListType() || ty.IsTupleType()) && key.Type().Equals(cty.Number) {
|
||||
if key.IsKnown() && !key.IsNull() {
|
||||
bf := key.AsBigFloat()
|
||||
if _, acc := bf.Int(nil); acc != big.Exact {
|
||||
return cty.DynamicVal, Diagnostics{
|
||||
{
|
||||
Severity: DiagError,
|
||||
Summary: "Invalid index",
|
||||
Detail: fmt.Sprintf("The given key does not identify an element in this collection value: indexing a sequence requires a whole number, but the given index (%g) has a fractional part.", bf),
|
||||
Subject: srcRange,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cty.DynamicVal, Diagnostics{
|
||||
{
|
||||
Severity: DiagError,
|
||||
Summary: "Invalid index",
|
||||
Detail: "The given key does not identify an element in this collection value.",
|
||||
Subject: srcRange,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return collection.Index(key), nil
|
||||
|
||||
case ty.IsObjectType():
|
||||
key, keyErr := convert.Convert(key, cty.String)
|
||||
if keyErr != nil {
|
||||
return cty.DynamicVal, Diagnostics{
|
||||
{
|
||||
Severity: DiagError,
|
||||
Summary: "Invalid index",
|
||||
Detail: fmt.Sprintf(
|
||||
"The given key does not identify an element in this collection value: %s.",
|
||||
keyErr.Error(),
|
||||
),
|
||||
Subject: srcRange,
|
||||
},
|
||||
}
|
||||
}
|
||||
if !collection.IsKnown() {
|
||||
return cty.DynamicVal, nil
|
||||
}
|
||||
if !key.IsKnown() {
|
||||
return cty.DynamicVal, nil
|
||||
}
|
||||
|
||||
attrName := key.AsString()
|
||||
|
||||
if !ty.HasAttribute(attrName) {
|
||||
return cty.DynamicVal, Diagnostics{
|
||||
{
|
||||
Severity: DiagError,
|
||||
Summary: "Invalid index",
|
||||
Detail: "The given key does not identify an element in this collection value.",
|
||||
Subject: srcRange,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return collection.GetAttr(attrName), nil
|
||||
|
||||
default:
|
||||
return cty.DynamicVal, Diagnostics{
|
||||
{
|
||||
Severity: DiagError,
|
||||
Summary: "Invalid index",
|
||||
Detail: "This value does not have any indices.",
|
||||
Subject: srcRange,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// GetAttr is a helper function that performs the same operation as the
|
||||
// attribute access in the HCL expression language. That is, the result is the
|
||||
// same as it would be for obj.attr in a configuration expression.
|
||||
//
|
||||
// This is exported so that applications can access attributes in a manner
|
||||
// consistent with how the language does it, including handling of null and
|
||||
// unknown values, etc.
|
||||
//
|
||||
// Diagnostics are produced if the given combination of values is not valid.
|
||||
// Therefore a pointer to a source range must be provided to use in diagnostics,
|
||||
// though nil can be provided if the calling application is going to
|
||||
// ignore the subject of the returned diagnostics anyway.
|
||||
func GetAttr(obj cty.Value, attrName string, srcRange *Range) (cty.Value, Diagnostics) {
|
||||
if obj.IsNull() {
|
||||
return cty.DynamicVal, Diagnostics{
|
||||
{
|
||||
Severity: DiagError,
|
||||
Summary: "Attempt to get attribute from null value",
|
||||
Detail: "This value is null, so it does not have any attributes.",
|
||||
Subject: srcRange,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
ty := obj.Type()
|
||||
switch {
|
||||
case ty.IsObjectType():
|
||||
if !ty.HasAttribute(attrName) {
|
||||
return cty.DynamicVal, Diagnostics{
|
||||
{
|
||||
Severity: DiagError,
|
||||
Summary: "Unsupported attribute",
|
||||
Detail: fmt.Sprintf("This object does not have an attribute named %q.", attrName),
|
||||
Subject: srcRange,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if !obj.IsKnown() {
|
||||
return cty.UnknownVal(ty.AttributeType(attrName)), nil
|
||||
}
|
||||
|
||||
return obj.GetAttr(attrName), nil
|
||||
case ty.IsMapType():
|
||||
if !obj.IsKnown() {
|
||||
return cty.UnknownVal(ty.ElementType()), nil
|
||||
}
|
||||
|
||||
idx := cty.StringVal(attrName)
|
||||
if obj.HasIndex(idx).False() {
|
||||
return cty.DynamicVal, Diagnostics{
|
||||
{
|
||||
Severity: DiagError,
|
||||
Summary: "Missing map element",
|
||||
Detail: fmt.Sprintf("This map does not have an element with the key %q.", attrName),
|
||||
Subject: srcRange,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return obj.Index(idx), nil
|
||||
case ty == cty.DynamicPseudoType:
|
||||
return cty.DynamicVal, nil
|
||||
default:
|
||||
return cty.DynamicVal, Diagnostics{
|
||||
{
|
||||
Severity: DiagError,
|
||||
Summary: "Unsupported attribute",
|
||||
Detail: "This value does not have any attributes.",
|
||||
Subject: srcRange,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ApplyPath is a helper function that applies a cty.Path to a value using the
|
||||
// indexing and attribute access operations from HCL.
|
||||
//
|
||||
// This is similar to calling the path's own Apply method, but ApplyPath uses
|
||||
// the more relaxed typing rules that apply to these operations in HCL, rather
|
||||
// than cty's relatively-strict rules. ApplyPath is implemented in terms of
|
||||
// Index and GetAttr, and so it has the same behavior for individual steps
|
||||
// but will stop and return any errors returned by intermediate steps.
|
||||
//
|
||||
// Diagnostics are produced if the given path cannot be applied to the given
|
||||
// value. Therefore a pointer to a source range must be provided to use in
|
||||
// diagnostics, though nil can be provided if the calling application is going
|
||||
// to ignore the subject of the returned diagnostics anyway.
|
||||
func ApplyPath(val cty.Value, path cty.Path, srcRange *Range) (cty.Value, Diagnostics) {
|
||||
var diags Diagnostics
|
||||
|
||||
for _, step := range path {
|
||||
var stepDiags Diagnostics
|
||||
switch ts := step.(type) {
|
||||
case cty.IndexStep:
|
||||
val, stepDiags = Index(val, ts.Key, srcRange)
|
||||
case cty.GetAttrStep:
|
||||
val, stepDiags = GetAttr(val, ts.Name, srcRange)
|
||||
default:
|
||||
// Should never happen because the above are all of the step types.
|
||||
diags = diags.Append(&Diagnostic{
|
||||
Severity: DiagError,
|
||||
Summary: "Invalid path step",
|
||||
Detail: fmt.Sprintf("Go type %T is not a valid path step. This is a bug in this program.", step),
|
||||
Subject: srcRange,
|
||||
})
|
||||
return cty.DynamicVal, diags
|
||||
}
|
||||
|
||||
diags = append(diags, stepDiags...)
|
||||
if stepDiags.HasErrors() {
|
||||
return cty.DynamicVal, diags
|
||||
}
|
||||
}
|
||||
|
||||
return val, diags
|
||||
}
|
@ -0,0 +1,275 @@
|
||||
package hcl
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Pos represents a single position in a source file, by addressing the
|
||||
// start byte of a unicode character encoded in UTF-8.
|
||||
//
|
||||
// Pos is generally used only in the context of a Range, which then defines
|
||||
// which source file the position is within.
|
||||
type Pos struct {
|
||||
// Line is the source code line where this position points. Lines are
|
||||
// counted starting at 1 and incremented for each newline character
|
||||
// encountered.
|
||||
Line int
|
||||
|
||||
// Column is the source code column where this position points, in
|
||||
// unicode characters, with counting starting at 1.
|
||||
//
|
||||
// Column counts characters as they appear visually, so for example a
|
||||
// latin letter with a combining diacritic mark counts as one character.
|
||||
// This is intended for rendering visual markers against source code in
|
||||
// contexts where these diacritics would be rendered in a single character
|
||||
// cell. Technically speaking, Column is counting grapheme clusters as
|
||||
// used in unicode normalization.
|
||||
Column int
|
||||
|
||||
// Byte is the byte offset into the file where the indicated character
|
||||
// begins. This is a zero-based offset to the first byte of the first
|
||||
// UTF-8 codepoint sequence in the character, and thus gives a position
|
||||
// that can be resolved _without_ awareness of Unicode characters.
|
||||
Byte int
|
||||
}
|
||||
|
||||
// InitialPos is a suitable position to use to mark the start of a file.
|
||||
var InitialPos = Pos{Byte: 0, Line: 1, Column: 1}
|
||||
|
||||
// Range represents a span of characters between two positions in a source
|
||||
// file.
|
||||
//
|
||||
// This struct is usually used by value in types that represent AST nodes,
|
||||
// but by pointer in types that refer to the positions of other objects,
|
||||
// such as in diagnostics.
|
||||
type Range struct {
|
||||
// Filename is the name of the file into which this range's positions
|
||||
// point.
|
||||
Filename string
|
||||
|
||||
// Start and End represent the bounds of this range. Start is inclusive
|
||||
// and End is exclusive.
|
||||
Start, End Pos
|
||||
}
|
||||
|
||||
// RangeBetween returns a new range that spans from the beginning of the
|
||||
// start range to the end of the end range.
|
||||
//
|
||||
// The result is meaningless if the two ranges do not belong to the same
|
||||
// source file or if the end range appears before the start range.
|
||||
func RangeBetween(start, end Range) Range {
|
||||
return Range{
|
||||
Filename: start.Filename,
|
||||
Start: start.Start,
|
||||
End: end.End,
|
||||
}
|
||||
}
|
||||
|
||||
// RangeOver returns a new range that covers both of the given ranges and
|
||||
// possibly additional content between them if the two ranges do not overlap.
|
||||
//
|
||||
// If either range is empty then it is ignored. The result is empty if both
|
||||
// given ranges are empty.
|
||||
//
|
||||
// The result is meaningless if the two ranges to not belong to the same
|
||||
// source file.
|
||||
func RangeOver(a, b Range) Range {
|
||||
if a.Empty() {
|
||||
return b
|
||||
}
|
||||
if b.Empty() {
|
||||
return a
|
||||
}
|
||||
|
||||
var start, end Pos
|
||||
if a.Start.Byte < b.Start.Byte {
|
||||
start = a.Start
|
||||
} else {
|
||||
start = b.Start
|
||||
}
|
||||
if a.End.Byte > b.End.Byte {
|
||||
end = a.End
|
||||
} else {
|
||||
end = b.End
|
||||
}
|
||||
return Range{
|
||||
Filename: a.Filename,
|
||||
Start: start,
|
||||
End: end,
|
||||
}
|
||||
}
|
||||
|
||||
// ContainsPos returns true if and only if the given position is contained within
|
||||
// the receiving range.
|
||||
//
|
||||
// In the unlikely case that the line/column information disagree with the byte
|
||||
// offset information in the given position or receiving range, the byte
|
||||
// offsets are given priority.
|
||||
func (r Range) ContainsPos(pos Pos) bool {
|
||||
return r.ContainsOffset(pos.Byte)
|
||||
}
|
||||
|
||||
// ContainsOffset returns true if and only if the given byte offset is within
|
||||
// the receiving Range.
|
||||
func (r Range) ContainsOffset(offset int) bool {
|
||||
return offset >= r.Start.Byte && offset < r.End.Byte
|
||||
}
|
||||
|
||||
// Ptr returns a pointer to a copy of the receiver. This is a convenience when
|
||||
// ranges in places where pointers are required, such as in Diagnostic, but
|
||||
// the range in question is returned from a method. Go would otherwise not
|
||||
// allow one to take the address of a function call.
|
||||
func (r Range) Ptr() *Range {
|
||||
return &r
|
||||
}
|
||||
|
||||
// String returns a compact string representation of the receiver.
|
||||
// Callers should generally prefer to present a range more visually,
|
||||
// e.g. via markers directly on the relevant portion of source code.
|
||||
func (r Range) String() string {
|
||||
if r.Start.Line == r.End.Line {
|
||||
return fmt.Sprintf(
|
||||
"%s:%d,%d-%d",
|
||||
r.Filename,
|
||||
r.Start.Line, r.Start.Column,
|
||||
r.End.Column,
|
||||
)
|
||||
} else {
|
||||
return fmt.Sprintf(
|
||||
"%s:%d,%d-%d,%d",
|
||||
r.Filename,
|
||||
r.Start.Line, r.Start.Column,
|
||||
r.End.Line, r.End.Column,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (r Range) Empty() bool {
|
||||
return r.Start.Byte == r.End.Byte
|
||||
}
|
||||
|
||||
// CanSliceBytes returns true if SliceBytes could return an accurate
|
||||
// sub-slice of the given slice.
|
||||
//
|
||||
// This effectively tests whether the start and end offsets of the range
|
||||
// are within the bounds of the slice, and thus whether SliceBytes can be
|
||||
// trusted to produce an accurate start and end position within that slice.
|
||||
func (r Range) CanSliceBytes(b []byte) bool {
|
||||
switch {
|
||||
case r.Start.Byte < 0 || r.Start.Byte > len(b):
|
||||
return false
|
||||
case r.End.Byte < 0 || r.End.Byte > len(b):
|
||||
return false
|
||||
case r.End.Byte < r.Start.Byte:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// SliceBytes returns a sub-slice of the given slice that is covered by the
|
||||
// receiving range, assuming that the given slice is the source code of the
|
||||
// file indicated by r.Filename.
|
||||
//
|
||||
// If the receiver refers to any byte offsets that are outside of the slice
|
||||
// then the result is constrained to the overlapping portion only, to avoid
|
||||
// a panic. Use CanSliceBytes to determine if the result is guaranteed to
|
||||
// be an accurate span of the requested range.
|
||||
func (r Range) SliceBytes(b []byte) []byte {
|
||||
start := r.Start.Byte
|
||||
end := r.End.Byte
|
||||
if start < 0 {
|
||||
start = 0
|
||||
} else if start > len(b) {
|
||||
start = len(b)
|
||||
}
|
||||
if end < 0 {
|
||||
end = 0
|
||||
} else if end > len(b) {
|
||||
end = len(b)
|
||||
}
|
||||
if end < start {
|
||||
end = start
|
||||
}
|
||||
return b[start:end]
|
||||
}
|
||||
|
||||
// Overlaps returns true if the receiver and the other given range share any
|
||||
// characters in common.
|
||||
func (r Range) Overlaps(other Range) bool {
|
||||
switch {
|
||||
case r.Filename != other.Filename:
|
||||
// If the ranges are in different files then they can't possibly overlap
|
||||
return false
|
||||
case r.Empty() || other.Empty():
|
||||
// Empty ranges can never overlap
|
||||
return false
|
||||
case r.ContainsOffset(other.Start.Byte) || r.ContainsOffset(other.End.Byte):
|
||||
return true
|
||||
case other.ContainsOffset(r.Start.Byte) || other.ContainsOffset(r.End.Byte):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Overlap finds a range that is either identical to or a sub-range of both
|
||||
// the receiver and the other given range. It returns an empty range
|
||||
// within the receiver if there is no overlap between the two ranges.
|
||||
//
|
||||
// A non-empty result is either identical to or a subset of the receiver.
|
||||
func (r Range) Overlap(other Range) Range {
|
||||
if !r.Overlaps(other) {
|
||||
// Start == End indicates an empty range
|
||||
return Range{
|
||||
Filename: r.Filename,
|
||||
Start: r.Start,
|
||||
End: r.Start,
|
||||
}
|
||||
}
|
||||
|
||||
var start, end Pos
|
||||
if r.Start.Byte > other.Start.Byte {
|
||||
start = r.Start
|
||||
} else {
|
||||
start = other.Start
|
||||
}
|
||||
if r.End.Byte < other.End.Byte {
|
||||
end = r.End
|
||||
} else {
|
||||
end = other.End
|
||||
}
|
||||
|
||||
return Range{
|
||||
Filename: r.Filename,
|
||||
Start: start,
|
||||
End: end,
|
||||
}
|
||||
}
|
||||
|
||||
// PartitionAround finds the portion of the given range that overlaps with
|
||||
// the reciever and returns three ranges: the portion of the reciever that
|
||||
// precedes the overlap, the overlap itself, and then the portion of the
|
||||
// reciever that comes after the overlap.
|
||||
//
|
||||
// If the two ranges do not overlap then all three returned ranges are empty.
|
||||
//
|
||||
// If the given range aligns with or extends beyond either extent of the
|
||||
// reciever then the corresponding outer range will be empty.
|
||||
func (r Range) PartitionAround(other Range) (before, overlap, after Range) {
|
||||
overlap = r.Overlap(other)
|
||||
if overlap.Empty() {
|
||||
return overlap, overlap, overlap
|
||||
}
|
||||
|
||||
before = Range{
|
||||
Filename: r.Filename,
|
||||
Start: r.Start,
|
||||
End: overlap.Start,
|
||||
}
|
||||
after = Range{
|
||||
Filename: r.Filename,
|
||||
Start: overlap.End,
|
||||
End: r.End,
|
||||
}
|
||||
|
||||
return before, overlap, after
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
package hcl
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
|
||||
"github.com/apparentlymart/go-textseg/v12/textseg"
|
||||
)
|
||||
|
||||
// RangeScanner is a helper that will scan over a buffer using a bufio.SplitFunc
|
||||
// and visit a source range for each token matched.
|
||||
//
|
||||
// For example, this can be used with bufio.ScanLines to find the source range
|
||||
// for each line in the file, skipping over the actual newline characters, which
|
||||
// may be useful when printing source code snippets as part of diagnostic
|
||||
// messages.
|
||||
//
|
||||
// The line and column information in the returned ranges is produced by
|
||||
// counting newline characters and grapheme clusters respectively, which
|
||||
// mimics the behavior we expect from a parser when producing ranges.
|
||||
type RangeScanner struct {
|
||||
filename string
|
||||
b []byte
|
||||
cb bufio.SplitFunc
|
||||
|
||||
pos Pos // position of next byte to process in b
|
||||
cur Range // latest range
|
||||
tok []byte // slice of b that is covered by cur
|
||||
err error // error from last scan, if any
|
||||
}
|
||||
|
||||
// NewRangeScanner creates a new RangeScanner for the given buffer, producing
|
||||
// ranges for the given filename.
|
||||
//
|
||||
// Since ranges have grapheme-cluster granularity rather than byte granularity,
|
||||
// the scanner will produce incorrect results if the given SplitFunc creates
|
||||
// tokens between grapheme cluster boundaries. In particular, it is incorrect
|
||||
// to use RangeScanner with bufio.ScanRunes because it will produce tokens
|
||||
// around individual UTF-8 sequences, which will split any multi-sequence
|
||||
// grapheme clusters.
|
||||
func NewRangeScanner(b []byte, filename string, cb bufio.SplitFunc) *RangeScanner {
|
||||
return NewRangeScannerFragment(b, filename, InitialPos, cb)
|
||||
}
|
||||
|
||||
// NewRangeScannerFragment is like NewRangeScanner but the ranges it produces
|
||||
// will be offset by the given starting position, which is appropriate for
|
||||
// sub-slices of a file, whereas NewRangeScanner assumes it is scanning an
|
||||
// entire file.
|
||||
func NewRangeScannerFragment(b []byte, filename string, start Pos, cb bufio.SplitFunc) *RangeScanner {
|
||||
return &RangeScanner{
|
||||
filename: filename,
|
||||
b: b,
|
||||
cb: cb,
|
||||
pos: start,
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *RangeScanner) Scan() bool {
|
||||
if sc.pos.Byte >= len(sc.b) || sc.err != nil {
|
||||
// All done
|
||||
return false
|
||||
}
|
||||
|
||||
// Since we're operating on an in-memory buffer, we always pass the whole
|
||||
// remainder of the buffer to our SplitFunc and set isEOF to let it know
|
||||
// that it has the whole thing.
|
||||
advance, token, err := sc.cb(sc.b[sc.pos.Byte:], true)
|
||||
|
||||
// Since we are setting isEOF to true this should never happen, but
|
||||
// if it does we will just abort and assume the SplitFunc is misbehaving.
|
||||
if advance == 0 && token == nil && err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
sc.err = err
|
||||
sc.cur = Range{
|
||||
Filename: sc.filename,
|
||||
Start: sc.pos,
|
||||
End: sc.pos,
|
||||
}
|
||||
sc.tok = nil
|
||||
return false
|
||||
}
|
||||
|
||||
sc.tok = token
|
||||
start := sc.pos
|
||||
end := sc.pos
|
||||
new := sc.pos
|
||||
|
||||
// adv is similar to token but it also includes any subsequent characters
|
||||
// we're being asked to skip over by the SplitFunc.
|
||||
// adv is a slice covering any additional bytes we are skipping over, based
|
||||
// on what the SplitFunc told us to do with advance.
|
||||
adv := sc.b[sc.pos.Byte : sc.pos.Byte+advance]
|
||||
|
||||
// We now need to scan over our token to count the grapheme clusters
|
||||
// so we can correctly advance Column, and count the newlines so we
|
||||
// can correctly advance Line.
|
||||
advR := bytes.NewReader(adv)
|
||||
gsc := bufio.NewScanner(advR)
|
||||
advanced := 0
|
||||
gsc.Split(textseg.ScanGraphemeClusters)
|
||||
for gsc.Scan() {
|
||||
gr := gsc.Bytes()
|
||||
new.Byte += len(gr)
|
||||
new.Column++
|
||||
|
||||
// We rely here on the fact that \r\n is considered a grapheme cluster
|
||||
// and so we don't need to worry about miscounting additional lines
|
||||
// on files with Windows-style line endings.
|
||||
if len(gr) != 0 && (gr[0] == '\r' || gr[0] == '\n') {
|
||||
new.Column = 1
|
||||
new.Line++
|
||||
}
|
||||
|
||||
if advanced < len(token) {
|
||||
// If we've not yet found the end of our token then we'll
|
||||
// also push our "end" marker along.
|
||||
// (if advance > len(token) then we'll stop moving "end" early
|
||||
// so that the caller only sees the range covered by token.)
|
||||
end = new
|
||||
}
|
||||
advanced += len(gr)
|
||||
}
|
||||
|
||||
sc.cur = Range{
|
||||
Filename: sc.filename,
|
||||
Start: start,
|
||||
End: end,
|
||||
}
|
||||
sc.pos = new
|
||||
return true
|
||||
}
|
||||
|
||||
// Range returns a range that covers the latest token obtained after a call
|
||||
// to Scan returns true.
|
||||
func (sc *RangeScanner) Range() Range {
|
||||
return sc.cur
|
||||
}
|
||||
|
||||
// Bytes returns the slice of the input buffer that is covered by the range
|
||||
// that would be returned by Range.
|
||||
func (sc *RangeScanner) Bytes() []byte {
|
||||
return sc.tok
|
||||
}
|
||||
|
||||
// Err can be called after Scan returns false to determine if the latest read
|
||||
// resulted in an error, and obtain that error if so.
|
||||
func (sc *RangeScanner) Err() error {
|
||||
return sc.err
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package hcl
|
||||
|
||||
// BlockHeaderSchema represents the shape of a block header, and is
|
||||
// used for matching blocks within bodies.
|
||||
type BlockHeaderSchema struct {
|
||||
Type string
|
||||
LabelNames []string
|
||||
}
|
||||
|
||||
// AttributeSchema represents the requirements for an attribute, and is used
|
||||
// for matching attributes within bodies.
|
||||
type AttributeSchema struct {
|
||||
Name string
|
||||
Required bool
|
||||
}
|
||||
|
||||
// BodySchema represents the desired shallow structure of a body.
|
||||
type BodySchema struct {
|
||||
Attributes []AttributeSchema
|
||||
Blocks []BlockHeaderSchema
|
||||
}
|
@ -0,0 +1,691 @@
|
||||
# HCL Syntax-Agnostic Information Model
|
||||
|
||||
This is the specification for the general information model (abstract types and
|
||||
semantics) for hcl. HCL is a system for defining configuration languages for
|
||||
applications. The HCL information model is designed to support multiple
|
||||
concrete syntaxes for configuration, each with a mapping to the model defined
|
||||
in this specification.
|
||||
|
||||
The two primary syntaxes intended for use in conjunction with this model are
|
||||
[the HCL native syntax](./hclsyntax/spec.md) and [the JSON syntax](./json/spec.md).
|
||||
In principle other syntaxes are possible as long as either their language model
|
||||
is sufficiently rich to express the concepts described in this specification
|
||||
or the language targets a well-defined subset of the specification.
|
||||
|
||||
## Structural Elements
|
||||
|
||||
The primary structural element is the _body_, which is a container representing
|
||||
a set of zero or more _attributes_ and a set of zero or more _blocks_.
|
||||
|
||||
A _configuration file_ is the top-level object, and will usually be produced
|
||||
by reading a file from disk and parsing it as a particular syntax. A
|
||||
configuration file has its own _body_, representing the top-level attributes
|
||||
and blocks.
|
||||
|
||||
An _attribute_ is a name and value pair associated with a body. Attribute names
|
||||
are unique within a given body. Attribute values are provided as _expressions_,
|
||||
which are discussed in detail in a later section.
|
||||
|
||||
A _block_ is a nested structure that has a _type name_, zero or more string
|
||||
_labels_ (e.g. identifiers), and a nested body.
|
||||
|
||||
Together the structural elements create a hierarchical data structure, with
|
||||
attributes intended to represent the direct properties of a particular object
|
||||
in the calling application, and blocks intended to represent child objects
|
||||
of a particular object.
|
||||
|
||||
## Body Content
|
||||
|
||||
To support the expression of the HCL concepts in languages whose information
|
||||
model is a subset of HCL's, such as JSON, a _body_ is an opaque container
|
||||
whose content can only be accessed by providing information on the expected
|
||||
structure of the content.
|
||||
|
||||
The specification for each syntax must describe how its physical constructs
|
||||
are mapped on to body content given a schema. For syntaxes that have
|
||||
first-class syntax distinguishing attributes and bodies this can be relatively
|
||||
straightforward, while more detailed mapping rules may be required in syntaxes
|
||||
where the representation of attributes vs. blocks is ambiguous.
|
||||
|
||||
### Schema-driven Processing
|
||||
|
||||
Schema-driven processing is the primary way to access body content.
|
||||
A _body schema_ is a description of what is expected within a particular body,
|
||||
which can then be used to extract the _body content_, which then provides
|
||||
access to the specific attributes and blocks requested.
|
||||
|
||||
A _body schema_ consists of a list of _attribute schemata_ and
|
||||
_block header schemata_:
|
||||
|
||||
- An _attribute schema_ provides the name of an attribute and whether its
|
||||
presence is required.
|
||||
|
||||
- A _block header schema_ provides a block type name and the semantic names
|
||||
assigned to each of the labels of that block type, if any.
|
||||
|
||||
Within a schema, it is an error to request the same attribute name twice or
|
||||
to request a block type whose name is also an attribute name. While this can
|
||||
in principle be supported in some syntaxes, in other syntaxes the attribute
|
||||
and block namespaces are combined and so an attribute cannot coexist with
|
||||
a block whose type name is identical to the attribute name.
|
||||
|
||||
The result of applying a body schema to a body is _body content_, which
|
||||
consists of an _attribute map_ and a _block sequence_:
|
||||
|
||||
- The _attribute map_ is a map data structure whose keys are attribute names
|
||||
and whose values are _expressions_ that represent the corresponding attribute
|
||||
values.
|
||||
|
||||
- The _block sequence_ is an ordered sequence of blocks, with each specifying
|
||||
a block _type name_, the sequence of _labels_ specified for the block,
|
||||
and the body object (not body _content_) representing the block's own body.
|
||||
|
||||
After obtaining _body content_, the calling application may continue processing
|
||||
by evaluating attribute expressions and/or recursively applying further
|
||||
schema-driven processing to the child block bodies.
|
||||
|
||||
**Note:** The _body schema_ is intentionally minimal, to reduce the set of
|
||||
mapping rules that must be defined for each syntax. Higher-level utility
|
||||
libraries may be provided to assist in the construction of a schema and
|
||||
perform additional processing, such as automatically evaluating attribute
|
||||
expressions and assigning their result values into a data structure, or
|
||||
recursively applying a schema to child blocks. Such utilities are not part of
|
||||
this core specification and will vary depending on the capabilities and idiom
|
||||
of the implementation language.
|
||||
|
||||
### _Dynamic Attributes_ Processing
|
||||
|
||||
The _schema-driven_ processing model is useful when the expected structure
|
||||
of a body is known a priori by the calling application. Some blocks are
|
||||
instead more free-form, such as a user-provided set of arbitrary key/value
|
||||
pairs.
|
||||
|
||||
The alternative _dynamic attributes_ processing mode allows for this more
|
||||
ad-hoc approach. Processing in this mode behaves as if a schema had been
|
||||
constructed without any _block header schemata_ and with an attribute
|
||||
schema for each distinct key provided within the physical representation
|
||||
of the body.
|
||||
|
||||
The means by which _distinct keys_ are identified is dependent on the
|
||||
physical syntax; this processing mode assumes that the syntax has a way
|
||||
to enumerate keys provided by the author and identify expressions that
|
||||
correspond with those keys, but does not define the means by which this is
|
||||
done.
|
||||
|
||||
The result of _dynamic attributes_ processing is an _attribute map_ as
|
||||
defined in the previous section. No _block sequence_ is produced in this
|
||||
processing mode.
|
||||
|
||||
### Partial Processing of Body Content
|
||||
|
||||
Under _schema-driven processing_, by default the given schema is assumed
|
||||
to be exhaustive, such that any attribute or block not matched by schema
|
||||
elements is considered an error. This allows feedback about unsupported
|
||||
attributes and blocks (such as typos) to be provided.
|
||||
|
||||
An alternative is _partial processing_, where any additional elements within
|
||||
the body are not considered an error.
|
||||
|
||||
Under partial processing, the result is both body content as described
|
||||
above _and_ a new body that represents any body elements that remain after
|
||||
the schema has been processed.
|
||||
|
||||
Specifically:
|
||||
|
||||
- Any attribute whose name is specified in the schema is returned in body
|
||||
content and elided from the new body.
|
||||
|
||||
- Any block whose type is specified in the schema is returned in body content
|
||||
and elided from the new body.
|
||||
|
||||
- Any attribute or block _not_ meeting the above conditions is placed into
|
||||
the new body, unmodified.
|
||||
|
||||
The new body can then be recursively processed using any of the body
|
||||
processing models. This facility allows different subsets of body content
|
||||
to be processed by different parts of the calling application.
|
||||
|
||||
Processing a body in two steps — first partial processing of a source body,
|
||||
then exhaustive processing of the returned body — is equivalent to single-step
|
||||
processing with a schema that is the union of the schemata used
|
||||
across the two steps.
|
||||
|
||||
## Expressions
|
||||
|
||||
Attribute values are represented by _expressions_. Depending on the concrete
|
||||
syntax in use, an expression may just be a literal value or it may describe
|
||||
a computation in terms of literal values, variables, and functions.
|
||||
|
||||
Each syntax defines its own representation of expressions. For syntaxes based
|
||||
in languages that do not have any non-literal expression syntax, it is
|
||||
recommended to embed the template language from
|
||||
[the native syntax](./hclsyntax/spec.md) e.g. as a post-processing step on
|
||||
string literals.
|
||||
|
||||
### Expression Evaluation
|
||||
|
||||
In order to obtain a concrete value, each expression must be _evaluated_.
|
||||
Evaluation is performed in terms of an evaluation context, which
|
||||
consists of the following:
|
||||
|
||||
- An _evaluation mode_, which is defined below.
|
||||
- A _variable scope_, which provides a set of named variables for use in
|
||||
expressions.
|
||||
- A _function table_, which provides a set of named functions for use in
|
||||
expressions.
|
||||
|
||||
The _evaluation mode_ allows for two different interpretations of an
|
||||
expression:
|
||||
|
||||
- In _literal-only mode_, variables and functions are not available and it
|
||||
is assumed that the calling application's intent is to treat the attribute
|
||||
value as a literal.
|
||||
|
||||
- In _full expression mode_, variables and functions are defined and it is
|
||||
assumed that the calling application wishes to provide a full expression
|
||||
language for definition of the attribute value.
|
||||
|
||||
The actual behavior of these two modes depends on the syntax in use. For
|
||||
languages with first-class expression syntax, these two modes may be considered
|
||||
equivalent, with _literal-only mode_ simply not defining any variables or
|
||||
functions. For languages that embed arbitrary expressions via string templates,
|
||||
_literal-only mode_ may disable such processing, allowing literal strings to
|
||||
pass through without interpretation as templates.
|
||||
|
||||
Since literal-only mode does not support variables and functions, it is an
|
||||
error for the calling application to enable this mode and yet provide a
|
||||
variable scope and/or function table.
|
||||
|
||||
## Values and Value Types
|
||||
|
||||
The result of expression evaluation is a _value_. Each value has a _type_,
|
||||
which is dynamically determined during evaluation. The _variable scope_ in
|
||||
the evaluation context is a map from variable name to value, using the same
|
||||
definition of value.
|
||||
|
||||
The type system for HCL values is intended to be of a level abstraction
|
||||
suitable for configuration of various applications. A well-defined,
|
||||
implementation-language-agnostic type system is defined to allow for
|
||||
consistent processing of configuration across many implementation languages.
|
||||
Concrete implementations may provide additional functionality to lower
|
||||
HCL values and types to corresponding native language types, which may then
|
||||
impose additional constraints on the values outside of the scope of this
|
||||
specification.
|
||||
|
||||
Two values are _equal_ if and only if they have identical types and their
|
||||
values are equal according to the rules of their shared type.
|
||||
|
||||
### Primitive Types
|
||||
|
||||
The primitive types are _string_, _bool_, and _number_.
|
||||
|
||||
A _string_ is a sequence of unicode characters. Two strings are equal if
|
||||
NFC normalization ([UAX#15](http://unicode.org/reports/tr15/)
|
||||
of each string produces two identical sequences of characters.
|
||||
NFC normalization ensures that, for example, a precomposed combination of a
|
||||
latin letter and a diacritic compares equal with the letter followed by
|
||||
a combining diacritic.
|
||||
|
||||
The _bool_ type has only two non-null values: _true_ and _false_. Two bool
|
||||
values are equal if and only if they are either both true or both false.
|
||||
|
||||
A _number_ is an arbitrary-precision floating point value. An implementation
|
||||
_must_ make the full-precision values available to the calling application
|
||||
for interpretation into any suitable number representation. An implementation
|
||||
may in practice implement numbers with limited precision so long as the
|
||||
following constraints are met:
|
||||
|
||||
- Integers are represented with at least 256 bits.
|
||||
- Non-integer numbers are represented as floating point values with a
|
||||
mantissa of at least 256 bits and a signed binary exponent of at least
|
||||
16 bits.
|
||||
- An error is produced if an integer value given in source cannot be
|
||||
represented precisely.
|
||||
- An error is produced if a non-integer value cannot be represented due to
|
||||
overflow.
|
||||
- A non-integer number is rounded to the nearest possible value when a
|
||||
value is of too high a precision to be represented.
|
||||
|
||||
The _number_ type also requires representation of both positive and negative
|
||||
infinity. A "not a number" (NaN) value is _not_ provided nor used.
|
||||
|
||||
Two number values are equal if they are numerically equal to the precision
|
||||
associated with the number. Positive infinity and negative infinity are
|
||||
equal to themselves but not to each other. Positive infinity is greater than
|
||||
any other number value, and negative infinity is less than any other number
|
||||
value.
|
||||
|
||||
Some syntaxes may be unable to represent numeric literals of arbitrary
|
||||
precision. This must be defined in the syntax specification as part of its
|
||||
description of mapping numeric literals to HCL values.
|
||||
|
||||
### Structural Types
|
||||
|
||||
_Structural types_ are types that are constructed by combining other types.
|
||||
Each distinct combination of other types is itself a distinct type. There
|
||||
are two structural type _kinds_:
|
||||
|
||||
- _Object types_ are constructed of a set of named attributes, each of which
|
||||
has a type. Attribute names are always strings. (_Object_ attributes are a
|
||||
distinct idea from _body_ attributes, though calling applications
|
||||
may choose to blur the distinction by use of common naming schemes.)
|
||||
- _Tuple types_ are constructed of a sequence of elements, each of which
|
||||
has a type.
|
||||
|
||||
Values of structural types are compared for equality in terms of their
|
||||
attributes or elements. A structural type value is equal to another if and
|
||||
only if all of the corresponding attributes or elements are equal.
|
||||
|
||||
Two structural types are identical if they are of the same kind and
|
||||
have attributes or elements with identical types.
|
||||
|
||||
### Collection Types
|
||||
|
||||
_Collection types_ are types that combine together an arbitrary number of
|
||||
values of some other single type. There are three collection type _kinds_:
|
||||
|
||||
- _List types_ represent ordered sequences of values of their element type.
|
||||
- _Map types_ represent values of their element type accessed via string keys.
|
||||
- _Set types_ represent unordered sets of distinct values of their element type.
|
||||
|
||||
For each of these kinds and each distinct element type there is a distinct
|
||||
collection type. For example, "list of string" is a distinct type from
|
||||
"set of string", and "list of number" is a distinct type from "list of string".
|
||||
|
||||
Values of collection types are compared for equality in terms of their
|
||||
elements. A collection type value is equal to another if and only if both
|
||||
have the same number of elements and their corresponding elements are equal.
|
||||
|
||||
Two collection types are identical if they are of the same kind and have
|
||||
the same element type.
|
||||
|
||||
### Null values
|
||||
|
||||
Each type has a null value. The null value of a type represents the absence
|
||||
of a value, but with type information retained to allow for type checking.
|
||||
|
||||
Null values are used primarily to represent the conditional absence of a
|
||||
body attribute. In a syntax with a conditional operator, one of the result
|
||||
values of that conditional may be null to indicate that the attribute should be
|
||||
considered not present in that case.
|
||||
|
||||
Calling applications _should_ consider an attribute with a null value as
|
||||
equivalent to the value not being present at all.
|
||||
|
||||
A null value of a particular type is equal to itself.
|
||||
|
||||
### Unknown Values and the Dynamic Pseudo-type
|
||||
|
||||
An _unknown value_ is a placeholder for a value that is not yet known.
|
||||
Operations on unknown values themselves return unknown values that have a
|
||||
type appropriate to the operation. For example, adding together two unknown
|
||||
numbers yields an unknown number, while comparing two unknown values of any
|
||||
type for equality yields an unknown bool.
|
||||
|
||||
Each type has a distinct unknown value. For example, an unknown _number_ is
|
||||
a distinct value from an unknown _string_.
|
||||
|
||||
_The dynamic pseudo-type_ is a placeholder for a type that is not yet known.
|
||||
The only values of this type are its null value and its unknown value. It is
|
||||
referred to as a _pseudo-type_ because it should not be considered a type in
|
||||
its own right, but rather as a placeholder for a type yet to be established.
|
||||
The unknown value of the dynamic pseudo-type is referred to as _the dynamic
|
||||
value_.
|
||||
|
||||
Operations on values of the dynamic pseudo-type behave as if it is a value
|
||||
of the expected type, optimistically assuming that once the value and type
|
||||
are known they will be valid for the operation. For example, adding together
|
||||
a number and the dynamic value produces an unknown number.
|
||||
|
||||
Unknown values and the dynamic pseudo-type can be used as a mechanism for
|
||||
partial type checking and semantic checking: by evaluating an expression with
|
||||
all variables set to an unknown value, the expression can be evaluated to
|
||||
produce an unknown value of a given type, or produce an error if any operation
|
||||
is provably invalid with only type information.
|
||||
|
||||
Unknown values and the dynamic pseudo-type must never be returned from
|
||||
operations unless at least one operand is unknown or dynamic. Calling
|
||||
applications are guaranteed that unless the global scope includes unknown
|
||||
values, or the function table includes functions that return unknown values,
|
||||
no expression will evaluate to an unknown value. The calling application is
|
||||
thus in total control over the use and meaning of unknown values.
|
||||
|
||||
The dynamic pseudo-type is identical only to itself.
|
||||
|
||||
### Capsule Types
|
||||
|
||||
A _capsule type_ is a custom type defined by the calling application. A value
|
||||
of a capsule type is considered opaque to HCL, but may be accepted
|
||||
by functions provided by the calling application.
|
||||
|
||||
A particular capsule type is identical only to itself. The equality of two
|
||||
values of the same capsule type is defined by the calling application. No
|
||||
other operations are supported for values of capsule types.
|
||||
|
||||
Support for capsule types in a HCL implementation is optional. Capsule types
|
||||
are intended to allow calling applications to pass through values that are
|
||||
not part of the standard type system. For example, an application that
|
||||
deals with raw binary data may define a capsule type representing a byte
|
||||
array, and provide functions that produce or operate on byte arrays.
|
||||
|
||||
### Type Specifications
|
||||
|
||||
In certain situations it is necessary to define expectations about the expected
|
||||
type of a value. Whereas two _types_ have a commutative _identity_ relationship,
|
||||
a type has a non-commutative _matches_ relationship with a _type specification_.
|
||||
A type specification is, in practice, just a different interpretation of a
|
||||
type such that:
|
||||
|
||||
- Any type _matches_ any type that it is identical to.
|
||||
|
||||
- Any type _matches_ the dynamic pseudo-type.
|
||||
|
||||
For example, given a type specification "list of dynamic pseudo-type", the
|
||||
concrete types "list of string" and "list of map" match, but the
|
||||
type "set of string" does not.
|
||||
|
||||
## Functions and Function Calls
|
||||
|
||||
The evaluation context used to evaluate an expression includes a function
|
||||
table, which represents an application-defined set of named functions
|
||||
available for use in expressions.
|
||||
|
||||
Each syntax defines whether function calls are supported and how they are
|
||||
physically represented in source code, but the semantics of function calls are
|
||||
defined here to ensure consistent results across syntaxes and to allow
|
||||
applications to provide functions that are interoperable with all syntaxes.
|
||||
|
||||
A _function_ is defined from the following elements:
|
||||
|
||||
- Zero or more _positional parameters_, each with a name used for documentation,
|
||||
a type specification for expected argument values, and a flag for whether
|
||||
each of null values, unknown values, and values of the dynamic pseudo-type
|
||||
are accepted.
|
||||
|
||||
- Zero or one _variadic parameters_, with the same structure as the _positional_
|
||||
parameters, which if present collects any additional arguments provided at
|
||||
the function call site.
|
||||
|
||||
- A _result type definition_, which specifies the value type returned for each
|
||||
valid sequence of argument values.
|
||||
|
||||
- A _result value definition_, which specifies the value returned for each
|
||||
valid sequence of argument values.
|
||||
|
||||
A _function call_, regardless of source syntax, consists of a sequence of
|
||||
argument values. The argument values are each mapped to a corresponding
|
||||
parameter as follows:
|
||||
|
||||
- For each of the function's positional parameters in sequence, take the next
|
||||
argument. If there are no more arguments, the call is erroneous.
|
||||
|
||||
- If the function has a variadic parameter, take all remaining arguments that
|
||||
where not yet assigned to a positional parameter and collect them into
|
||||
a sequence of variadic arguments that each correspond to the variadic
|
||||
parameter.
|
||||
|
||||
- If the function has _no_ variadic parameter, it is an error if any arguments
|
||||
remain after taking one argument for each positional parameter.
|
||||
|
||||
After mapping each argument to a parameter, semantic checking proceeds
|
||||
for each argument:
|
||||
|
||||
- If the argument value corresponding to a parameter does not match the
|
||||
parameter's type specification, the call is erroneous.
|
||||
|
||||
- If the argument value corresponding to a parameter is null and the parameter
|
||||
is not specified as accepting nulls, the call is erroneous.
|
||||
|
||||
- If the argument value corresponding to a parameter is the dynamic value
|
||||
and the parameter is not specified as accepting values of the dynamic
|
||||
pseudo-type, the call is valid but its _result type_ is forced to be the
|
||||
dynamic pseudo type.
|
||||
|
||||
- If neither of the above conditions holds for any argument, the call is
|
||||
valid and the function's value type definition is used to determine the
|
||||
call's _result type_. A function _may_ vary its result type depending on
|
||||
the argument _values_ as well as the argument _types_; for example, a
|
||||
function that decodes a JSON value will return a different result type
|
||||
depending on the data structure described by the given JSON source code.
|
||||
|
||||
If semantic checking succeeds without error, the call is _executed_:
|
||||
|
||||
- For each argument, if its value is unknown and its corresponding parameter
|
||||
is not specified as accepting unknowns, the _result value_ is forced to be an
|
||||
unknown value of the result type.
|
||||
|
||||
- If the previous condition does not apply, the function's result value
|
||||
definition is used to determine the call's _result value_.
|
||||
|
||||
The result of a function call expression is either an error, if one of the
|
||||
erroneous conditions above applies, or the _result value_.
|
||||
|
||||
## Type Conversions and Unification
|
||||
|
||||
Values given in configuration may not always match the expectations of the
|
||||
operations applied to them or to the calling application. In such situations,
|
||||
automatic type conversion is attempted as a convenience to the user.
|
||||
|
||||
Along with conversions to a _specified_ type, it is sometimes necessary to
|
||||
ensure that a selection of values are all of the _same_ type, without any
|
||||
constraint on which type that is. This is the process of _type unification_,
|
||||
which attempts to find the most general type that all of the given types can
|
||||
be converted to.
|
||||
|
||||
Both type conversions and unification are defined in the syntax-agnostic
|
||||
model to ensure consistency of behavior between syntaxes.
|
||||
|
||||
Type conversions are broadly characterized into two categories: _safe_ and
|
||||
_unsafe_. A conversion is "safe" if any distinct value of the source type
|
||||
has a corresponding distinct value in the target type. A conversion is
|
||||
"unsafe" if either the target type values are _not_ distinct (information
|
||||
may be lost in conversion) or if some values of the source type do not have
|
||||
any corresponding value in the target type. An unsafe conversion may result
|
||||
in an error.
|
||||
|
||||
A given type can always be converted to itself, which is a no-op.
|
||||
|
||||
### Conversion of Null Values
|
||||
|
||||
All null values are safely convertable to a null value of any other type,
|
||||
regardless of other type-specific rules specified in the sections below.
|
||||
|
||||
### Conversion to and from the Dynamic Pseudo-type
|
||||
|
||||
Conversion _from_ the dynamic pseudo-type _to_ any other type always succeeds,
|
||||
producing an unknown value of the target type.
|
||||
|
||||
Conversion of any value _to_ the dynamic pseudo-type is a no-op. The result
|
||||
is the input value, verbatim. This is the only situation where the conversion
|
||||
result value is not of the given target type.
|
||||
|
||||
### Primitive Type Conversions
|
||||
|
||||
Bidirectional conversions are available between the string and number types,
|
||||
and between the string and boolean types.
|
||||
|
||||
The bool value true corresponds to the string containing the characters "true",
|
||||
while the bool value false corresponds to the string containing the characters
|
||||
"false". Conversion from bool to string is safe, while the converse is
|
||||
unsafe. The strings "1" and "0" are alternative string representations
|
||||
of true and false respectively. It is an error to convert a string other than
|
||||
the four in this paragraph to type bool.
|
||||
|
||||
A number value is converted to string by translating its integer portion
|
||||
into a sequence of decimal digits (`0` through `9`), and then if it has a
|
||||
non-zero fractional part, a period `.` followed by a sequence of decimal
|
||||
digits representing its fractional part. No exponent portion is included.
|
||||
The number is converted at its full precision. Conversion from number to
|
||||
string is safe.
|
||||
|
||||
A string is converted to a number value by reversing the above mapping.
|
||||
No exponent portion is allowed. Conversion from string to number is unsafe.
|
||||
It is an error to convert a string that does not comply with the expected
|
||||
syntax to type number.
|
||||
|
||||
No direct conversion is available between the bool and number types.
|
||||
|
||||
### Collection and Structural Type Conversions
|
||||
|
||||
Conversion from set types to list types is _safe_, as long as their
|
||||
element types are safely convertable. If the element types are _unsafely_
|
||||
convertable, then the collection conversion is also unsafe. Each set element
|
||||
becomes a corresponding list element, in an undefined order. Although no
|
||||
particular ordering is required, implementations _should_ produce list
|
||||
elements in a consistent order for a given input set, as a convenience
|
||||
to calling applications.
|
||||
|
||||
Conversion from list types to set types is _unsafe_, as long as their element
|
||||
types are convertable. Each distinct list item becomes a distinct set item.
|
||||
If two list items are equal, one of the two is lost in the conversion.
|
||||
|
||||
Conversion from tuple types to list types permitted if all of the
|
||||
tuple element types are convertable to the target list element type.
|
||||
The safety of the conversion depends on the safety of each of the element
|
||||
conversions. Each element in turn is converted to the list element type,
|
||||
producing a list of identical length.
|
||||
|
||||
Conversion from tuple types to set types is permitted, behaving as if the
|
||||
tuple type was first converted to a list of the same element type and then
|
||||
that list converted to the target set type.
|
||||
|
||||
Conversion from object types to map types is permitted if all of the object
|
||||
attribute types are convertable to the target map element type. The safety
|
||||
of the conversion depends on the safety of each of the attribute conversions.
|
||||
Each attribute in turn is converted to the map element type, and map element
|
||||
keys are set to the name of each corresponding object attribute.
|
||||
|
||||
Conversion from list and set types to tuple types is permitted, following
|
||||
the opposite steps as the converse conversions. Such conversions are _unsafe_.
|
||||
It is an error to convert a list or set to a tuple type whose number of
|
||||
elements does not match the list or set length.
|
||||
|
||||
Conversion from map types to object types is permitted if each map key
|
||||
corresponds to an attribute in the target object type. It is an error to
|
||||
convert from a map value whose set of keys does not exactly match the target
|
||||
type's attributes. The conversion takes the opposite steps of the converse
|
||||
conversion.
|
||||
|
||||
Conversion from one object type to another is permitted as long as the
|
||||
common attribute names have convertable types. Any attribute present in the
|
||||
target type but not in the source type is populated with a null value of
|
||||
the appropriate type.
|
||||
|
||||
Conversion from one tuple type to another is permitted as long as the
|
||||
tuples have the same length and the elements have convertable types.
|
||||
|
||||
### Type Unification
|
||||
|
||||
Type unification is an operation that takes a list of types and attempts
|
||||
to find a single type to which they can all be converted. Since some
|
||||
type pairs have bidirectional conversions, preference is given to _safe_
|
||||
conversions. In technical terms, all possible types are arranged into
|
||||
a lattice, from which a most general supertype is selected where possible.
|
||||
|
||||
The type resulting from type unification may be one of the input types, or
|
||||
it may be an entirely new type produced by combination of two or more
|
||||
input types.
|
||||
|
||||
The following rules do not guarantee a valid result. In addition to these
|
||||
rules, unification fails if any of the given types are not convertable
|
||||
(per the above rules) to the selected result type.
|
||||
|
||||
The following unification rules apply transitively. That is, if a rule is
|
||||
defined from A to B, and one from B to C, then A can unify to C.
|
||||
|
||||
Number and bool types both unify with string by preferring string.
|
||||
|
||||
Two collection types of the same kind unify according to the unification
|
||||
of their element types.
|
||||
|
||||
List and set types unify by preferring the list type.
|
||||
|
||||
Map and object types unify by preferring the object type.
|
||||
|
||||
List, set and tuple types unify by preferring the tuple type.
|
||||
|
||||
The dynamic pseudo-type unifies with any other type by selecting that other
|
||||
type. The dynamic pseudo-type is the result type only if _all_ input types
|
||||
are the dynamic pseudo-type.
|
||||
|
||||
Two object types unify by constructing a new type whose attributes are
|
||||
the union of those of the two input types. Any common attributes themselves
|
||||
have their types unified.
|
||||
|
||||
Two tuple types of the same length unify constructing a new type of the
|
||||
same length whose elements are the unification of the corresponding elements
|
||||
in the two input types.
|
||||
|
||||
## Static Analysis
|
||||
|
||||
In most applications, full expression evaluation is sufficient for understanding
|
||||
the provided configuration. However, some specialized applications require more
|
||||
direct access to the physical structures in the expressions, which can for
|
||||
example allow the construction of new language constructs in terms of the
|
||||
existing syntax elements.
|
||||
|
||||
Since static analysis analyses the physical structure of configuration, the
|
||||
details will vary depending on syntax. Each syntax must decide which of its
|
||||
physical structures corresponds to the following analyses, producing error
|
||||
diagnostics if they are applied to inappropriate expressions.
|
||||
|
||||
The following are the required static analysis functions:
|
||||
|
||||
- **Static List**: Require list/tuple construction syntax to be used and
|
||||
return a list of expressions for each of the elements given.
|
||||
|
||||
- **Static Map**: Require map/object construction syntax to be used and
|
||||
return a list of key/value pairs -- both expressions -- for each of
|
||||
the elements given. The usual constraint that a map key must be a string
|
||||
must not apply to this analysis, thus allowing applications to interpret
|
||||
arbitrary keys as they see fit.
|
||||
|
||||
- **Static Call**: Require function call syntax to be used and return an
|
||||
object describing the called function name and a list of expressions
|
||||
representing each of the call arguments.
|
||||
|
||||
- **Static Traversal**: Require a reference to a symbol in the variable
|
||||
scope and return a description of the path from the root scope to the
|
||||
accessed attribute or index.
|
||||
|
||||
The intent of a calling application using these features is to require a more
|
||||
rigid interpretation of the configuration than in expression evaluation.
|
||||
Syntax implementations should make use of the extra contextual information
|
||||
provided in order to make an intuitive mapping onto the constructs of the
|
||||
underlying syntax, possibly interpreting the expression slightly differently
|
||||
than it would be interpreted in normal evaluation.
|
||||
|
||||
Each syntax must define which of its expression elements each of the analyses
|
||||
above applies to, and how those analyses behave given those expression elements.
|
||||
|
||||
## Implementation Considerations
|
||||
|
||||
Implementations of this specification are free to adopt any strategy that
|
||||
produces behavior consistent with the specification. This non-normative
|
||||
section describes some possible implementation strategies that are consistent
|
||||
with the goals of this specification.
|
||||
|
||||
### Language-agnosticism
|
||||
|
||||
The language-agnosticism of this specification assumes that certain behaviors
|
||||
are implemented separately for each syntax:
|
||||
|
||||
- Matching of a body schema with the physical elements of a body in the
|
||||
source language, to determine correspondence between physical constructs
|
||||
and schema elements.
|
||||
|
||||
- Implementing the _dynamic attributes_ body processing mode by either
|
||||
interpreting all physical constructs as attributes or producing an error
|
||||
if non-attribute constructs are present.
|
||||
|
||||
- Providing an evaluation function for all possible expressions that produces
|
||||
a value given an evaluation context.
|
||||
|
||||
- Providing the static analysis functionality described above in a manner that
|
||||
makes sense within the convention of the syntax.
|
||||
|
||||
The suggested implementation strategy is to use an implementation language's
|
||||
closest concept to an _abstract type_, _virtual type_ or _interface type_
|
||||
to represent both Body and Expression. Each language-specific implementation
|
||||
can then provide an implementation of each of these types wrapping AST nodes
|
||||
or other physical constructs from the language parser.
|
@ -0,0 +1,40 @@
|
||||
package hcl
|
||||
|
||||
import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
type staticExpr struct {
|
||||
val cty.Value
|
||||
rng Range
|
||||
}
|
||||
|
||||
// StaticExpr returns an Expression that always evaluates to the given value.
|
||||
//
|
||||
// This is useful to substitute default values for expressions that are
|
||||
// not explicitly given in configuration and thus would otherwise have no
|
||||
// Expression to return.
|
||||
//
|
||||
// Since expressions are expected to have a source range, the caller must
|
||||
// provide one. Ideally this should be a real source range, but it can
|
||||
// be a synthetic one (with an empty-string filename) if no suitable range
|
||||
// is available.
|
||||
func StaticExpr(val cty.Value, rng Range) Expression {
|
||||
return staticExpr{val, rng}
|
||||
}
|
||||
|
||||
func (e staticExpr) Value(ctx *EvalContext) (cty.Value, Diagnostics) {
|
||||
return e.val, nil
|
||||
}
|
||||
|
||||
func (e staticExpr) Variables() []Traversal {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e staticExpr) Range() Range {
|
||||
return e.rng
|
||||
}
|
||||
|
||||
func (e staticExpr) StartRange() Range {
|
||||
return e.rng
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
package hcl
|
||||
|
||||
import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// File is the top-level node that results from parsing a HCL file.
|
||||
type File struct {
|
||||
Body Body
|
||||
Bytes []byte
|
||||
|
||||
// Nav is used to integrate with the "hcled" editor integration package,
|
||||
// and with diagnostic information formatters. It is not for direct use
|
||||
// by a calling application.
|
||||
Nav interface{}
|
||||
}
|
||||
|
||||
// Block represents a nested block within a Body.
|
||||
type Block struct {
|
||||
Type string
|
||||
Labels []string
|
||||
Body Body
|
||||
|
||||
DefRange Range // Range that can be considered the "definition" for seeking in an editor
|
||||
TypeRange Range // Range for the block type declaration specifically.
|
||||
LabelRanges []Range // Ranges for the label values specifically.
|
||||
}
|
||||
|
||||
// Blocks is a sequence of Block.
|
||||
type Blocks []*Block
|
||||
|
||||
// Attributes is a set of attributes keyed by their names.
|
||||
type Attributes map[string]*Attribute
|
||||
|
||||
// Body is a container for attributes and blocks. It serves as the primary
|
||||
// unit of hierarchical structure within configuration.
|
||||
//
|
||||
// The content of a body cannot be meaningfully interpreted without a schema,
|
||||
// so Body represents the raw body content and has methods that allow the
|
||||
// content to be extracted in terms of a given schema.
|
||||
type Body interface {
|
||||
// Content verifies that the entire body content conforms to the given
|
||||
// schema and then returns it, and/or returns diagnostics. The returned
|
||||
// body content is valid if non-nil, regardless of whether Diagnostics
|
||||
// are provided, but diagnostics should still be eventually shown to
|
||||
// the user.
|
||||
Content(schema *BodySchema) (*BodyContent, Diagnostics)
|
||||
|
||||
// PartialContent is like Content except that it permits the configuration
|
||||
// to contain additional blocks or attributes not specified in the
|
||||
// schema. If any are present, the returned Body is non-nil and contains
|
||||
// the remaining items from the body that were not selected by the schema.
|
||||
PartialContent(schema *BodySchema) (*BodyContent, Body, Diagnostics)
|
||||
|
||||
// JustAttributes attempts to interpret all of the contents of the body
|
||||
// as attributes, allowing for the contents to be accessed without a priori
|
||||
// knowledge of the structure.
|
||||
//
|
||||
// The behavior of this method depends on the body's source language.
|
||||
// Some languages, like JSON, can't distinguish between attributes and
|
||||
// blocks without schema hints, but for languages that _can_ error
|
||||
// diagnostics will be generated if any blocks are present in the body.
|
||||
//
|
||||
// Diagnostics may be produced for other reasons too, such as duplicate
|
||||
// declarations of the same attribute.
|
||||
JustAttributes() (Attributes, Diagnostics)
|
||||
|
||||
// MissingItemRange returns a range that represents where a missing item
|
||||
// might hypothetically be inserted. This is used when producing
|
||||
// diagnostics about missing required attributes or blocks. Not all bodies
|
||||
// will have an obvious single insertion point, so the result here may
|
||||
// be rather arbitrary.
|
||||
MissingItemRange() Range
|
||||
}
|
||||
|
||||
// BodyContent is the result of applying a BodySchema to a Body.
|
||||
type BodyContent struct {
|
||||
Attributes Attributes
|
||||
Blocks Blocks
|
||||
|
||||
MissingItemRange Range
|
||||
}
|
||||
|
||||
// Attribute represents an attribute from within a body.
|
||||
type Attribute struct {
|
||||
Name string
|
||||
Expr Expression
|
||||
|
||||
Range Range
|
||||
NameRange Range
|
||||
}
|
||||
|
||||
// Expression is a literal value or an expression provided in the
|
||||
// configuration, which can be evaluated within a scope to produce a value.
|
||||
type Expression interface {
|
||||
// Value returns the value resulting from evaluating the expression
|
||||
// in the given evaluation context.
|
||||
//
|
||||
// The context may be nil, in which case the expression may contain
|
||||
// only constants and diagnostics will be produced for any non-constant
|
||||
// sub-expressions. (The exact definition of this depends on the source
|
||||
// language.)
|
||||
//
|
||||
// The context may instead be set but have either its Variables or
|
||||
// Functions maps set to nil, in which case only use of these features
|
||||
// will return diagnostics.
|
||||
//
|
||||
// Different diagnostics are provided depending on whether the given
|
||||
// context maps are nil or empty. In the former case, the message
|
||||
// tells the user that variables/functions are not permitted at all,
|
||||
// while in the latter case usage will produce a "not found" error for
|
||||
// the specific symbol in question.
|
||||
Value(ctx *EvalContext) (cty.Value, Diagnostics)
|
||||
|
||||
// Variables returns a list of variables referenced in the receiving
|
||||
// expression. These are expressed as absolute Traversals, so may include
|
||||
// additional information about how the variable is used, such as
|
||||
// attribute lookups, which the calling application can potentially use
|
||||
// to only selectively populate the scope.
|
||||
Variables() []Traversal
|
||||
|
||||
Range() Range
|
||||
StartRange() Range
|
||||
}
|
||||
|
||||
// OfType filters the receiving block sequence by block type name,
|
||||
// returning a new block sequence including only the blocks of the
|
||||
// requested type.
|
||||
func (els Blocks) OfType(typeName string) Blocks {
|
||||
ret := make(Blocks, 0)
|
||||
for _, el := range els {
|
||||
if el.Type == typeName {
|
||||
ret = append(ret, el)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// ByType transforms the receiving block sequence into a map from type
|
||||
// name to block sequences of only that type.
|
||||
func (els Blocks) ByType() map[string]Blocks {
|
||||
ret := make(map[string]Blocks)
|
||||
for _, el := range els {
|
||||
ty := el.Type
|
||||
if ret[ty] == nil {
|
||||
ret[ty] = make(Blocks, 0, 1)
|
||||
}
|
||||
ret[ty] = append(ret[ty], el)
|
||||
}
|
||||
return ret
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
package hcl
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// The methods in this file all have the general pattern of making a best-effort
|
||||
// to find one or more constructs that contain a given source position.
|
||||
//
|
||||
// These all operate by delegating to an optional method of the same name and
|
||||
// signature on the file's root body, allowing each syntax to potentially
|
||||
// provide its own implementations of these. For syntaxes that don't implement
|
||||
// them, the result is always nil.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// BlocksAtPos attempts to find all of the blocks that contain the given
|
||||
// position, ordered so that the outermost block is first and the innermost
|
||||
// block is last. This is a best-effort method that may not be able to produce
|
||||
// a complete result for all positions or for all HCL syntaxes.
|
||||
//
|
||||
// If the returned slice is non-empty, the first element is guaranteed to
|
||||
// represent the same block as would be the result of OutermostBlockAtPos and
|
||||
// the last element the result of InnermostBlockAtPos. However, the
|
||||
// implementation may return two different objects describing the same block,
|
||||
// so comparison by pointer identity is not possible.
|
||||
//
|
||||
// The result is nil if no blocks at all contain the given position.
|
||||
func (f *File) BlocksAtPos(pos Pos) []*Block {
|
||||
// The root body of the file must implement this interface in order
|
||||
// to support BlocksAtPos.
|
||||
type Interface interface {
|
||||
BlocksAtPos(pos Pos) []*Block
|
||||
}
|
||||
|
||||
impl, ok := f.Body.(Interface)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return impl.BlocksAtPos(pos)
|
||||
}
|
||||
|
||||
// OutermostBlockAtPos attempts to find a top-level block in the receiving file
|
||||
// that contains the given position. This is a best-effort method that may not
|
||||
// be able to produce a result for all positions or for all HCL syntaxes.
|
||||
//
|
||||
// The result is nil if no single block could be selected for any reason.
|
||||
func (f *File) OutermostBlockAtPos(pos Pos) *Block {
|
||||
// The root body of the file must implement this interface in order
|
||||
// to support OutermostBlockAtPos.
|
||||
type Interface interface {
|
||||
OutermostBlockAtPos(pos Pos) *Block
|
||||
}
|
||||
|
||||
impl, ok := f.Body.(Interface)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return impl.OutermostBlockAtPos(pos)
|
||||
}
|
||||
|
||||
// InnermostBlockAtPos attempts to find the most deeply-nested block in the
|
||||
// receiving file that contains the given position. This is a best-effort
|
||||
// method that may not be able to produce a result for all positions or for
|
||||
// all HCL syntaxes.
|
||||
//
|
||||
// The result is nil if no single block could be selected for any reason.
|
||||
func (f *File) InnermostBlockAtPos(pos Pos) *Block {
|
||||
// The root body of the file must implement this interface in order
|
||||
// to support InnermostBlockAtPos.
|
||||
type Interface interface {
|
||||
InnermostBlockAtPos(pos Pos) *Block
|
||||
}
|
||||
|
||||
impl, ok := f.Body.(Interface)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return impl.InnermostBlockAtPos(pos)
|
||||
}
|
||||
|
||||
// OutermostExprAtPos attempts to find an expression in the receiving file
|
||||
// that contains the given position. This is a best-effort method that may not
|
||||
// be able to produce a result for all positions or for all HCL syntaxes.
|
||||
//
|
||||
// Since expressions are often nested inside one another, this method returns
|
||||
// the outermost "root" expression that is not contained by any other.
|
||||
//
|
||||
// The result is nil if no single expression could be selected for any reason.
|
||||
func (f *File) OutermostExprAtPos(pos Pos) Expression {
|
||||
// The root body of the file must implement this interface in order
|
||||
// to support OutermostExprAtPos.
|
||||
type Interface interface {
|
||||
OutermostExprAtPos(pos Pos) Expression
|
||||
}
|
||||
|
||||
impl, ok := f.Body.(Interface)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return impl.OutermostExprAtPos(pos)
|
||||
}
|
||||
|
||||
// AttributeAtPos attempts to find an attribute definition in the receiving
|
||||
// file that contains the given position. This is a best-effort method that may
|
||||
// not be able to produce a result for all positions or for all HCL syntaxes.
|
||||
//
|
||||
// The result is nil if no single attribute could be selected for any reason.
|
||||
func (f *File) AttributeAtPos(pos Pos) *Attribute {
|
||||
// The root body of the file must implement this interface in order
|
||||
// to support OutermostExprAtPos.
|
||||
type Interface interface {
|
||||
AttributeAtPos(pos Pos) *Attribute
|
||||
}
|
||||
|
||||
impl, ok := f.Body.(Interface)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return impl.AttributeAtPos(pos)
|
||||
}
|
@ -0,0 +1,293 @@
|
||||
package hcl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// A Traversal is a description of traversing through a value through a
|
||||
// series of operations such as attribute lookup, index lookup, etc.
|
||||
//
|
||||
// It is used to look up values in scopes, for example.
|
||||
//
|
||||
// The traversal operations are implementations of interface Traverser.
|
||||
// This is a closed set of implementations, so the interface cannot be
|
||||
// implemented from outside this package.
|
||||
//
|
||||
// A traversal can be absolute (its first value is a symbol name) or relative
|
||||
// (starts from an existing value).
|
||||
type Traversal []Traverser
|
||||
|
||||
// TraversalJoin appends a relative traversal to an absolute traversal to
|
||||
// produce a new absolute traversal.
|
||||
func TraversalJoin(abs Traversal, rel Traversal) Traversal {
|
||||
if abs.IsRelative() {
|
||||
panic("first argument to TraversalJoin must be absolute")
|
||||
}
|
||||
if !rel.IsRelative() {
|
||||
panic("second argument to TraversalJoin must be relative")
|
||||
}
|
||||
|
||||
ret := make(Traversal, len(abs)+len(rel))
|
||||
copy(ret, abs)
|
||||
copy(ret[len(abs):], rel)
|
||||
return ret
|
||||
}
|
||||
|
||||
// TraverseRel applies the receiving traversal to the given value, returning
|
||||
// the resulting value. This is supported only for relative traversals,
|
||||
// and will panic if applied to an absolute traversal.
|
||||
func (t Traversal) TraverseRel(val cty.Value) (cty.Value, Diagnostics) {
|
||||
if !t.IsRelative() {
|
||||
panic("can't use TraverseRel on an absolute traversal")
|
||||
}
|
||||
|
||||
current := val
|
||||
var diags Diagnostics
|
||||
for _, tr := range t {
|
||||
var newDiags Diagnostics
|
||||
current, newDiags = tr.TraversalStep(current)
|
||||
diags = append(diags, newDiags...)
|
||||
if newDiags.HasErrors() {
|
||||
return cty.DynamicVal, diags
|
||||
}
|
||||
}
|
||||
return current, diags
|
||||
}
|
||||
|
||||
// TraverseAbs applies the receiving traversal to the given eval context,
|
||||
// returning the resulting value. This is supported only for absolute
|
||||
// traversals, and will panic if applied to a relative traversal.
|
||||
func (t Traversal) TraverseAbs(ctx *EvalContext) (cty.Value, Diagnostics) {
|
||||
if t.IsRelative() {
|
||||
panic("can't use TraverseAbs on a relative traversal")
|
||||
}
|
||||
|
||||
split := t.SimpleSplit()
|
||||
root := split.Abs[0].(TraverseRoot)
|
||||
name := root.Name
|
||||
|
||||
thisCtx := ctx
|
||||
hasNonNil := false
|
||||
for thisCtx != nil {
|
||||
if thisCtx.Variables == nil {
|
||||
thisCtx = thisCtx.parent
|
||||
continue
|
||||
}
|
||||
hasNonNil = true
|
||||
val, exists := thisCtx.Variables[name]
|
||||
if exists {
|
||||
return split.Rel.TraverseRel(val)
|
||||
}
|
||||
thisCtx = thisCtx.parent
|
||||
}
|
||||
|
||||
if !hasNonNil {
|
||||
return cty.DynamicVal, Diagnostics{
|
||||
{
|
||||
Severity: DiagError,
|
||||
Summary: "Variables not allowed",
|
||||
Detail: "Variables may not be used here.",
|
||||
Subject: &root.SrcRange,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
suggestions := make([]string, 0, len(ctx.Variables))
|
||||
thisCtx = ctx
|
||||
for thisCtx != nil {
|
||||
for k := range thisCtx.Variables {
|
||||
suggestions = append(suggestions, k)
|
||||
}
|
||||
thisCtx = thisCtx.parent
|
||||
}
|
||||
suggestion := nameSuggestion(name, suggestions)
|
||||
if suggestion != "" {
|
||||
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
|
||||
}
|
||||
|
||||
return cty.DynamicVal, Diagnostics{
|
||||
{
|
||||
Severity: DiagError,
|
||||
Summary: "Unknown variable",
|
||||
Detail: fmt.Sprintf("There is no variable named %q.%s", name, suggestion),
|
||||
Subject: &root.SrcRange,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// IsRelative returns true if the receiver is a relative traversal, or false
|
||||
// otherwise.
|
||||
func (t Traversal) IsRelative() bool {
|
||||
if len(t) == 0 {
|
||||
return true
|
||||
}
|
||||
if _, firstIsRoot := t[0].(TraverseRoot); firstIsRoot {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// SimpleSplit returns a TraversalSplit where the name lookup is the absolute
|
||||
// part and the remainder is the relative part. Supported only for
|
||||
// absolute traversals, and will panic if applied to a relative traversal.
|
||||
//
|
||||
// This can be used by applications that have a relatively-simple variable
|
||||
// namespace where only the top-level is directly populated in the scope, with
|
||||
// everything else handled by relative lookups from those initial values.
|
||||
func (t Traversal) SimpleSplit() TraversalSplit {
|
||||
if t.IsRelative() {
|
||||
panic("can't use SimpleSplit on a relative traversal")
|
||||
}
|
||||
return TraversalSplit{
|
||||
Abs: t[0:1],
|
||||
Rel: t[1:],
|
||||
}
|
||||
}
|
||||
|
||||
// RootName returns the root name for a absolute traversal. Will panic if
|
||||
// called on a relative traversal.
|
||||
func (t Traversal) RootName() string {
|
||||
if t.IsRelative() {
|
||||
panic("can't use RootName on a relative traversal")
|
||||
|
||||
}
|
||||
return t[0].(TraverseRoot).Name
|
||||
}
|
||||
|
||||
// SourceRange returns the source range for the traversal.
|
||||
func (t Traversal) SourceRange() Range {
|
||||
if len(t) == 0 {
|
||||
// Nothing useful to return here, but we'll return something
|
||||
// that's correctly-typed at least.
|
||||
return Range{}
|
||||
}
|
||||
|
||||
return RangeBetween(t[0].SourceRange(), t[len(t)-1].SourceRange())
|
||||
}
|
||||
|
||||
// TraversalSplit represents a pair of traversals, the first of which is
|
||||
// an absolute traversal and the second of which is relative to the first.
|
||||
//
|
||||
// This is used by calling applications that only populate prefixes of the
|
||||
// traversals in the scope, with Abs representing the part coming from the
|
||||
// scope and Rel representing the remaining steps once that part is
|
||||
// retrieved.
|
||||
type TraversalSplit struct {
|
||||
Abs Traversal
|
||||
Rel Traversal
|
||||
}
|
||||
|
||||
// TraverseAbs traverses from a scope to the value resulting from the
|
||||
// absolute traversal.
|
||||
func (t TraversalSplit) TraverseAbs(ctx *EvalContext) (cty.Value, Diagnostics) {
|
||||
return t.Abs.TraverseAbs(ctx)
|
||||
}
|
||||
|
||||
// TraverseRel traverses from a given value, assumed to be the result of
|
||||
// TraverseAbs on some scope, to a final result for the entire split traversal.
|
||||
func (t TraversalSplit) TraverseRel(val cty.Value) (cty.Value, Diagnostics) {
|
||||
return t.Rel.TraverseRel(val)
|
||||
}
|
||||
|
||||
// Traverse is a convenience function to apply TraverseAbs followed by
|
||||
// TraverseRel.
|
||||
func (t TraversalSplit) Traverse(ctx *EvalContext) (cty.Value, Diagnostics) {
|
||||
v1, diags := t.TraverseAbs(ctx)
|
||||
if diags.HasErrors() {
|
||||
return cty.DynamicVal, diags
|
||||
}
|
||||
v2, newDiags := t.TraverseRel(v1)
|
||||
diags = append(diags, newDiags...)
|
||||
return v2, diags
|
||||
}
|
||||
|
||||
// Join concatenates together the Abs and Rel parts to produce a single
|
||||
// absolute traversal.
|
||||
func (t TraversalSplit) Join() Traversal {
|
||||
return TraversalJoin(t.Abs, t.Rel)
|
||||
}
|
||||
|
||||
// RootName returns the root name for the absolute part of the split.
|
||||
func (t TraversalSplit) RootName() string {
|
||||
return t.Abs.RootName()
|
||||
}
|
||||
|
||||
// A Traverser is a step within a Traversal.
|
||||
type Traverser interface {
|
||||
TraversalStep(cty.Value) (cty.Value, Diagnostics)
|
||||
SourceRange() Range
|
||||
isTraverserSigil() isTraverser
|
||||
}
|
||||
|
||||
// Embed this in a struct to declare it as a Traverser
|
||||
type isTraverser struct {
|
||||
}
|
||||
|
||||
func (tr isTraverser) isTraverserSigil() isTraverser {
|
||||
return isTraverser{}
|
||||
}
|
||||
|
||||
// TraverseRoot looks up a root name in a scope. It is used as the first step
|
||||
// of an absolute Traversal, and cannot itself be traversed directly.
|
||||
type TraverseRoot struct {
|
||||
isTraverser
|
||||
Name string
|
||||
SrcRange Range
|
||||
}
|
||||
|
||||
// TraversalStep on a TraverseName immediately panics, because absolute
|
||||
// traversals cannot be directly traversed.
|
||||
func (tn TraverseRoot) TraversalStep(cty.Value) (cty.Value, Diagnostics) {
|
||||
panic("Cannot traverse an absolute traversal")
|
||||
}
|
||||
|
||||
func (tn TraverseRoot) SourceRange() Range {
|
||||
return tn.SrcRange
|
||||
}
|
||||
|
||||
// TraverseAttr looks up an attribute in its initial value.
|
||||
type TraverseAttr struct {
|
||||
isTraverser
|
||||
Name string
|
||||
SrcRange Range
|
||||
}
|
||||
|
||||
func (tn TraverseAttr) TraversalStep(val cty.Value) (cty.Value, Diagnostics) {
|
||||
return GetAttr(val, tn.Name, &tn.SrcRange)
|
||||
}
|
||||
|
||||
func (tn TraverseAttr) SourceRange() Range {
|
||||
return tn.SrcRange
|
||||
}
|
||||
|
||||
// TraverseIndex applies the index operation to its initial value.
|
||||
type TraverseIndex struct {
|
||||
isTraverser
|
||||
Key cty.Value
|
||||
SrcRange Range
|
||||
}
|
||||
|
||||
func (tn TraverseIndex) TraversalStep(val cty.Value) (cty.Value, Diagnostics) {
|
||||
return Index(val, tn.Key, &tn.SrcRange)
|
||||
}
|
||||
|
||||
func (tn TraverseIndex) SourceRange() Range {
|
||||
return tn.SrcRange
|
||||
}
|
||||
|
||||
// TraverseSplat applies the splat operation to its initial value.
|
||||
type TraverseSplat struct {
|
||||
isTraverser
|
||||
Each Traversal
|
||||
SrcRange Range
|
||||
}
|
||||
|
||||
func (tn TraverseSplat) TraversalStep(val cty.Value) (cty.Value, Diagnostics) {
|
||||
panic("TraverseSplat not yet implemented")
|
||||
}
|
||||
|
||||
func (tn TraverseSplat) SourceRange() Range {
|
||||
return tn.SrcRange
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
package hcl
|
||||
|
||||
// AbsTraversalForExpr attempts to interpret the given expression as
|
||||
// an absolute traversal, or returns error diagnostic(s) if that is
|
||||
// not possible for the given expression.
|
||||
//
|
||||
// A particular Expression implementation can support this function by
|
||||
// offering a method called AsTraversal that takes no arguments and
|
||||
// returns either a valid absolute traversal or nil to indicate that
|
||||
// no traversal is possible. Alternatively, an implementation can support
|
||||
// UnwrapExpression to delegate handling of this function to a wrapped
|
||||
// Expression object.
|
||||
//
|
||||
// In most cases the calling application is interested in the value
|
||||
// that results from an expression, but in rarer cases the application
|
||||
// needs to see the the name of the variable and subsequent
|
||||
// attributes/indexes itself, for example to allow users to give references
|
||||
// to the variables themselves rather than to their values. An implementer
|
||||
// of this function should at least support attribute and index steps.
|
||||
func AbsTraversalForExpr(expr Expression) (Traversal, Diagnostics) {
|
||||
type asTraversal interface {
|
||||
AsTraversal() Traversal
|
||||
}
|
||||
|
||||
physExpr := UnwrapExpressionUntil(expr, func(expr Expression) bool {
|
||||
_, supported := expr.(asTraversal)
|
||||
return supported
|
||||
})
|
||||
|
||||
if asT, supported := physExpr.(asTraversal); supported {
|
||||
if traversal := asT.AsTraversal(); traversal != nil {
|
||||
return traversal, nil
|
||||
}
|
||||
}
|
||||
return nil, Diagnostics{
|
||||
&Diagnostic{
|
||||
Severity: DiagError,
|
||||
Summary: "Invalid expression",
|
||||
Detail: "A single static variable reference is required: only attribute access and indexing with constant keys. No calculations, function calls, template expressions, etc are allowed here.",
|
||||
Subject: expr.Range().Ptr(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RelTraversalForExpr is similar to AbsTraversalForExpr but it returns
|
||||
// a relative traversal instead. Due to the nature of HCL expressions, the
|
||||
// first element of the returned traversal is always a TraverseAttr, and
|
||||
// then it will be followed by zero or more other expressions.
|
||||
//
|
||||
// Any expression accepted by AbsTraversalForExpr is also accepted by
|
||||
// RelTraversalForExpr.
|
||||
func RelTraversalForExpr(expr Expression) (Traversal, Diagnostics) {
|
||||
traversal, diags := AbsTraversalForExpr(expr)
|
||||
if len(traversal) > 0 {
|
||||
ret := make(Traversal, len(traversal))
|
||||
copy(ret, traversal)
|
||||
root := traversal[0].(TraverseRoot)
|
||||
ret[0] = TraverseAttr{
|
||||
Name: root.Name,
|
||||
SrcRange: root.SrcRange,
|
||||
}
|
||||
return ret, diags
|
||||
}
|
||||
return traversal, diags
|
||||
}
|
||||
|
||||
// ExprAsKeyword attempts to interpret the given expression as a static keyword,
|
||||
// returning the keyword string if possible, and the empty string if not.
|
||||
//
|
||||
// A static keyword, for the sake of this function, is a single identifier.
|
||||
// For example, the following attribute has an expression that would produce
|
||||
// the keyword "foo":
|
||||
//
|
||||
// example = foo
|
||||
//
|
||||
// This function is a variant of AbsTraversalForExpr, which uses the same
|
||||
// interface on the given expression. This helper constrains the result
|
||||
// further by requiring only a single root identifier.
|
||||
//
|
||||
// This function is intended to be used with the following idiom, to recognize
|
||||
// situations where one of a fixed set of keywords is required and arbitrary
|
||||
// expressions are not allowed:
|
||||
//
|
||||
// switch hcl.ExprAsKeyword(expr) {
|
||||
// case "allow":
|
||||
// // (take suitable action for keyword "allow")
|
||||
// case "deny":
|
||||
// // (take suitable action for keyword "deny")
|
||||
// default:
|
||||
// diags = append(diags, &hcl.Diagnostic{
|
||||
// // ... "invalid keyword" diagnostic message ...
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// The above approach will generate the same message for both the use of an
|
||||
// unrecognized keyword and for not using a keyword at all, which is usually
|
||||
// reasonable if the message specifies that the given value must be a keyword
|
||||
// from that fixed list.
|
||||
//
|
||||
// Note that in the native syntax the keywords "true", "false", and "null" are
|
||||
// recognized as literal values during parsing and so these reserved words
|
||||
// cannot not be accepted as keywords by this function.
|
||||
//
|
||||
// Since interpreting an expression as a keyword bypasses usual expression
|
||||
// evaluation, it should be used sparingly for situations where e.g. one of
|
||||
// a fixed set of keywords is used in a structural way in a special attribute
|
||||
// to affect the further processing of a block.
|
||||
func ExprAsKeyword(expr Expression) string {
|
||||
type asTraversal interface {
|
||||
AsTraversal() Traversal
|
||||
}
|
||||
|
||||
physExpr := UnwrapExpressionUntil(expr, func(expr Expression) bool {
|
||||
_, supported := expr.(asTraversal)
|
||||
return supported
|
||||
})
|
||||
|
||||
if asT, supported := physExpr.(asTraversal); supported {
|
||||
if traversal := asT.AsTraversal(); len(traversal) == 1 {
|
||||
return traversal.RootName()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Mitchell Hashimoto
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
@ -0,0 +1,39 @@
|
||||
# go-wordwrap
|
||||
|
||||
`go-wordwrap` (Golang package: `wordwrap`) is a package for Go that
|
||||
automatically wraps words into multiple lines. The primary use case for this
|
||||
is in formatting CLI output, but of course word wrapping is a generally useful
|
||||
thing to do.
|
||||
|
||||
## Installation and Usage
|
||||
|
||||
Install using `go get github.com/mitchellh/go-wordwrap`.
|
||||
|
||||
Full documentation is available at
|
||||
http://godoc.org/github.com/mitchellh/go-wordwrap
|
||||
|
||||
Below is an example of its usage ignoring errors:
|
||||
|
||||
```go
|
||||
wrapped := wordwrap.WrapString("foo bar baz", 3)
|
||||
fmt.Println(wrapped)
|
||||
```
|
||||
|
||||
Would output:
|
||||
|
||||
```
|
||||
foo
|
||||
bar
|
||||
baz
|
||||
```
|
||||
|
||||
## Word Wrap Algorithm
|
||||
|
||||
This library doesn't use any clever algorithm for word wrapping. The wrapping
|
||||
is actually very naive: whenever there is whitespace or an explicit linebreak.
|
||||
The goal of this library is for word wrapping CLI output, so the input is
|
||||
typically pretty well controlled human language. Because of this, the naive
|
||||
approach typically works just fine.
|
||||
|
||||
In the future, we'd like to make the algorithm more advanced. We would do
|
||||
so without breaking the API.
|
@ -0,0 +1,73 @@
|
||||
package wordwrap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// WrapString wraps the given string within lim width in characters.
|
||||
//
|
||||
// Wrapping is currently naive and only happens at white-space. A future
|
||||
// version of the library will implement smarter wrapping. This means that
|
||||
// pathological cases can dramatically reach past the limit, such as a very
|
||||
// long word.
|
||||
func WrapString(s string, lim uint) string {
|
||||
// Initialize a buffer with a slightly larger size to account for breaks
|
||||
init := make([]byte, 0, len(s))
|
||||
buf := bytes.NewBuffer(init)
|
||||
|
||||
var current uint
|
||||
var wordBuf, spaceBuf bytes.Buffer
|
||||
|
||||
for _, char := range s {
|
||||
if char == '\n' {
|
||||
if wordBuf.Len() == 0 {
|
||||
if current+uint(spaceBuf.Len()) > lim {
|
||||
current = 0
|
||||
} else {
|
||||
current += uint(spaceBuf.Len())
|
||||
spaceBuf.WriteTo(buf)
|
||||
}
|
||||
spaceBuf.Reset()
|
||||
} else {
|
||||
current += uint(spaceBuf.Len() + wordBuf.Len())
|
||||
spaceBuf.WriteTo(buf)
|
||||
spaceBuf.Reset()
|
||||
wordBuf.WriteTo(buf)
|
||||
wordBuf.Reset()
|
||||
}
|
||||
buf.WriteRune(char)
|
||||
current = 0
|
||||
} else if unicode.IsSpace(char) {
|
||||
if spaceBuf.Len() == 0 || wordBuf.Len() > 0 {
|
||||
current += uint(spaceBuf.Len() + wordBuf.Len())
|
||||
spaceBuf.WriteTo(buf)
|
||||
spaceBuf.Reset()
|
||||
wordBuf.WriteTo(buf)
|
||||
wordBuf.Reset()
|
||||
}
|
||||
|
||||
spaceBuf.WriteRune(char)
|
||||
} else {
|
||||
|
||||
wordBuf.WriteRune(char)
|
||||
|
||||
if current+uint(spaceBuf.Len()+wordBuf.Len()) > lim && uint(wordBuf.Len()) < lim {
|
||||
buf.WriteRune('\n')
|
||||
current = 0
|
||||
spaceBuf.Reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if wordBuf.Len() == 0 {
|
||||
if current+uint(spaceBuf.Len()) <= lim {
|
||||
spaceBuf.WriteTo(buf)
|
||||
}
|
||||
} else {
|
||||
spaceBuf.WriteTo(buf)
|
||||
wordBuf.WriteTo(buf)
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-2018 Martin Atkins
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,128 @@
|
||||
package cty
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type capsuleType struct {
|
||||
typeImplSigil
|
||||
Name string
|
||||
GoType reflect.Type
|
||||
Ops *CapsuleOps
|
||||
}
|
||||
|
||||
func (t *capsuleType) Equals(other Type) bool {
|
||||
if otherP, ok := other.typeImpl.(*capsuleType); ok {
|
||||
// capsule types compare by pointer identity
|
||||
return otherP == t
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *capsuleType) FriendlyName(mode friendlyTypeNameMode) string {
|
||||
return t.Name
|
||||
}
|
||||
|
||||
func (t *capsuleType) GoString() string {
|
||||
impl := t.Ops.TypeGoString
|
||||
if impl == nil {
|
||||
// To get a useful representation of our native type requires some
|
||||
// shenanigans.
|
||||
victimVal := reflect.Zero(t.GoType)
|
||||
if t.Ops == noCapsuleOps {
|
||||
return fmt.Sprintf("cty.Capsule(%q, reflect.TypeOf(%#v))", t.Name, victimVal.Interface())
|
||||
} else {
|
||||
// Including the operations in the output will make this _very_ long,
|
||||
// so in practice any capsule type with ops ought to provide a
|
||||
// TypeGoString function to override this with something more
|
||||
// reasonable.
|
||||
return fmt.Sprintf("cty.CapsuleWithOps(%q, reflect.TypeOf(%#v), %#v)", t.Name, victimVal.Interface(), t.Ops)
|
||||
}
|
||||
}
|
||||
return impl(t.GoType)
|
||||
}
|
||||
|
||||
// Capsule creates a new Capsule type.
|
||||
//
|
||||
// A Capsule type is a special type that can be used to transport arbitrary
|
||||
// Go native values of a given type through the cty type system. A language
|
||||
// that uses cty as its type system might, for example, provide functions
|
||||
// that return capsule-typed values and then other functions that operate
|
||||
// on those values.
|
||||
//
|
||||
// From cty's perspective, Capsule types have a few interesting characteristics,
|
||||
// described in the following paragraphs.
|
||||
//
|
||||
// Each capsule type has an associated Go native type that it is able to
|
||||
// transport. Capsule types compare by identity, so each call to the
|
||||
// Capsule function creates an entirely-distinct cty Type, even if two calls
|
||||
// use the same native type.
|
||||
//
|
||||
// Each capsule-typed value contains a pointer to a value of the given native
|
||||
// type. A capsule-typed value by default supports no operations except
|
||||
// equality, and equality is implemented by pointer identity of the
|
||||
// encapsulated pointer. A capsule type can optionally have its own
|
||||
// implementations of certain operations if it is created with CapsuleWithOps
|
||||
// instead of Capsule.
|
||||
//
|
||||
// The given name is used as the new type's "friendly name". This can be any
|
||||
// string in principle, but will usually be a short, all-lowercase name aimed
|
||||
// at users of the embedding language (i.e. not mention Go-specific details)
|
||||
// and will ideally not create ambiguity with any predefined cty type.
|
||||
//
|
||||
// Capsule types are never introduced by any standard cty operation, so a
|
||||
// calling application opts in to including them within its own type system
|
||||
// by creating them and introducing them via its own functions. At that point,
|
||||
// the application is responsible for dealing with any capsule-typed values
|
||||
// that might be returned.
|
||||
func Capsule(name string, nativeType reflect.Type) Type {
|
||||
return Type{
|
||||
&capsuleType{
|
||||
Name: name,
|
||||
GoType: nativeType,
|
||||
Ops: noCapsuleOps,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CapsuleWithOps is like Capsule except the caller may provide an object
|
||||
// representing some overloaded operation implementations to associate with
|
||||
// the given capsule type.
|
||||
//
|
||||
// All of the other caveats and restrictions for capsule types still apply, but
|
||||
// overloaded operations can potentially help a capsule type participate better
|
||||
// in cty operations.
|
||||
func CapsuleWithOps(name string, nativeType reflect.Type, ops *CapsuleOps) Type {
|
||||
// Copy the operations to make sure the caller can't modify them after
|
||||
// we're constructed.
|
||||
ourOps := *ops
|
||||
ourOps.assertValid()
|
||||
|
||||
return Type{
|
||||
&capsuleType{
|
||||
Name: name,
|
||||
GoType: nativeType,
|
||||
Ops: &ourOps,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// IsCapsuleType returns true if this type is a capsule type, as created
|
||||
// by cty.Capsule .
|
||||
func (t Type) IsCapsuleType() bool {
|
||||
_, ok := t.typeImpl.(*capsuleType)
|
||||
return ok
|
||||
}
|
||||
|
||||
// EncapsulatedType returns the encapsulated native type of a capsule type,
|
||||
// or panics if the receiver is not a Capsule type.
|
||||
//
|
||||
// Is IsCapsuleType to determine if this method is safe to call.
|
||||
func (t Type) EncapsulatedType() reflect.Type {
|
||||
impl, ok := t.typeImpl.(*capsuleType)
|
||||
if !ok {
|
||||
panic("not a capsule type")
|
||||
}
|
||||
return impl.GoType
|
||||
}
|
@ -0,0 +1,132 @@
|
||||
package cty
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// CapsuleOps represents a set of overloaded operations for a capsule type.
|
||||
//
|
||||
// Each field is a reference to a function that can either be nil or can be
|
||||
// set to an implementation of the corresponding operation. If an operation
|
||||
// function is nil then it isn't supported for the given capsule type.
|
||||
type CapsuleOps struct {
|
||||
// GoString provides the GoString implementation for values of the
|
||||
// corresponding type. Conventionally this should return a string
|
||||
// representation of an expression that would produce an equivalent
|
||||
// value.
|
||||
GoString func(val interface{}) string
|
||||
|
||||
// TypeGoString provides the GoString implementation for the corresponding
|
||||
// capsule type itself.
|
||||
TypeGoString func(goTy reflect.Type) string
|
||||
|
||||
// Equals provides the implementation of the Equals operation. This is
|
||||
// called only with known, non-null values of the corresponding type,
|
||||
// but if the corresponding type is a compound type then it must be
|
||||
// ready to detect and handle nested unknown or null values, usually
|
||||
// by recursively calling Value.Equals on those nested values.
|
||||
//
|
||||
// The result value must always be of type cty.Bool, or the Equals
|
||||
// operation will panic.
|
||||
//
|
||||
// If RawEquals is set without also setting Equals, the RawEquals
|
||||
// implementation will be used as a fallback implementation. That fallback
|
||||
// is appropriate only for leaf types that do not contain any nested
|
||||
// cty.Value that would need to distinguish Equals vs. RawEquals for their
|
||||
// own equality.
|
||||
//
|
||||
// If RawEquals is nil then Equals must also be nil, selecting the default
|
||||
// pointer-identity comparison instead.
|
||||
Equals func(a, b interface{}) Value
|
||||
|
||||
// RawEquals provides the implementation of the RawEquals operation.
|
||||
// This is called only with known, non-null values of the corresponding
|
||||
// type, but if the corresponding type is a compound type then it must be
|
||||
// ready to detect and handle nested unknown or null values, usually
|
||||
// by recursively calling Value.RawEquals on those nested values.
|
||||
//
|
||||
// If RawEquals is nil, values of the corresponding type are compared by
|
||||
// pointer identity of the encapsulated value.
|
||||
RawEquals func(a, b interface{}) bool
|
||||
|
||||
// ConversionFrom can provide conversions from the corresponding type to
|
||||
// some other type when values of the corresponding type are used with
|
||||
// the "convert" package. (The main cty package does not use this operation.)
|
||||
//
|
||||
// This function itself returns a function, allowing it to switch its
|
||||
// behavior depending on the given source type. Return nil to indicate
|
||||
// that no such conversion is available.
|
||||
ConversionFrom func(src Type) func(interface{}, Path) (Value, error)
|
||||
|
||||
// ConversionTo can provide conversions to the corresponding type from
|
||||
// some other type when values of the corresponding type are used with
|
||||
// the "convert" package. (The main cty package does not use this operation.)
|
||||
//
|
||||
// This function itself returns a function, allowing it to switch its
|
||||
// behavior depending on the given destination type. Return nil to indicate
|
||||
// that no such conversion is available.
|
||||
ConversionTo func(dst Type) func(Value, Path) (interface{}, error)
|
||||
|
||||
// ExtensionData is an extension point for applications that wish to
|
||||
// create their own extension features using capsule types.
|
||||
//
|
||||
// The key argument is any value that can be compared with Go's ==
|
||||
// operator, but should be of a named type in a package belonging to the
|
||||
// application defining the key. An ExtensionData implementation must
|
||||
// check to see if the given key is familar to it, and if so return a
|
||||
// suitable value for the key.
|
||||
//
|
||||
// If the given key is unrecognized, the ExtensionData function must
|
||||
// return a nil interface. (Importantly, not an interface containing a nil
|
||||
// pointer of some other type.)
|
||||
// The common implementation of ExtensionData is a single switch statement
|
||||
// over "key" which has a default case returning nil.
|
||||
//
|
||||
// The meaning of any given key is entirely up to the application that
|
||||
// defines it. Applications consuming ExtensionData from capsule types
|
||||
// should do so defensively: if the result of ExtensionData is not valid,
|
||||
// prefer to ignore it or gracefully produce an error rather than causing
|
||||
// a panic.
|
||||
ExtensionData func(key interface{}) interface{}
|
||||
}
|
||||
|
||||
// noCapsuleOps is a pointer to a CapsuleOps with no functions set, which
|
||||
// is used as the default operations value when a type is created using
|
||||
// the Capsule function.
|
||||
var noCapsuleOps = &CapsuleOps{}
|
||||
|
||||
func (ops *CapsuleOps) assertValid() {
|
||||
if ops.RawEquals == nil && ops.Equals != nil {
|
||||
panic("Equals cannot be set without RawEquals")
|
||||
}
|
||||
}
|
||||
|
||||
// CapsuleOps returns a pointer to the CapsuleOps value for a capsule type,
|
||||
// or panics if the receiver is not a capsule type.
|
||||
//
|
||||
// The caller must not modify the CapsuleOps.
|
||||
func (ty Type) CapsuleOps() *CapsuleOps {
|
||||
if !ty.IsCapsuleType() {
|
||||
panic("not a capsule-typed value")
|
||||
}
|
||||
|
||||
return ty.typeImpl.(*capsuleType).Ops
|
||||
}
|
||||
|
||||
// CapsuleExtensionData is a convenience interface to the ExtensionData
|
||||
// function that can be optionally implemented for a capsule type. It will
|
||||
// check to see if the underlying type implements ExtensionData and call it
|
||||
// if so. If not, it will return nil to indicate that the given key is not
|
||||
// supported.
|
||||
//
|
||||
// See the documentation for CapsuleOps.ExtensionData for more information
|
||||
// on the purpose of and usage of this mechanism.
|
||||
//
|
||||
// If CapsuleExtensionData is called on a non-capsule type then it will panic.
|
||||
func (ty Type) CapsuleExtensionData(key interface{}) interface{} {
|
||||
ops := ty.CapsuleOps()
|
||||
if ops.ExtensionData == nil {
|
||||
return nil
|
||||
}
|
||||
return ops.ExtensionData(key)
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package cty
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
type collectionTypeImpl interface {
|
||||
ElementType() Type
|
||||
}
|
||||
|
||||
// IsCollectionType returns true if the given type supports the operations
|
||||
// that are defined for all collection types.
|
||||
func (t Type) IsCollectionType() bool {
|
||||
_, ok := t.typeImpl.(collectionTypeImpl)
|
||||
return ok
|
||||
}
|
||||
|
||||
// ElementType returns the element type of the receiver if it is a collection
|
||||
// type, or panics if it is not. Use IsCollectionType first to test whether
|
||||
// this method will succeed.
|
||||
func (t Type) ElementType() Type {
|
||||
if ct, ok := t.typeImpl.(collectionTypeImpl); ok {
|
||||
return ct.ElementType()
|
||||
}
|
||||
panic(errors.New("not a collection type"))
|
||||
}
|
||||
|
||||
// ElementCallback is a callback type used for iterating over elements of
|
||||
// collections and attributes of objects.
|
||||
//
|
||||
// The types of key and value depend on what type is being iterated over.
|
||||
// Return true to stop iterating after the current element, or false to
|
||||
// continue iterating.
|
||||
type ElementCallback func(key Value, val Value) (stop bool)
|
@ -0,0 +1,165 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// compareTypes implements a preference order for unification.
|
||||
//
|
||||
// The result of this method is not useful for anything other than unification
|
||||
// preferences, since it assumes that the caller will verify that any suggested
|
||||
// conversion is actually possible and it is thus able to to make certain
|
||||
// optimistic assumptions.
|
||||
func compareTypes(a cty.Type, b cty.Type) int {
|
||||
|
||||
// DynamicPseudoType always has lowest preference, because anything can
|
||||
// convert to it (it acts as a placeholder for "any type") and we want
|
||||
// to optimistically assume that any dynamics will converge on matching
|
||||
// their neighbors.
|
||||
if a == cty.DynamicPseudoType || b == cty.DynamicPseudoType {
|
||||
if a != cty.DynamicPseudoType {
|
||||
return -1
|
||||
}
|
||||
if b != cty.DynamicPseudoType {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
if a.IsPrimitiveType() && b.IsPrimitiveType() {
|
||||
// String is a supertype of all primitive types, because we can
|
||||
// represent all primitive values as specially-formatted strings.
|
||||
if a == cty.String || b == cty.String {
|
||||
if a != cty.String {
|
||||
return 1
|
||||
}
|
||||
if b != cty.String {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
if a.IsListType() && b.IsListType() {
|
||||
return compareTypes(a.ElementType(), b.ElementType())
|
||||
}
|
||||
if a.IsSetType() && b.IsSetType() {
|
||||
return compareTypes(a.ElementType(), b.ElementType())
|
||||
}
|
||||
if a.IsMapType() && b.IsMapType() {
|
||||
return compareTypes(a.ElementType(), b.ElementType())
|
||||
}
|
||||
|
||||
// From this point on we may have swapped the two items in order to
|
||||
// simplify our cases. Therefore any non-zero return after this point
|
||||
// must be multiplied by "swap" to potentially invert the return value
|
||||
// if needed.
|
||||
swap := 1
|
||||
switch {
|
||||
case a.IsTupleType() && b.IsListType():
|
||||
fallthrough
|
||||
case a.IsObjectType() && b.IsMapType():
|
||||
fallthrough
|
||||
case a.IsSetType() && b.IsTupleType():
|
||||
fallthrough
|
||||
case a.IsSetType() && b.IsListType():
|
||||
a, b = b, a
|
||||
swap = -1
|
||||
}
|
||||
|
||||
if b.IsSetType() && (a.IsTupleType() || a.IsListType()) {
|
||||
// We'll just optimistically assume that the element types are
|
||||
// unifyable/convertible, and let a second recursive pass
|
||||
// figure out how to make that so.
|
||||
return -1 * swap
|
||||
}
|
||||
|
||||
if a.IsListType() && b.IsTupleType() {
|
||||
// We'll just optimistically assume that the tuple's element types
|
||||
// can be unified into something compatible with the list's element
|
||||
// type.
|
||||
return -1 * swap
|
||||
}
|
||||
|
||||
if a.IsMapType() && b.IsObjectType() {
|
||||
// We'll just optimistically assume that the object's attribute types
|
||||
// can be unified into something compatible with the map's element
|
||||
// type.
|
||||
return -1 * swap
|
||||
}
|
||||
|
||||
// For object and tuple types, comparing two types doesn't really tell
|
||||
// the whole story because it may be possible to construct a new type C
|
||||
// that is the supertype of both A and B by unifying each attribute/element
|
||||
// separately. That possibility is handled by Unify as a follow-up if
|
||||
// type sorting is insufficient to produce a valid result.
|
||||
//
|
||||
// Here we will take care of the simple possibilities where no new type
|
||||
// is needed.
|
||||
if a.IsObjectType() && b.IsObjectType() {
|
||||
atysA := a.AttributeTypes()
|
||||
atysB := b.AttributeTypes()
|
||||
|
||||
if len(atysA) != len(atysB) {
|
||||
return 0
|
||||
}
|
||||
|
||||
hasASuper := false
|
||||
hasBSuper := false
|
||||
for k := range atysA {
|
||||
if _, has := atysB[k]; !has {
|
||||
return 0
|
||||
}
|
||||
|
||||
cmp := compareTypes(atysA[k], atysB[k])
|
||||
if cmp < 0 {
|
||||
hasASuper = true
|
||||
} else if cmp > 0 {
|
||||
hasBSuper = true
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case hasASuper && hasBSuper:
|
||||
return 0
|
||||
case hasASuper:
|
||||
return -1 * swap
|
||||
case hasBSuper:
|
||||
return 1 * swap
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
if a.IsTupleType() && b.IsTupleType() {
|
||||
etysA := a.TupleElementTypes()
|
||||
etysB := b.TupleElementTypes()
|
||||
|
||||
if len(etysA) != len(etysB) {
|
||||
return 0
|
||||
}
|
||||
|
||||
hasASuper := false
|
||||
hasBSuper := false
|
||||
for i := range etysA {
|
||||
cmp := compareTypes(etysA[i], etysB[i])
|
||||
if cmp < 0 {
|
||||
hasASuper = true
|
||||
} else if cmp > 0 {
|
||||
hasBSuper = true
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case hasASuper && hasBSuper:
|
||||
return 0
|
||||
case hasASuper:
|
||||
return -1 * swap
|
||||
case hasBSuper:
|
||||
return 1 * swap
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
@ -0,0 +1,181 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// conversion is an internal variant of Conversion that carries around
|
||||
// a cty.Path to be used in error responses.
|
||||
type conversion func(cty.Value, cty.Path) (cty.Value, error)
|
||||
|
||||
func getConversion(in cty.Type, out cty.Type, unsafe bool) conversion {
|
||||
conv := getConversionKnown(in, out, unsafe)
|
||||
if conv == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wrap the conversion in some standard checks that we don't want to
|
||||
// have to repeat in every conversion function.
|
||||
var ret conversion
|
||||
ret = func(in cty.Value, path cty.Path) (cty.Value, error) {
|
||||
if in.IsMarked() {
|
||||
// We must unmark during the conversion and then re-apply the
|
||||
// same marks to the result.
|
||||
in, inMarks := in.Unmark()
|
||||
v, err := ret(in, path)
|
||||
if v != cty.NilVal {
|
||||
v = v.WithMarks(inMarks)
|
||||
}
|
||||
return v, err
|
||||
}
|
||||
|
||||
if out == cty.DynamicPseudoType {
|
||||
// Conversion to DynamicPseudoType always just passes through verbatim.
|
||||
return in, nil
|
||||
}
|
||||
if !in.IsKnown() {
|
||||
return cty.UnknownVal(out), nil
|
||||
}
|
||||
if in.IsNull() {
|
||||
// We'll pass through nulls, albeit type converted, and let
|
||||
// the caller deal with whatever handling they want to do in
|
||||
// case null values are considered valid in some applications.
|
||||
return cty.NullVal(out), nil
|
||||
}
|
||||
|
||||
return conv(in, path)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func getConversionKnown(in cty.Type, out cty.Type, unsafe bool) conversion {
|
||||
switch {
|
||||
|
||||
case out == cty.DynamicPseudoType:
|
||||
// Conversion *to* DynamicPseudoType means that the caller wishes
|
||||
// to allow any type in this position, so we'll produce a do-nothing
|
||||
// conversion that just passes through the value as-is.
|
||||
return dynamicPassthrough
|
||||
|
||||
case unsafe && in == cty.DynamicPseudoType:
|
||||
// Conversion *from* DynamicPseudoType means that we have a value
|
||||
// whose type isn't yet known during type checking. For these we will
|
||||
// assume that conversion will succeed and deal with any errors that
|
||||
// result (which is why we can only do this when "unsafe" is set).
|
||||
return dynamicFixup(out)
|
||||
|
||||
case in.IsPrimitiveType() && out.IsPrimitiveType():
|
||||
conv := primitiveConversionsSafe[in][out]
|
||||
if conv != nil {
|
||||
return conv
|
||||
}
|
||||
if unsafe {
|
||||
return primitiveConversionsUnsafe[in][out]
|
||||
}
|
||||
return nil
|
||||
|
||||
case out.IsObjectType() && in.IsObjectType():
|
||||
return conversionObjectToObject(in, out, unsafe)
|
||||
|
||||
case out.IsTupleType() && in.IsTupleType():
|
||||
return conversionTupleToTuple(in, out, unsafe)
|
||||
|
||||
case out.IsListType() && (in.IsListType() || in.IsSetType()):
|
||||
inEty := in.ElementType()
|
||||
outEty := out.ElementType()
|
||||
if inEty.Equals(outEty) {
|
||||
// This indicates that we're converting from list to set with
|
||||
// the same element type, so we don't need an element converter.
|
||||
return conversionCollectionToList(outEty, nil)
|
||||
}
|
||||
|
||||
convEty := getConversion(inEty, outEty, unsafe)
|
||||
if convEty == nil {
|
||||
return nil
|
||||
}
|
||||
return conversionCollectionToList(outEty, convEty)
|
||||
|
||||
case out.IsSetType() && (in.IsListType() || in.IsSetType()):
|
||||
if in.IsListType() && !unsafe {
|
||||
// Conversion from list to map is unsafe because it will lose
|
||||
// information: the ordering will not be preserved, and any
|
||||
// duplicate elements will be conflated.
|
||||
return nil
|
||||
}
|
||||
inEty := in.ElementType()
|
||||
outEty := out.ElementType()
|
||||
convEty := getConversion(inEty, outEty, unsafe)
|
||||
if inEty.Equals(outEty) {
|
||||
// This indicates that we're converting from set to list with
|
||||
// the same element type, so we don't need an element converter.
|
||||
return conversionCollectionToSet(outEty, nil)
|
||||
}
|
||||
|
||||
if convEty == nil {
|
||||
return nil
|
||||
}
|
||||
return conversionCollectionToSet(outEty, convEty)
|
||||
|
||||
case out.IsMapType() && in.IsMapType():
|
||||
inEty := in.ElementType()
|
||||
outEty := out.ElementType()
|
||||
convEty := getConversion(inEty, outEty, unsafe)
|
||||
if convEty == nil {
|
||||
return nil
|
||||
}
|
||||
return conversionCollectionToMap(outEty, convEty)
|
||||
|
||||
case out.IsListType() && in.IsTupleType():
|
||||
outEty := out.ElementType()
|
||||
return conversionTupleToList(in, outEty, unsafe)
|
||||
|
||||
case out.IsSetType() && in.IsTupleType():
|
||||
outEty := out.ElementType()
|
||||
return conversionTupleToSet(in, outEty, unsafe)
|
||||
|
||||
case out.IsMapType() && in.IsObjectType():
|
||||
outEty := out.ElementType()
|
||||
return conversionObjectToMap(in, outEty, unsafe)
|
||||
|
||||
case in.IsCapsuleType() || out.IsCapsuleType():
|
||||
if !unsafe {
|
||||
// Capsule types can only participate in "unsafe" conversions,
|
||||
// because we don't know enough about their conversion behaviors
|
||||
// to be sure that they will always be safe.
|
||||
return nil
|
||||
}
|
||||
if in.Equals(out) {
|
||||
// conversion to self is never allowed
|
||||
return nil
|
||||
}
|
||||
if out.IsCapsuleType() {
|
||||
if fn := out.CapsuleOps().ConversionTo; fn != nil {
|
||||
return conversionToCapsule(in, out, fn)
|
||||
}
|
||||
}
|
||||
if in.IsCapsuleType() {
|
||||
if fn := in.CapsuleOps().ConversionFrom; fn != nil {
|
||||
return conversionFromCapsule(in, out, fn)
|
||||
}
|
||||
}
|
||||
// No conversion operation is available, then.
|
||||
return nil
|
||||
|
||||
default:
|
||||
return nil
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// retConversion wraps a conversion (internal type) so it can be returned
|
||||
// as a Conversion (public type).
|
||||
func retConversion(conv conversion) Conversion {
|
||||
if conv == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return func(in cty.Value) (cty.Value, error) {
|
||||
return conv(in, cty.Path(nil))
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func conversionToCapsule(inTy, outTy cty.Type, fn func(inTy cty.Type) func(cty.Value, cty.Path) (interface{}, error)) conversion {
|
||||
rawConv := fn(inTy)
|
||||
if rawConv == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return func(in cty.Value, path cty.Path) (cty.Value, error) {
|
||||
rawV, err := rawConv(in, path)
|
||||
if err != nil {
|
||||
return cty.NilVal, err
|
||||
}
|
||||
return cty.CapsuleVal(outTy, rawV), nil
|
||||
}
|
||||
}
|
||||
|
||||
func conversionFromCapsule(inTy, outTy cty.Type, fn func(outTy cty.Type) func(interface{}, cty.Path) (cty.Value, error)) conversion {
|
||||
rawConv := fn(outTy)
|
||||
if rawConv == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return func(in cty.Value, path cty.Path) (cty.Value, error) {
|
||||
return rawConv(in.EncapsulatedValue(), path)
|
||||
}
|
||||
}
|
@ -0,0 +1,340 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// conversionCollectionToList returns a conversion that will apply the given
|
||||
// conversion to all of the elements of a collection (something that supports
|
||||
// ForEachElement and LengthInt) and then returns the result as a list.
|
||||
//
|
||||
// "conv" can be nil if the elements are expected to already be of the
|
||||
// correct type and just need to be re-wrapped into a list. (For example,
|
||||
// if we're converting from a set into a list of the same element type.)
|
||||
func conversionCollectionToList(ety cty.Type, conv conversion) conversion {
|
||||
return func(val cty.Value, path cty.Path) (cty.Value, error) {
|
||||
elems := make([]cty.Value, 0, val.LengthInt())
|
||||
i := int64(0)
|
||||
path = append(path, nil)
|
||||
it := val.ElementIterator()
|
||||
for it.Next() {
|
||||
_, val := it.Element()
|
||||
var err error
|
||||
|
||||
path[len(path)-1] = cty.IndexStep{
|
||||
Key: cty.NumberIntVal(i),
|
||||
}
|
||||
|
||||
if conv != nil {
|
||||
val, err = conv(val, path)
|
||||
if err != nil {
|
||||
return cty.NilVal, err
|
||||
}
|
||||
}
|
||||
elems = append(elems, val)
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
if len(elems) == 0 {
|
||||
return cty.ListValEmpty(ety), nil
|
||||
}
|
||||
|
||||
return cty.ListVal(elems), nil
|
||||
}
|
||||
}
|
||||
|
||||
// conversionCollectionToSet returns a conversion that will apply the given
|
||||
// conversion to all of the elements of a collection (something that supports
|
||||
// ForEachElement and LengthInt) and then returns the result as a set.
|
||||
//
|
||||
// "conv" can be nil if the elements are expected to already be of the
|
||||
// correct type and just need to be re-wrapped into a set. (For example,
|
||||
// if we're converting from a list into a set of the same element type.)
|
||||
func conversionCollectionToSet(ety cty.Type, conv conversion) conversion {
|
||||
return func(val cty.Value, path cty.Path) (cty.Value, error) {
|
||||
elems := make([]cty.Value, 0, val.LengthInt())
|
||||
i := int64(0)
|
||||
path = append(path, nil)
|
||||
it := val.ElementIterator()
|
||||
for it.Next() {
|
||||
_, val := it.Element()
|
||||
var err error
|
||||
|
||||
path[len(path)-1] = cty.IndexStep{
|
||||
Key: cty.NumberIntVal(i),
|
||||
}
|
||||
|
||||
if conv != nil {
|
||||
val, err = conv(val, path)
|
||||
if err != nil {
|
||||
return cty.NilVal, err
|
||||
}
|
||||
}
|
||||
elems = append(elems, val)
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
if len(elems) == 0 {
|
||||
return cty.SetValEmpty(ety), nil
|
||||
}
|
||||
|
||||
return cty.SetVal(elems), nil
|
||||
}
|
||||
}
|
||||
|
||||
// conversionCollectionToMap returns a conversion that will apply the given
|
||||
// conversion to all of the elements of a collection (something that supports
|
||||
// ForEachElement and LengthInt) and then returns the result as a map.
|
||||
//
|
||||
// "conv" can be nil if the elements are expected to already be of the
|
||||
// correct type and just need to be re-wrapped into a map.
|
||||
func conversionCollectionToMap(ety cty.Type, conv conversion) conversion {
|
||||
return func(val cty.Value, path cty.Path) (cty.Value, error) {
|
||||
elems := make(map[string]cty.Value, 0)
|
||||
path = append(path, nil)
|
||||
it := val.ElementIterator()
|
||||
for it.Next() {
|
||||
key, val := it.Element()
|
||||
var err error
|
||||
|
||||
path[len(path)-1] = cty.IndexStep{
|
||||
Key: key,
|
||||
}
|
||||
|
||||
keyStr, err := Convert(key, cty.String)
|
||||
if err != nil {
|
||||
// Should never happen, because keys can only be numbers or
|
||||
// strings and both can convert to string.
|
||||
return cty.DynamicVal, path.NewErrorf("cannot convert key type %s to string for map", key.Type().FriendlyName())
|
||||
}
|
||||
|
||||
if conv != nil {
|
||||
val, err = conv(val, path)
|
||||
if err != nil {
|
||||
return cty.NilVal, err
|
||||
}
|
||||
}
|
||||
|
||||
elems[keyStr.AsString()] = val
|
||||
}
|
||||
|
||||
if len(elems) == 0 {
|
||||
return cty.MapValEmpty(ety), nil
|
||||
}
|
||||
|
||||
return cty.MapVal(elems), nil
|
||||
}
|
||||
}
|
||||
|
||||
// conversionTupleToSet returns a conversion that will take a value of the
|
||||
// given tuple type and return a set of the given element type.
|
||||
//
|
||||
// Will panic if the given tupleType isn't actually a tuple type.
|
||||
func conversionTupleToSet(tupleType cty.Type, listEty cty.Type, unsafe bool) conversion {
|
||||
tupleEtys := tupleType.TupleElementTypes()
|
||||
|
||||
if len(tupleEtys) == 0 {
|
||||
// Empty tuple short-circuit
|
||||
return func(val cty.Value, path cty.Path) (cty.Value, error) {
|
||||
return cty.SetValEmpty(listEty), nil
|
||||
}
|
||||
}
|
||||
|
||||
if listEty == cty.DynamicPseudoType {
|
||||
// This is a special case where the caller wants us to find
|
||||
// a suitable single type that all elements can convert to, if
|
||||
// possible.
|
||||
listEty, _ = unify(tupleEtys, unsafe)
|
||||
if listEty == cty.NilType {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
elemConvs := make([]conversion, len(tupleEtys))
|
||||
for i, tupleEty := range tupleEtys {
|
||||
if tupleEty.Equals(listEty) {
|
||||
// no conversion required
|
||||
continue
|
||||
}
|
||||
|
||||
elemConvs[i] = getConversion(tupleEty, listEty, unsafe)
|
||||
if elemConvs[i] == nil {
|
||||
// If any of our element conversions are impossible, then the our
|
||||
// whole conversion is impossible.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// If we fall out here then a conversion is possible, using the
|
||||
// element conversions in elemConvs
|
||||
return func(val cty.Value, path cty.Path) (cty.Value, error) {
|
||||
elems := make([]cty.Value, 0, len(elemConvs))
|
||||
path = append(path, nil)
|
||||
i := int64(0)
|
||||
it := val.ElementIterator()
|
||||
for it.Next() {
|
||||
_, val := it.Element()
|
||||
var err error
|
||||
|
||||
path[len(path)-1] = cty.IndexStep{
|
||||
Key: cty.NumberIntVal(i),
|
||||
}
|
||||
|
||||
conv := elemConvs[i]
|
||||
if conv != nil {
|
||||
val, err = conv(val, path)
|
||||
if err != nil {
|
||||
return cty.NilVal, err
|
||||
}
|
||||
}
|
||||
elems = append(elems, val)
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
return cty.SetVal(elems), nil
|
||||
}
|
||||
}
|
||||
|
||||
// conversionTupleToList returns a conversion that will take a value of the
|
||||
// given tuple type and return a list of the given element type.
|
||||
//
|
||||
// Will panic if the given tupleType isn't actually a tuple type.
|
||||
func conversionTupleToList(tupleType cty.Type, listEty cty.Type, unsafe bool) conversion {
|
||||
tupleEtys := tupleType.TupleElementTypes()
|
||||
|
||||
if len(tupleEtys) == 0 {
|
||||
// Empty tuple short-circuit
|
||||
return func(val cty.Value, path cty.Path) (cty.Value, error) {
|
||||
return cty.ListValEmpty(listEty), nil
|
||||
}
|
||||
}
|
||||
|
||||
if listEty == cty.DynamicPseudoType {
|
||||
// This is a special case where the caller wants us to find
|
||||
// a suitable single type that all elements can convert to, if
|
||||
// possible.
|
||||
listEty, _ = unify(tupleEtys, unsafe)
|
||||
if listEty == cty.NilType {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
elemConvs := make([]conversion, len(tupleEtys))
|
||||
for i, tupleEty := range tupleEtys {
|
||||
if tupleEty.Equals(listEty) {
|
||||
// no conversion required
|
||||
continue
|
||||
}
|
||||
|
||||
elemConvs[i] = getConversion(tupleEty, listEty, unsafe)
|
||||
if elemConvs[i] == nil {
|
||||
// If any of our element conversions are impossible, then the our
|
||||
// whole conversion is impossible.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// If we fall out here then a conversion is possible, using the
|
||||
// element conversions in elemConvs
|
||||
return func(val cty.Value, path cty.Path) (cty.Value, error) {
|
||||
elems := make([]cty.Value, 0, len(elemConvs))
|
||||
path = append(path, nil)
|
||||
i := int64(0)
|
||||
it := val.ElementIterator()
|
||||
for it.Next() {
|
||||
_, val := it.Element()
|
||||
var err error
|
||||
|
||||
path[len(path)-1] = cty.IndexStep{
|
||||
Key: cty.NumberIntVal(i),
|
||||
}
|
||||
|
||||
conv := elemConvs[i]
|
||||
if conv != nil {
|
||||
val, err = conv(val, path)
|
||||
if err != nil {
|
||||
return cty.NilVal, err
|
||||
}
|
||||
}
|
||||
elems = append(elems, val)
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
return cty.ListVal(elems), nil
|
||||
}
|
||||
}
|
||||
|
||||
// conversionObjectToMap returns a conversion that will take a value of the
|
||||
// given object type and return a map of the given element type.
|
||||
//
|
||||
// Will panic if the given objectType isn't actually an object type.
|
||||
func conversionObjectToMap(objectType cty.Type, mapEty cty.Type, unsafe bool) conversion {
|
||||
objectAtys := objectType.AttributeTypes()
|
||||
|
||||
if len(objectAtys) == 0 {
|
||||
// Empty object short-circuit
|
||||
return func(val cty.Value, path cty.Path) (cty.Value, error) {
|
||||
return cty.MapValEmpty(mapEty), nil
|
||||
}
|
||||
}
|
||||
|
||||
if mapEty == cty.DynamicPseudoType {
|
||||
// This is a special case where the caller wants us to find
|
||||
// a suitable single type that all elements can convert to, if
|
||||
// possible.
|
||||
objectAtysList := make([]cty.Type, 0, len(objectAtys))
|
||||
for _, aty := range objectAtys {
|
||||
objectAtysList = append(objectAtysList, aty)
|
||||
}
|
||||
mapEty, _ = unify(objectAtysList, unsafe)
|
||||
if mapEty == cty.NilType {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
elemConvs := make(map[string]conversion, len(objectAtys))
|
||||
for name, objectAty := range objectAtys {
|
||||
if objectAty.Equals(mapEty) {
|
||||
// no conversion required
|
||||
continue
|
||||
}
|
||||
|
||||
elemConvs[name] = getConversion(objectAty, mapEty, unsafe)
|
||||
if elemConvs[name] == nil {
|
||||
// If any of our element conversions are impossible, then the our
|
||||
// whole conversion is impossible.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// If we fall out here then a conversion is possible, using the
|
||||
// element conversions in elemConvs
|
||||
return func(val cty.Value, path cty.Path) (cty.Value, error) {
|
||||
elems := make(map[string]cty.Value, len(elemConvs))
|
||||
path = append(path, nil)
|
||||
it := val.ElementIterator()
|
||||
for it.Next() {
|
||||
name, val := it.Element()
|
||||
var err error
|
||||
|
||||
path[len(path)-1] = cty.IndexStep{
|
||||
Key: name,
|
||||
}
|
||||
|
||||
conv := elemConvs[name.AsString()]
|
||||
if conv != nil {
|
||||
val, err = conv(val, path)
|
||||
if err != nil {
|
||||
return cty.NilVal, err
|
||||
}
|
||||
}
|
||||
elems[name.AsString()] = val
|
||||
}
|
||||
|
||||
return cty.MapVal(elems), nil
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// dynamicFixup deals with just-in-time conversions of values that were
|
||||
// input-typed as cty.DynamicPseudoType during analysis, ensuring that
|
||||
// we end up with the desired output type once the value is known, or
|
||||
// failing with an error if that is not possible.
|
||||
//
|
||||
// This is in the spirit of the cty philosophy of optimistically assuming that
|
||||
// DynamicPseudoType values will become the intended value eventually, and
|
||||
// dealing with any inconsistencies during final evaluation.
|
||||
func dynamicFixup(wantType cty.Type) conversion {
|
||||
return func(in cty.Value, path cty.Path) (cty.Value, error) {
|
||||
ret, err := Convert(in, wantType)
|
||||
if err != nil {
|
||||
// Re-wrap this error so that the returned path is relative
|
||||
// to the caller's original value, rather than relative to our
|
||||
// conversion value here.
|
||||
return cty.NilVal, path.NewError(err)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
}
|
||||
|
||||
// dynamicPassthrough is an identity conversion that is used when the
|
||||
// target type is DynamicPseudoType, indicating that the caller doesn't care
|
||||
// which type is returned.
|
||||
func dynamicPassthrough(in cty.Value, path cty.Path) (cty.Value, error) {
|
||||
return in, nil
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// conversionObjectToObject returns a conversion that will make the input
|
||||
// object type conform to the output object type, if possible.
|
||||
//
|
||||
// Conversion is possible only if the output type is a subset of the input
|
||||
// type, meaning that each attribute of the output type has a corresponding
|
||||
// attribute in the input type where a recursive conversion is available.
|
||||
//
|
||||
// Shallow object conversions work the same for both safe and unsafe modes,
|
||||
// but the safety flag is passed on to recursive conversions and may thus
|
||||
// limit the above definition of "subset".
|
||||
func conversionObjectToObject(in, out cty.Type, unsafe bool) conversion {
|
||||
inAtys := in.AttributeTypes()
|
||||
outAtys := out.AttributeTypes()
|
||||
attrConvs := make(map[string]conversion)
|
||||
|
||||
for name, outAty := range outAtys {
|
||||
inAty, exists := inAtys[name]
|
||||
if !exists {
|
||||
// No conversion is available, then.
|
||||
return nil
|
||||
}
|
||||
|
||||
if inAty.Equals(outAty) {
|
||||
// No conversion needed, but we'll still record the attribute
|
||||
// in our map for later reference.
|
||||
attrConvs[name] = nil
|
||||
continue
|
||||
}
|
||||
|
||||
attrConvs[name] = getConversion(inAty, outAty, unsafe)
|
||||
if attrConvs[name] == nil {
|
||||
// If a recursive conversion isn't available, then our top-level
|
||||
// configuration is impossible too.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here then a conversion is possible, using the attribute
|
||||
// conversions given in attrConvs.
|
||||
return func(val cty.Value, path cty.Path) (cty.Value, error) {
|
||||
attrVals := make(map[string]cty.Value, len(attrConvs))
|
||||
path = append(path, nil)
|
||||
pathStep := &path[len(path)-1]
|
||||
|
||||
for it := val.ElementIterator(); it.Next(); {
|
||||
nameVal, val := it.Element()
|
||||
var err error
|
||||
|
||||
name := nameVal.AsString()
|
||||
*pathStep = cty.GetAttrStep{
|
||||
Name: name,
|
||||
}
|
||||
|
||||
conv, exists := attrConvs[name]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
if conv != nil {
|
||||
val, err = conv(val, path)
|
||||
if err != nil {
|
||||
return cty.NilVal, err
|
||||
}
|
||||
}
|
||||
|
||||
attrVals[name] = val
|
||||
}
|
||||
|
||||
return cty.ObjectVal(attrVals), nil
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
var stringTrue = cty.StringVal("true")
|
||||
var stringFalse = cty.StringVal("false")
|
||||
|
||||
var primitiveConversionsSafe = map[cty.Type]map[cty.Type]conversion{
|
||||
cty.Number: {
|
||||
cty.String: func(val cty.Value, path cty.Path) (cty.Value, error) {
|
||||
f := val.AsBigFloat()
|
||||
return cty.StringVal(f.Text('f', -1)), nil
|
||||
},
|
||||
},
|
||||
cty.Bool: {
|
||||
cty.String: func(val cty.Value, path cty.Path) (cty.Value, error) {
|
||||
if val.True() {
|
||||
return stringTrue, nil
|
||||
} else {
|
||||
return stringFalse, nil
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var primitiveConversionsUnsafe = map[cty.Type]map[cty.Type]conversion{
|
||||
cty.String: {
|
||||
cty.Number: func(val cty.Value, path cty.Path) (cty.Value, error) {
|
||||
v, err := cty.ParseNumberVal(val.AsString())
|
||||
if err != nil {
|
||||
return cty.NilVal, path.NewErrorf("a number is required")
|
||||
}
|
||||
return v, nil
|
||||
},
|
||||
cty.Bool: func(val cty.Value, path cty.Path) (cty.Value, error) {
|
||||
switch val.AsString() {
|
||||
case "true", "1":
|
||||
return cty.True, nil
|
||||
case "false", "0":
|
||||
return cty.False, nil
|
||||
default:
|
||||
switch strings.ToLower(val.AsString()) {
|
||||
case "true":
|
||||
return cty.NilVal, path.NewErrorf("a bool is required; to convert from string, use lowercase \"true\"")
|
||||
case "false":
|
||||
return cty.NilVal, path.NewErrorf("a bool is required; to convert from string, use lowercase \"false\"")
|
||||
default:
|
||||
return cty.NilVal, path.NewErrorf("a bool is required")
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// conversionTupleToTuple returns a conversion that will make the input
|
||||
// tuple type conform to the output tuple type, if possible.
|
||||
//
|
||||
// Conversion is possible only if the two tuple types have the same number
|
||||
// of elements and the corresponding elements by index can be converted.
|
||||
//
|
||||
// Shallow tuple conversions work the same for both safe and unsafe modes,
|
||||
// but the safety flag is passed on to recursive conversions and may thus
|
||||
// limit which element type conversions are possible.
|
||||
func conversionTupleToTuple(in, out cty.Type, unsafe bool) conversion {
|
||||
inEtys := in.TupleElementTypes()
|
||||
outEtys := out.TupleElementTypes()
|
||||
|
||||
if len(inEtys) != len(outEtys) {
|
||||
return nil // no conversion is possible
|
||||
}
|
||||
|
||||
elemConvs := make([]conversion, len(inEtys))
|
||||
|
||||
for i, outEty := range outEtys {
|
||||
inEty := inEtys[i]
|
||||
|
||||
if inEty.Equals(outEty) {
|
||||
// No conversion needed, so we can leave this one nil.
|
||||
continue
|
||||
}
|
||||
|
||||
elemConvs[i] = getConversion(inEty, outEty, unsafe)
|
||||
if elemConvs[i] == nil {
|
||||
// If a recursive conversion isn't available, then our top-level
|
||||
// configuration is impossible too.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here then a conversion is possible, using the element
|
||||
// conversions given in elemConvs.
|
||||
return func(val cty.Value, path cty.Path) (cty.Value, error) {
|
||||
elemVals := make([]cty.Value, len(elemConvs))
|
||||
path = append(path, nil)
|
||||
pathStep := &path[len(path)-1]
|
||||
|
||||
i := 0
|
||||
for it := val.ElementIterator(); it.Next(); i++ {
|
||||
_, val := it.Element()
|
||||
var err error
|
||||
|
||||
*pathStep = cty.IndexStep{
|
||||
Key: cty.NumberIntVal(int64(i)),
|
||||
}
|
||||
|
||||
conv := elemConvs[i]
|
||||
if conv != nil {
|
||||
val, err = conv(val, path)
|
||||
if err != nil {
|
||||
return cty.NilVal, err
|
||||
}
|
||||
}
|
||||
|
||||
elemVals[i] = val
|
||||
}
|
||||
|
||||
return cty.TupleVal(elemVals), nil
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
// Package convert contains some routines for converting between cty types.
|
||||
// The intent of providing this package is to encourage applications using
|
||||
// cty to have consistent type conversion behavior for maximal interoperability
|
||||
// when Values pass from one application to another.
|
||||
//
|
||||
// The conversions are categorized into two categories. "Safe" conversions are
|
||||
// ones that are guaranteed to succeed if given a non-null value of the
|
||||
// appropriate source type. "Unsafe" conversions, on the other hand, are valid
|
||||
// for only a subset of input values, and thus may fail with an error when
|
||||
// called for values outside of that valid subset.
|
||||
//
|
||||
// The functions whose names end in Unsafe support all of the conversions that
|
||||
// are supported by the corresponding functions whose names do not have that
|
||||
// suffix, and then additional unsafe conversions as well.
|
||||
package convert
|
@ -0,0 +1,220 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// MismatchMessage is a helper to return an English-language description of
|
||||
// the differences between got and want, phrased as a reason why got does
|
||||
// not conform to want.
|
||||
//
|
||||
// This function does not itself attempt conversion, and so it should generally
|
||||
// be used only after a conversion has failed, to report the conversion failure
|
||||
// to an English-speaking user. The result will be confusing got is actually
|
||||
// conforming to or convertable to want.
|
||||
//
|
||||
// The shorthand helper function Convert uses this function internally to
|
||||
// produce its error messages, so callers of that function do not need to
|
||||
// also use MismatchMessage.
|
||||
//
|
||||
// This function is similar to Type.TestConformance, but it is tailored to
|
||||
// describing conversion failures and so the messages it generates relate
|
||||
// specifically to the conversion rules implemented in this package.
|
||||
func MismatchMessage(got, want cty.Type) string {
|
||||
switch {
|
||||
|
||||
case got.IsObjectType() && want.IsObjectType():
|
||||
// If both types are object types then we may be able to say something
|
||||
// about their respective attributes.
|
||||
return mismatchMessageObjects(got, want)
|
||||
|
||||
case got.IsTupleType() && want.IsListType() && want.ElementType() == cty.DynamicPseudoType:
|
||||
// If conversion from tuple to list failed then it's because we couldn't
|
||||
// find a common type to convert all of the tuple elements to.
|
||||
return "all list elements must have the same type"
|
||||
|
||||
case got.IsTupleType() && want.IsSetType() && want.ElementType() == cty.DynamicPseudoType:
|
||||
// If conversion from tuple to set failed then it's because we couldn't
|
||||
// find a common type to convert all of the tuple elements to.
|
||||
return "all set elements must have the same type"
|
||||
|
||||
case got.IsObjectType() && want.IsMapType() && want.ElementType() == cty.DynamicPseudoType:
|
||||
// If conversion from object to map failed then it's because we couldn't
|
||||
// find a common type to convert all of the object attributes to.
|
||||
return "all map elements must have the same type"
|
||||
|
||||
case (got.IsTupleType() || got.IsObjectType()) && want.IsCollectionType():
|
||||
return mismatchMessageCollectionsFromStructural(got, want)
|
||||
|
||||
case got.IsCollectionType() && want.IsCollectionType():
|
||||
return mismatchMessageCollectionsFromCollections(got, want)
|
||||
|
||||
default:
|
||||
// If we have nothing better to say, we'll just state what was required.
|
||||
return want.FriendlyNameForConstraint() + " required"
|
||||
}
|
||||
}
|
||||
|
||||
func mismatchMessageObjects(got, want cty.Type) string {
|
||||
// Per our conversion rules, "got" is allowed to be a superset of "want",
|
||||
// and so we'll produce error messages here under that assumption.
|
||||
gotAtys := got.AttributeTypes()
|
||||
wantAtys := want.AttributeTypes()
|
||||
|
||||
// If we find missing attributes then we'll report those in preference,
|
||||
// but if not then we will report a maximum of one non-conforming
|
||||
// attribute, just to keep our messages relatively terse.
|
||||
// We'll also prefer to report a recursive type error from an _unsafe_
|
||||
// conversion over a safe one, because these are subjectively more
|
||||
// "serious".
|
||||
var missingAttrs []string
|
||||
var unsafeMismatchAttr string
|
||||
var safeMismatchAttr string
|
||||
|
||||
for name, wantAty := range wantAtys {
|
||||
gotAty, exists := gotAtys[name]
|
||||
if !exists {
|
||||
missingAttrs = append(missingAttrs, name)
|
||||
continue
|
||||
}
|
||||
|
||||
// We'll now try to convert these attributes in isolation and
|
||||
// see if we have a nested conversion error to report.
|
||||
// We'll try an unsafe conversion first, and then fall back on
|
||||
// safe if unsafe is possible.
|
||||
|
||||
// If we already have an unsafe mismatch attr error then we won't bother
|
||||
// hunting for another one.
|
||||
if unsafeMismatchAttr != "" {
|
||||
continue
|
||||
}
|
||||
if conv := GetConversionUnsafe(gotAty, wantAty); conv == nil {
|
||||
unsafeMismatchAttr = fmt.Sprintf("attribute %q: %s", name, MismatchMessage(gotAty, wantAty))
|
||||
}
|
||||
|
||||
// If we already have a safe mismatch attr error then we won't bother
|
||||
// hunting for another one.
|
||||
if safeMismatchAttr != "" {
|
||||
continue
|
||||
}
|
||||
if conv := GetConversion(gotAty, wantAty); conv == nil {
|
||||
safeMismatchAttr = fmt.Sprintf("attribute %q: %s", name, MismatchMessage(gotAty, wantAty))
|
||||
}
|
||||
}
|
||||
|
||||
// We should now have collected at least one problem. If we have more than
|
||||
// one then we'll use our preference order to decide what is most important
|
||||
// to report.
|
||||
switch {
|
||||
|
||||
case len(missingAttrs) != 0:
|
||||
sort.Strings(missingAttrs)
|
||||
switch len(missingAttrs) {
|
||||
case 1:
|
||||
return fmt.Sprintf("attribute %q is required", missingAttrs[0])
|
||||
case 2:
|
||||
return fmt.Sprintf("attributes %q and %q are required", missingAttrs[0], missingAttrs[1])
|
||||
default:
|
||||
sort.Strings(missingAttrs)
|
||||
var buf bytes.Buffer
|
||||
for _, name := range missingAttrs[:len(missingAttrs)-1] {
|
||||
fmt.Fprintf(&buf, "%q, ", name)
|
||||
}
|
||||
fmt.Fprintf(&buf, "and %q", missingAttrs[len(missingAttrs)-1])
|
||||
return fmt.Sprintf("attributes %s are required", buf.Bytes())
|
||||
}
|
||||
|
||||
case unsafeMismatchAttr != "":
|
||||
return unsafeMismatchAttr
|
||||
|
||||
case safeMismatchAttr != "":
|
||||
return safeMismatchAttr
|
||||
|
||||
default:
|
||||
// We should never get here, but if we do then we'll return
|
||||
// just a generic message.
|
||||
return "incorrect object attributes"
|
||||
}
|
||||
}
|
||||
|
||||
func mismatchMessageCollectionsFromStructural(got, want cty.Type) string {
|
||||
// First some straightforward cases where the kind is just altogether wrong.
|
||||
switch {
|
||||
case want.IsListType() && !got.IsTupleType():
|
||||
return want.FriendlyNameForConstraint() + " required"
|
||||
case want.IsSetType() && !got.IsTupleType():
|
||||
return want.FriendlyNameForConstraint() + " required"
|
||||
case want.IsMapType() && !got.IsObjectType():
|
||||
return want.FriendlyNameForConstraint() + " required"
|
||||
}
|
||||
|
||||
// If the kinds are matched well enough then we'll move on to checking
|
||||
// individual elements.
|
||||
wantEty := want.ElementType()
|
||||
switch {
|
||||
case got.IsTupleType():
|
||||
for i, gotEty := range got.TupleElementTypes() {
|
||||
if gotEty.Equals(wantEty) {
|
||||
continue // exact match, so no problem
|
||||
}
|
||||
if conv := getConversion(gotEty, wantEty, true); conv != nil {
|
||||
continue // conversion is available, so no problem
|
||||
}
|
||||
return fmt.Sprintf("element %d: %s", i, MismatchMessage(gotEty, wantEty))
|
||||
}
|
||||
|
||||
// If we get down here then something weird is going on but we'll
|
||||
// return a reasonable fallback message anyway.
|
||||
return fmt.Sprintf("all elements must be %s", wantEty.FriendlyNameForConstraint())
|
||||
|
||||
case got.IsObjectType():
|
||||
for name, gotAty := range got.AttributeTypes() {
|
||||
if gotAty.Equals(wantEty) {
|
||||
continue // exact match, so no problem
|
||||
}
|
||||
if conv := getConversion(gotAty, wantEty, true); conv != nil {
|
||||
continue // conversion is available, so no problem
|
||||
}
|
||||
return fmt.Sprintf("element %q: %s", name, MismatchMessage(gotAty, wantEty))
|
||||
}
|
||||
|
||||
// If we get down here then something weird is going on but we'll
|
||||
// return a reasonable fallback message anyway.
|
||||
return fmt.Sprintf("all elements must be %s", wantEty.FriendlyNameForConstraint())
|
||||
|
||||
default:
|
||||
// Should not be possible to get here since we only call this function
|
||||
// with got as structural types, but...
|
||||
return want.FriendlyNameForConstraint() + " required"
|
||||
}
|
||||
}
|
||||
|
||||
func mismatchMessageCollectionsFromCollections(got, want cty.Type) string {
|
||||
// First some straightforward cases where the kind is just altogether wrong.
|
||||
switch {
|
||||
case want.IsListType() && !(got.IsListType() || got.IsSetType()):
|
||||
return want.FriendlyNameForConstraint() + " required"
|
||||
case want.IsSetType() && !(got.IsListType() || got.IsSetType()):
|
||||
return want.FriendlyNameForConstraint() + " required"
|
||||
case want.IsMapType() && !got.IsMapType():
|
||||
return want.FriendlyNameForConstraint() + " required"
|
||||
}
|
||||
|
||||
// If the kinds are matched well enough then we'll check the element types.
|
||||
gotEty := got.ElementType()
|
||||
wantEty := want.ElementType()
|
||||
noun := "element type"
|
||||
switch {
|
||||
case want.IsListType():
|
||||
noun = "list element type"
|
||||
case want.IsSetType():
|
||||
noun = "set element type"
|
||||
case want.IsMapType():
|
||||
noun = "map element type"
|
||||
}
|
||||
return fmt.Sprintf("incorrect %s: %s", noun, MismatchMessage(gotEty, wantEty))
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// This file contains the public interface of this package, which is intended
|
||||
// to be a small, convenient interface designed for easy integration into
|
||||
// a hypothetical language type checker and interpreter.
|
||||
|
||||
// Conversion is a named function type representing a conversion from a
|
||||
// value of one type to a value of another type.
|
||||
//
|
||||
// The source type for a conversion is always the source type given to
|
||||
// the function that returned the Conversion, but there is no way to recover
|
||||
// that from a Conversion value itself. If a Conversion is given a value
|
||||
// that is not of its expected type (with the exception of DynamicPseudoType,
|
||||
// which is always supported) then the function may panic or produce undefined
|
||||
// results.
|
||||
type Conversion func(in cty.Value) (out cty.Value, err error)
|
||||
|
||||
// GetConversion returns a Conversion between the given in and out Types if
|
||||
// a safe one is available, or returns nil otherwise.
|
||||
func GetConversion(in cty.Type, out cty.Type) Conversion {
|
||||
return retConversion(getConversion(in, out, false))
|
||||
}
|
||||
|
||||
// GetConversionUnsafe returns a Conversion between the given in and out Types
|
||||
// if either a safe or unsafe one is available, or returns nil otherwise.
|
||||
func GetConversionUnsafe(in cty.Type, out cty.Type) Conversion {
|
||||
return retConversion(getConversion(in, out, true))
|
||||
}
|
||||
|
||||
// Convert returns the result of converting the given value to the given type
|
||||
// if an safe or unsafe conversion is available, or returns an error if such a
|
||||
// conversion is impossible.
|
||||
//
|
||||
// This is a convenience wrapper around calling GetConversionUnsafe and then
|
||||
// immediately passing the given value to the resulting function.
|
||||
func Convert(in cty.Value, want cty.Type) (cty.Value, error) {
|
||||
if in.Type().Equals(want) {
|
||||
return in, nil
|
||||
}
|
||||
|
||||
conv := GetConversionUnsafe(in.Type(), want)
|
||||
if conv == nil {
|
||||
return cty.NilVal, errors.New(MismatchMessage(in.Type(), want))
|
||||
}
|
||||
return conv(in)
|
||||
}
|
||||
|
||||
// Unify attempts to find the most general type that can be converted from
|
||||
// all of the given types. If this is possible, that type is returned along
|
||||
// with a slice of necessary conversions for some of the given types.
|
||||
//
|
||||
// If no common supertype can be found, this function returns cty.NilType and
|
||||
// a nil slice.
|
||||
//
|
||||
// If a common supertype *can* be found, the returned slice will always be
|
||||
// non-nil and will contain a non-nil conversion for each given type that
|
||||
// needs to be converted, with indices corresponding to the input slice.
|
||||
// Any given type that does *not* need conversion (because it is already of
|
||||
// the appropriate type) will have a nil Conversion.
|
||||
//
|
||||
// cty.DynamicPseudoType is, as usual, a special case. If the given type list
|
||||
// contains a mixture of dynamic and non-dynamic types, the dynamic types are
|
||||
// disregarded for type selection and a conversion is returned for them that
|
||||
// will attempt a late conversion of the given value to the target type,
|
||||
// failing with a conversion error if the eventual concrete type is not
|
||||
// compatible. If *all* given types are DynamicPseudoType, or in the
|
||||
// degenerate case of an empty slice of types, the returned type is itself
|
||||
// cty.DynamicPseudoType and no conversions are attempted.
|
||||
func Unify(types []cty.Type) (cty.Type, []Conversion) {
|
||||
return unify(types, false)
|
||||
}
|
||||
|
||||
// UnifyUnsafe is the same as Unify except that it may return unsafe
|
||||
// conversions in situations where a safe conversion isn't also available.
|
||||
func UnifyUnsafe(types []cty.Type) (cty.Type, []Conversion) {
|
||||
return unify(types, true)
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// sortTypes produces an ordering of the given types that serves as a
|
||||
// preference order for the result of unification of the given types.
|
||||
// The return value is a slice of indices into the given slice, and will
|
||||
// thus always be the same length as the given slice.
|
||||
//
|
||||
// The goal is that the most general of the given types will appear first
|
||||
// in the ordering. If there are uncomparable pairs of types in the list
|
||||
// then they will appear in an undefined order, and the unification pass
|
||||
// will presumably then fail.
|
||||
func sortTypes(tys []cty.Type) []int {
|
||||
l := len(tys)
|
||||
|
||||
// First we build a graph whose edges represent "more general than",
|
||||
// which we will then do a topological sort of.
|
||||
edges := make([][]int, l)
|
||||
for i := 0; i < (l - 1); i++ {
|
||||
for j := i + 1; j < l; j++ {
|
||||
cmp := compareTypes(tys[i], tys[j])
|
||||
switch {
|
||||
case cmp < 0:
|
||||
edges[i] = append(edges[i], j)
|
||||
case cmp > 0:
|
||||
edges[j] = append(edges[j], i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute the in-degree of each node
|
||||
inDegree := make([]int, l)
|
||||
for _, outs := range edges {
|
||||
for _, j := range outs {
|
||||
inDegree[j]++
|
||||
}
|
||||
}
|
||||
|
||||
// The array backing our result will double as our queue for visiting
|
||||
// the nodes, with the queue slice moving along this array until it
|
||||
// is empty and positioned at the end of the array. Thus our visiting
|
||||
// order is also our result order.
|
||||
result := make([]int, l)
|
||||
queue := result[0:0]
|
||||
|
||||
// Initialize the queue with any item of in-degree 0, preserving
|
||||
// their relative order.
|
||||
for i, n := range inDegree {
|
||||
if n == 0 {
|
||||
queue = append(queue, i)
|
||||
}
|
||||
}
|
||||
|
||||
for len(queue) != 0 {
|
||||
i := queue[0]
|
||||
queue = queue[1:]
|
||||
for _, j := range edges[i] {
|
||||
inDegree[j]--
|
||||
if inDegree[j] == 0 {
|
||||
queue = append(queue, j)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
@ -0,0 +1,314 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// The current unify implementation is somewhat inefficient, but we accept this
|
||||
// under the assumption that it will generally be used with small numbers of
|
||||
// types and with types of reasonable complexity. However, it does have a
|
||||
// "happy path" where all of the given types are equal.
|
||||
//
|
||||
// This function is likely to have poor performance in cases where any given
|
||||
// types are very complex (lots of deeply-nested structures) or if the list
|
||||
// of types itself is very large. In particular, it will walk the nested type
|
||||
// structure under the given types several times, especially when given a
|
||||
// list of types for which unification is not possible, since each permutation
|
||||
// will be tried to determine that result.
|
||||
func unify(types []cty.Type, unsafe bool) (cty.Type, []Conversion) {
|
||||
if len(types) == 0 {
|
||||
// Degenerate case
|
||||
return cty.NilType, nil
|
||||
}
|
||||
|
||||
// If all of the given types are of the same structural kind, we may be
|
||||
// able to construct a new type that they can all be unified to, even if
|
||||
// that is not one of the given types. We must try this before the general
|
||||
// behavior below because in unsafe mode we can convert an object type to
|
||||
// a subset of that type, which would be a much less useful conversion for
|
||||
// unification purposes.
|
||||
{
|
||||
objectCt := 0
|
||||
tupleCt := 0
|
||||
dynamicCt := 0
|
||||
for _, ty := range types {
|
||||
switch {
|
||||
case ty.IsObjectType():
|
||||
objectCt++
|
||||
case ty.IsTupleType():
|
||||
tupleCt++
|
||||
case ty == cty.DynamicPseudoType:
|
||||
dynamicCt++
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case objectCt > 0 && (objectCt+dynamicCt) == len(types):
|
||||
return unifyObjectTypes(types, unsafe, dynamicCt > 0)
|
||||
case tupleCt > 0 && (tupleCt+dynamicCt) == len(types):
|
||||
return unifyTupleTypes(types, unsafe, dynamicCt > 0)
|
||||
case objectCt > 0 && tupleCt > 0:
|
||||
// Can never unify object and tuple types since they have incompatible kinds
|
||||
return cty.NilType, nil
|
||||
}
|
||||
}
|
||||
|
||||
prefOrder := sortTypes(types)
|
||||
|
||||
// sortTypes gives us an order where earlier items are preferable as
|
||||
// our result type. We'll now walk through these and choose the first
|
||||
// one we encounter for which conversions exist for all source types.
|
||||
conversions := make([]Conversion, len(types))
|
||||
Preferences:
|
||||
for _, wantTypeIdx := range prefOrder {
|
||||
wantType := types[wantTypeIdx]
|
||||
for i, tryType := range types {
|
||||
if i == wantTypeIdx {
|
||||
// Don't need to convert our wanted type to itself
|
||||
conversions[i] = nil
|
||||
continue
|
||||
}
|
||||
|
||||
if tryType.Equals(wantType) {
|
||||
conversions[i] = nil
|
||||
continue
|
||||
}
|
||||
|
||||
if unsafe {
|
||||
conversions[i] = GetConversionUnsafe(tryType, wantType)
|
||||
} else {
|
||||
conversions[i] = GetConversion(tryType, wantType)
|
||||
}
|
||||
|
||||
if conversions[i] == nil {
|
||||
// wantType is not a suitable unification type, so we'll
|
||||
// try the next one in our preference order.
|
||||
continue Preferences
|
||||
}
|
||||
}
|
||||
|
||||
return wantType, conversions
|
||||
}
|
||||
|
||||
// If we fall out here, no unification is possible
|
||||
return cty.NilType, nil
|
||||
}
|
||||
|
||||
func unifyObjectTypes(types []cty.Type, unsafe bool, hasDynamic bool) (cty.Type, []Conversion) {
|
||||
// If we had any dynamic types in the input here then we can't predict
|
||||
// what path we'll take through here once these become known types, so
|
||||
// we'll conservatively produce DynamicVal for these.
|
||||
if hasDynamic {
|
||||
return unifyAllAsDynamic(types)
|
||||
}
|
||||
|
||||
// There are two different ways we can succeed here:
|
||||
// - If all of the given object types have the same set of attribute names
|
||||
// and the corresponding types are all unifyable, then we construct that
|
||||
// type.
|
||||
// - If the given object types have different attribute names or their
|
||||
// corresponding types are not unifyable, we'll instead try to unify
|
||||
// all of the attribute types together to produce a map type.
|
||||
//
|
||||
// Our unification behavior is intentionally stricter than our conversion
|
||||
// behavior for subset object types because user intent is different with
|
||||
// unification use-cases: it makes sense to allow {"foo":true} to convert
|
||||
// to emptyobjectval, but unifying an object with an attribute with the
|
||||
// empty object type should be an error because unifying to the empty
|
||||
// object type would be suprising and useless.
|
||||
|
||||
firstAttrs := types[0].AttributeTypes()
|
||||
for _, ty := range types[1:] {
|
||||
thisAttrs := ty.AttributeTypes()
|
||||
if len(thisAttrs) != len(firstAttrs) {
|
||||
// If number of attributes is different then there can be no
|
||||
// object type in common.
|
||||
return unifyObjectTypesToMap(types, unsafe)
|
||||
}
|
||||
for name := range thisAttrs {
|
||||
if _, ok := firstAttrs[name]; !ok {
|
||||
// If attribute names don't exactly match then there can be
|
||||
// no object type in common.
|
||||
return unifyObjectTypesToMap(types, unsafe)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here then we've proven that all of the given object types
|
||||
// have exactly the same set of attribute names, though the types may
|
||||
// differ.
|
||||
retAtys := make(map[string]cty.Type)
|
||||
atysAcross := make([]cty.Type, len(types))
|
||||
for name := range firstAttrs {
|
||||
for i, ty := range types {
|
||||
atysAcross[i] = ty.AttributeType(name)
|
||||
}
|
||||
retAtys[name], _ = unify(atysAcross, unsafe)
|
||||
if retAtys[name] == cty.NilType {
|
||||
// Cannot unify this attribute alone, which means that unification
|
||||
// of everything down to a map type can't be possible either.
|
||||
return cty.NilType, nil
|
||||
}
|
||||
}
|
||||
retTy := cty.Object(retAtys)
|
||||
|
||||
conversions := make([]Conversion, len(types))
|
||||
for i, ty := range types {
|
||||
if ty.Equals(retTy) {
|
||||
continue
|
||||
}
|
||||
if unsafe {
|
||||
conversions[i] = GetConversionUnsafe(ty, retTy)
|
||||
} else {
|
||||
conversions[i] = GetConversion(ty, retTy)
|
||||
}
|
||||
if conversions[i] == nil {
|
||||
// Shouldn't be reachable, since we were able to unify
|
||||
return unifyObjectTypesToMap(types, unsafe)
|
||||
}
|
||||
}
|
||||
|
||||
return retTy, conversions
|
||||
}
|
||||
|
||||
func unifyObjectTypesToMap(types []cty.Type, unsafe bool) (cty.Type, []Conversion) {
|
||||
// This is our fallback case for unifyObjectTypes, where we see if we can
|
||||
// construct a map type that can accept all of the attribute types.
|
||||
|
||||
var atys []cty.Type
|
||||
for _, ty := range types {
|
||||
for _, aty := range ty.AttributeTypes() {
|
||||
atys = append(atys, aty)
|
||||
}
|
||||
}
|
||||
|
||||
ety, _ := unify(atys, unsafe)
|
||||
if ety == cty.NilType {
|
||||
return cty.NilType, nil
|
||||
}
|
||||
|
||||
retTy := cty.Map(ety)
|
||||
conversions := make([]Conversion, len(types))
|
||||
for i, ty := range types {
|
||||
if ty.Equals(retTy) {
|
||||
continue
|
||||
}
|
||||
if unsafe {
|
||||
conversions[i] = GetConversionUnsafe(ty, retTy)
|
||||
} else {
|
||||
conversions[i] = GetConversion(ty, retTy)
|
||||
}
|
||||
if conversions[i] == nil {
|
||||
return cty.NilType, nil
|
||||
}
|
||||
}
|
||||
return retTy, conversions
|
||||
}
|
||||
|
||||
func unifyTupleTypes(types []cty.Type, unsafe bool, hasDynamic bool) (cty.Type, []Conversion) {
|
||||
// If we had any dynamic types in the input here then we can't predict
|
||||
// what path we'll take through here once these become known types, so
|
||||
// we'll conservatively produce DynamicVal for these.
|
||||
if hasDynamic {
|
||||
return unifyAllAsDynamic(types)
|
||||
}
|
||||
|
||||
// There are two different ways we can succeed here:
|
||||
// - If all of the given tuple types have the same sequence of element types
|
||||
// and the corresponding types are all unifyable, then we construct that
|
||||
// type.
|
||||
// - If the given tuple types have different element types or their
|
||||
// corresponding types are not unifyable, we'll instead try to unify
|
||||
// all of the elements types together to produce a list type.
|
||||
|
||||
firstEtys := types[0].TupleElementTypes()
|
||||
for _, ty := range types[1:] {
|
||||
thisEtys := ty.TupleElementTypes()
|
||||
if len(thisEtys) != len(firstEtys) {
|
||||
// If number of elements is different then there can be no
|
||||
// tuple type in common.
|
||||
return unifyTupleTypesToList(types, unsafe)
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here then we've proven that all of the given tuple types
|
||||
// have the same number of elements, though the types may differ.
|
||||
retEtys := make([]cty.Type, len(firstEtys))
|
||||
atysAcross := make([]cty.Type, len(types))
|
||||
for idx := range firstEtys {
|
||||
for tyI, ty := range types {
|
||||
atysAcross[tyI] = ty.TupleElementTypes()[idx]
|
||||
}
|
||||
retEtys[idx], _ = unify(atysAcross, unsafe)
|
||||
if retEtys[idx] == cty.NilType {
|
||||
// Cannot unify this element alone, which means that unification
|
||||
// of everything down to a map type can't be possible either.
|
||||
return cty.NilType, nil
|
||||
}
|
||||
}
|
||||
retTy := cty.Tuple(retEtys)
|
||||
|
||||
conversions := make([]Conversion, len(types))
|
||||
for i, ty := range types {
|
||||
if ty.Equals(retTy) {
|
||||
continue
|
||||
}
|
||||
if unsafe {
|
||||
conversions[i] = GetConversionUnsafe(ty, retTy)
|
||||
} else {
|
||||
conversions[i] = GetConversion(ty, retTy)
|
||||
}
|
||||
if conversions[i] == nil {
|
||||
// Shouldn't be reachable, since we were able to unify
|
||||
return unifyTupleTypesToList(types, unsafe)
|
||||
}
|
||||
}
|
||||
|
||||
return retTy, conversions
|
||||
}
|
||||
|
||||
func unifyTupleTypesToList(types []cty.Type, unsafe bool) (cty.Type, []Conversion) {
|
||||
// This is our fallback case for unifyTupleTypes, where we see if we can
|
||||
// construct a list type that can accept all of the element types.
|
||||
|
||||
var etys []cty.Type
|
||||
for _, ty := range types {
|
||||
for _, ety := range ty.TupleElementTypes() {
|
||||
etys = append(etys, ety)
|
||||
}
|
||||
}
|
||||
|
||||
ety, _ := unify(etys, unsafe)
|
||||
if ety == cty.NilType {
|
||||
return cty.NilType, nil
|
||||
}
|
||||
|
||||
retTy := cty.List(ety)
|
||||
conversions := make([]Conversion, len(types))
|
||||
for i, ty := range types {
|
||||
if ty.Equals(retTy) {
|
||||
continue
|
||||
}
|
||||
if unsafe {
|
||||
conversions[i] = GetConversionUnsafe(ty, retTy)
|
||||
} else {
|
||||
conversions[i] = GetConversion(ty, retTy)
|
||||
}
|
||||
if conversions[i] == nil {
|
||||
// Shouldn't be reachable, since we were able to unify
|
||||
return unifyObjectTypesToMap(types, unsafe)
|
||||
}
|
||||
}
|
||||
return retTy, conversions
|
||||
}
|
||||
|
||||
func unifyAllAsDynamic(types []cty.Type) (cty.Type, []Conversion) {
|
||||
conversions := make([]Conversion, len(types))
|
||||
for i := range conversions {
|
||||
conversions[i] = func(cty.Value) (cty.Value, error) {
|
||||
return cty.DynamicVal, nil
|
||||
}
|
||||
}
|
||||
return cty.DynamicPseudoType, conversions
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
// Package cty (pronounced see-tie) provides some infrastructure for a type
|
||||
// system that might be useful for applications that need to represent
|
||||
// configuration values provided by the user whose types are not known
|
||||
// at compile time, particularly if the calling application also allows
|
||||
// such values to be used in expressions.
|
||||
//
|
||||
// The type system consists of primitive types Number, String and Bool, as
|
||||
// well as List and Map collection types and Object types that can have
|
||||
// arbitrarily-typed sets of attributes.
|
||||
//
|
||||
// A set of operations is defined on these types, which is accessible via
|
||||
// the wrapper struct Value, which annotates the raw, internal representation
|
||||
// of a value with its corresponding type.
|
||||
//
|
||||
// This package is oriented towards being a building block for configuration
|
||||
// languages used to bootstrap an application. It is not optimized for use
|
||||
// in tight loops where CPU time or memory pressure are a concern.
|
||||
package cty
|
@ -0,0 +1,194 @@
|
||||
package cty
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/zclconf/go-cty/cty/set"
|
||||
)
|
||||
|
||||
// ElementIterator is the interface type returned by Value.ElementIterator to
|
||||
// allow the caller to iterate over elements of a collection-typed value.
|
||||
//
|
||||
// Its usage pattern is as follows:
|
||||
//
|
||||
// it := val.ElementIterator()
|
||||
// for it.Next() {
|
||||
// key, val := it.Element()
|
||||
// // ...
|
||||
// }
|
||||
type ElementIterator interface {
|
||||
Next() bool
|
||||
Element() (key Value, value Value)
|
||||
}
|
||||
|
||||
func canElementIterator(val Value) bool {
|
||||
switch {
|
||||
case val.IsMarked():
|
||||
return false
|
||||
case val.ty.IsListType():
|
||||
return true
|
||||
case val.ty.IsMapType():
|
||||
return true
|
||||
case val.ty.IsSetType():
|
||||
return true
|
||||
case val.ty.IsTupleType():
|
||||
return true
|
||||
case val.ty.IsObjectType():
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func elementIterator(val Value) ElementIterator {
|
||||
val.assertUnmarked()
|
||||
switch {
|
||||
case val.ty.IsListType():
|
||||
return &listElementIterator{
|
||||
ety: val.ty.ElementType(),
|
||||
vals: val.v.([]interface{}),
|
||||
idx: -1,
|
||||
}
|
||||
case val.ty.IsMapType():
|
||||
// We iterate the keys in a predictable lexicographical order so
|
||||
// that results will always be stable given the same input map.
|
||||
rawMap := val.v.(map[string]interface{})
|
||||
keys := make([]string, 0, len(rawMap))
|
||||
for key := range rawMap {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
return &mapElementIterator{
|
||||
ety: val.ty.ElementType(),
|
||||
vals: rawMap,
|
||||
keys: keys,
|
||||
idx: -1,
|
||||
}
|
||||
case val.ty.IsSetType():
|
||||
rawSet := val.v.(set.Set)
|
||||
return &setElementIterator{
|
||||
ety: val.ty.ElementType(),
|
||||
setIt: rawSet.Iterator(),
|
||||
}
|
||||
case val.ty.IsTupleType():
|
||||
return &tupleElementIterator{
|
||||
etys: val.ty.TupleElementTypes(),
|
||||
vals: val.v.([]interface{}),
|
||||
idx: -1,
|
||||
}
|
||||
case val.ty.IsObjectType():
|
||||
// We iterate the keys in a predictable lexicographical order so
|
||||
// that results will always be stable given the same object type.
|
||||
atys := val.ty.AttributeTypes()
|
||||
keys := make([]string, 0, len(atys))
|
||||
for key := range atys {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
return &objectElementIterator{
|
||||
atys: atys,
|
||||
vals: val.v.(map[string]interface{}),
|
||||
attrNames: keys,
|
||||
idx: -1,
|
||||
}
|
||||
default:
|
||||
panic("attempt to iterate on non-collection, non-tuple type")
|
||||
}
|
||||
}
|
||||
|
||||
type listElementIterator struct {
|
||||
ety Type
|
||||
vals []interface{}
|
||||
idx int
|
||||
}
|
||||
|
||||
func (it *listElementIterator) Element() (Value, Value) {
|
||||
i := it.idx
|
||||
return NumberIntVal(int64(i)), Value{
|
||||
ty: it.ety,
|
||||
v: it.vals[i],
|
||||
}
|
||||
}
|
||||
|
||||
func (it *listElementIterator) Next() bool {
|
||||
it.idx++
|
||||
return it.idx < len(it.vals)
|
||||
}
|
||||
|
||||
type mapElementIterator struct {
|
||||
ety Type
|
||||
vals map[string]interface{}
|
||||
keys []string
|
||||
idx int
|
||||
}
|
||||
|
||||
func (it *mapElementIterator) Element() (Value, Value) {
|
||||
key := it.keys[it.idx]
|
||||
return StringVal(key), Value{
|
||||
ty: it.ety,
|
||||
v: it.vals[key],
|
||||
}
|
||||
}
|
||||
|
||||
func (it *mapElementIterator) Next() bool {
|
||||
it.idx++
|
||||
return it.idx < len(it.keys)
|
||||
}
|
||||
|
||||
type setElementIterator struct {
|
||||
ety Type
|
||||
setIt *set.Iterator
|
||||
}
|
||||
|
||||
func (it *setElementIterator) Element() (Value, Value) {
|
||||
val := Value{
|
||||
ty: it.ety,
|
||||
v: it.setIt.Value(),
|
||||
}
|
||||
return val, val
|
||||
}
|
||||
|
||||
func (it *setElementIterator) Next() bool {
|
||||
return it.setIt.Next()
|
||||
}
|
||||
|
||||
type tupleElementIterator struct {
|
||||
etys []Type
|
||||
vals []interface{}
|
||||
idx int
|
||||
}
|
||||
|
||||
func (it *tupleElementIterator) Element() (Value, Value) {
|
||||
i := it.idx
|
||||
return NumberIntVal(int64(i)), Value{
|
||||
ty: it.etys[i],
|
||||
v: it.vals[i],
|
||||
}
|
||||
}
|
||||
|
||||
func (it *tupleElementIterator) Next() bool {
|
||||
it.idx++
|
||||
return it.idx < len(it.vals)
|
||||
}
|
||||
|
||||
type objectElementIterator struct {
|
||||
atys map[string]Type
|
||||
vals map[string]interface{}
|
||||
attrNames []string
|
||||
idx int
|
||||
}
|
||||
|
||||
func (it *objectElementIterator) Element() (Value, Value) {
|
||||
key := it.attrNames[it.idx]
|
||||
return StringVal(key), Value{
|
||||
ty: it.atys[key],
|
||||
v: it.vals[key],
|
||||
}
|
||||
}
|
||||
|
||||
func (it *objectElementIterator) Next() bool {
|
||||
it.idx++
|
||||
return it.idx < len(it.attrNames)
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package cty
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// PathError is a specialization of error that represents where in a
|
||||
// potentially-deep data structure an error occured, using a Path.
|
||||
type PathError struct {
|
||||
error
|
||||
Path Path
|
||||
}
|
||||
|
||||
func errorf(path Path, f string, args ...interface{}) error {
|
||||
// We need to copy the Path because often our caller builds it by
|
||||
// continually mutating the same underlying buffer.
|
||||
sPath := make(Path, len(path))
|
||||
copy(sPath, path)
|
||||
return PathError{
|
||||
error: fmt.Errorf(f, args...),
|
||||
Path: sPath,
|
||||
}
|
||||
}
|
||||
|
||||
// NewErrorf creates a new PathError for the current path by passing the
|
||||
// given format and arguments to fmt.Errorf and then wrapping the result
|
||||
// similarly to NewError.
|
||||
func (p Path) NewErrorf(f string, args ...interface{}) error {
|
||||
return errorf(p, f, args...)
|
||||
}
|
||||
|
||||
// NewError creates a new PathError for the current path, wrapping the given
|
||||
// error.
|
||||
func (p Path) NewError(err error) error {
|
||||
// if we're being asked to wrap an existing PathError then our new
|
||||
// PathError will be the concatenation of the two paths, ensuring
|
||||
// that we still get a single flat PathError that's thus easier for
|
||||
// callers to deal with.
|
||||
perr, wrappingPath := err.(PathError)
|
||||
pathLen := len(p)
|
||||
if wrappingPath {
|
||||
pathLen = pathLen + len(perr.Path)
|
||||
}
|
||||
|
||||
sPath := make(Path, pathLen)
|
||||
copy(sPath, p)
|
||||
if wrappingPath {
|
||||
copy(sPath[len(p):], perr.Path)
|
||||
}
|
||||
|
||||
return PathError{
|
||||
error: err,
|
||||
Path: sPath,
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
package function
|
||||
|
||||
import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// Parameter represents a parameter to a function.
|
||||
type Parameter struct {
|
||||
// Name is an optional name for the argument. This package ignores this
|
||||
// value, but callers may use it for documentation, etc.
|
||||
Name string
|
||||
|
||||
// A type that any argument for this parameter must conform to.
|
||||
// cty.DynamicPseudoType can be used, either at top-level or nested
|
||||
// in a parameterized type, to indicate that any type should be
|
||||
// permitted, to allow the definition of type-generic functions.
|
||||
Type cty.Type
|
||||
|
||||
// If AllowNull is set then null values may be passed into this
|
||||
// argument's slot in both the type-check function and the implementation
|
||||
// function. If not set, such values are rejected by the built-in
|
||||
// checking rules.
|
||||
AllowNull bool
|
||||
|
||||
// If AllowUnknown is set then unknown values may be passed into this
|
||||
// argument's slot in the implementation function. If not set, any
|
||||
// unknown values will cause the function to immediately return
|
||||
// an unkonwn value without calling the implementation function, thus
|
||||
// freeing the function implementer from dealing with this case.
|
||||
AllowUnknown bool
|
||||
|
||||
// If AllowDynamicType is set then DynamicVal may be passed into this
|
||||
// argument's slot in the implementation function. If not set, any
|
||||
// dynamic values will cause the function to immediately return
|
||||
// DynamicVal value without calling the implementation function, thus
|
||||
// freeing the function implementer from dealing with this case.
|
||||
//
|
||||
// Note that DynamicVal is also unknown, so in order to receive dynamic
|
||||
// *values* it is also necessary to set AllowUnknown.
|
||||
//
|
||||
// However, it is valid to set AllowDynamicType without AllowUnknown, in
|
||||
// which case a dynamic value may be passed to the type checking function
|
||||
// but will not make it to the *implementation* function. Instead, an
|
||||
// unknown value of the type returned by the type-check function will be
|
||||
// returned. This is suggested for functions that have a static return
|
||||
// type since it allows the return value to be typed even if the input
|
||||
// values are not, thus improving the type-check accuracy of derived
|
||||
// values.
|
||||
AllowDynamicType bool
|
||||
|
||||
// If AllowMarked is set then marked values may be passed into this
|
||||
// argument's slot in the implementation function. If not set, any
|
||||
// marked value will be unmarked before calling and then the markings
|
||||
// from that value will be applied automatically to the function result,
|
||||
// ensuring that the marks get propagated in a simplistic way even if
|
||||
// a function is unable to handle them.
|
||||
//
|
||||
// For any argument whose parameter has AllowMarked set, it's the
|
||||
// function implementation's responsibility to Unmark the given value
|
||||
// and propagate the marks appropriatedly to the result in order to
|
||||
// avoid losing the marks. Application-specific functions might use
|
||||
// special rules to selectively propagate particular marks.
|
||||
//
|
||||
// The automatic unmarking of values applies only to the main
|
||||
// implementation function. In an application that uses marked values,
|
||||
// the Type implementation for a function must always be prepared to accept
|
||||
// marked values, which is easy to achieve by consulting only the type
|
||||
// and ignoring the value itself.
|
||||
AllowMarked bool
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
// Package function builds on the functionality of cty by modeling functions
|
||||
// that operate on cty Values.
|
||||
//
|
||||
// Functions are, at their core, Go anonymous functions. However, this package
|
||||
// wraps around them utility functions for parameter type checking, etc.
|
||||
package function
|
@ -0,0 +1,50 @@
|
||||
package function
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
// ArgError represents an error with one of the arguments in a call. The
|
||||
// attribute Index represents the zero-based index of the argument in question.
|
||||
//
|
||||
// Its error *may* be a cty.PathError, in which case the error actually
|
||||
// pertains to a nested value within the data structure passed as the argument.
|
||||
type ArgError struct {
|
||||
error
|
||||
Index int
|
||||
}
|
||||
|
||||
func NewArgErrorf(i int, f string, args ...interface{}) error {
|
||||
return ArgError{
|
||||
error: fmt.Errorf(f, args...),
|
||||
Index: i,
|
||||
}
|
||||
}
|
||||
|
||||
func NewArgError(i int, err error) error {
|
||||
return ArgError{
|
||||
error: err,
|
||||
Index: i,
|
||||
}
|
||||
}
|
||||
|
||||
// PanicError indicates that a panic occurred while executing either a
|
||||
// function's type or implementation function. This is captured and wrapped
|
||||
// into a normal error so that callers (expected to be language runtimes)
|
||||
// are freed from having to deal with panics in buggy functions.
|
||||
type PanicError struct {
|
||||
Value interface{}
|
||||
Stack []byte
|
||||
}
|
||||
|
||||
func errorForPanic(val interface{}) error {
|
||||
return PanicError{
|
||||
Value: val,
|
||||
Stack: debug.Stack(),
|
||||
}
|
||||
}
|
||||
|
||||
func (e PanicError) Error() string {
|
||||
return fmt.Sprintf("panic in function implementation: %s\n%s", e.Value, e.Stack)
|
||||
}
|
@ -0,0 +1,342 @@
|
||||
package function
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// Function represents a function. This is the main type in this package.
|
||||
type Function struct {
|
||||
spec *Spec
|
||||
}
|
||||
|
||||
// Spec is the specification of a function, used to instantiate
|
||||
// a new Function.
|
||||
type Spec struct {
|
||||
// Params is a description of the positional parameters for the function.
|
||||
// The standard checking logic rejects any calls that do not provide
|
||||
// arguments conforming to this definition, freeing the function
|
||||
// implementer from dealing with such inconsistencies.
|
||||
Params []Parameter
|
||||
|
||||
// VarParam is an optional specification of additional "varargs" the
|
||||
// function accepts. If this is non-nil then callers may provide an
|
||||
// arbitrary number of additional arguments (after those matching with
|
||||
// the fixed parameters in Params) that conform to the given specification,
|
||||
// which will appear as additional values in the slices of values
|
||||
// provided to the type and implementation functions.
|
||||
VarParam *Parameter
|
||||
|
||||
// Type is the TypeFunc that decides the return type of the function
|
||||
// given its arguments, which may be Unknown. See the documentation
|
||||
// of TypeFunc for more information.
|
||||
//
|
||||
// Use StaticReturnType if the function's return type does not vary
|
||||
// depending on its arguments.
|
||||
Type TypeFunc
|
||||
|
||||
// Impl is the ImplFunc that implements the function's behavior.
|
||||
//
|
||||
// Functions are expected to behave as pure functions, and not create
|
||||
// any visible side-effects.
|
||||
//
|
||||
// If a TypeFunc is also provided, the value returned from Impl *must*
|
||||
// conform to the type it returns, or a call to the function will panic.
|
||||
Impl ImplFunc
|
||||
}
|
||||
|
||||
// New creates a new function with the given specification.
|
||||
//
|
||||
// After passing a Spec to this function, the caller must no longer read from
|
||||
// or mutate it.
|
||||
func New(spec *Spec) Function {
|
||||
f := Function{
|
||||
spec: spec,
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// TypeFunc is a callback type for determining the return type of a function
|
||||
// given its arguments.
|
||||
//
|
||||
// Any of the values passed to this function may be unknown, even if the
|
||||
// parameters are not configured to accept unknowns.
|
||||
//
|
||||
// If any of the given values are *not* unknown, the TypeFunc may use the
|
||||
// values for pre-validation and for choosing the return type. For example,
|
||||
// a hypothetical JSON-unmarshalling function could return
|
||||
// cty.DynamicPseudoType if the given JSON string is unknown, but return
|
||||
// a concrete type based on the JSON structure if the JSON string is already
|
||||
// known.
|
||||
type TypeFunc func(args []cty.Value) (cty.Type, error)
|
||||
|
||||
// ImplFunc is a callback type for the main implementation of a function.
|
||||
//
|
||||
// "args" are the values for the arguments, and this slice will always be at
|
||||
// least as long as the argument definition slice for the function.
|
||||
//
|
||||
// "retType" is the type returned from the Type callback, included as a
|
||||
// convenience to avoid the need to re-compute the return type for generic
|
||||
// functions whose return type is a function of the arguments.
|
||||
type ImplFunc func(args []cty.Value, retType cty.Type) (cty.Value, error)
|
||||
|
||||
// StaticReturnType returns a TypeFunc that always returns the given type.
|
||||
//
|
||||
// This is provided as a convenience for defining a function whose return
|
||||
// type does not depend on the argument types.
|
||||
func StaticReturnType(ty cty.Type) TypeFunc {
|
||||
return func([]cty.Value) (cty.Type, error) {
|
||||
return ty, nil
|
||||
}
|
||||
}
|
||||
|
||||
// ReturnType returns the return type of a function given a set of candidate
|
||||
// argument types, or returns an error if the given types are unacceptable.
|
||||
//
|
||||
// If the caller already knows values for at least some of the arguments
|
||||
// it can be better to call ReturnTypeForValues, since certain functions may
|
||||
// determine their return types from their values and return DynamicVal if
|
||||
// the values are unknown.
|
||||
func (f Function) ReturnType(argTypes []cty.Type) (cty.Type, error) {
|
||||
vals := make([]cty.Value, len(argTypes))
|
||||
for i, ty := range argTypes {
|
||||
vals[i] = cty.UnknownVal(ty)
|
||||
}
|
||||
return f.ReturnTypeForValues(vals)
|
||||
}
|
||||
|
||||
// ReturnTypeForValues is similar to ReturnType but can be used if the caller
|
||||
// already knows the values of some or all of the arguments, in which case
|
||||
// the function may be able to determine a more definite result if its
|
||||
// return type depends on the argument *values*.
|
||||
//
|
||||
// For any arguments whose values are not known, pass an Unknown value of
|
||||
// the appropriate type.
|
||||
func (f Function) ReturnTypeForValues(args []cty.Value) (ty cty.Type, err error) {
|
||||
var posArgs []cty.Value
|
||||
var varArgs []cty.Value
|
||||
|
||||
if f.spec.VarParam == nil {
|
||||
if len(args) != len(f.spec.Params) {
|
||||
return cty.Type{}, fmt.Errorf(
|
||||
"wrong number of arguments (%d required; %d given)",
|
||||
len(f.spec.Params), len(args),
|
||||
)
|
||||
}
|
||||
|
||||
posArgs = args
|
||||
varArgs = nil
|
||||
} else {
|
||||
if len(args) < len(f.spec.Params) {
|
||||
return cty.Type{}, fmt.Errorf(
|
||||
"wrong number of arguments (at least %d required; %d given)",
|
||||
len(f.spec.Params), len(args),
|
||||
)
|
||||
}
|
||||
|
||||
posArgs = args[0:len(f.spec.Params)]
|
||||
varArgs = args[len(f.spec.Params):]
|
||||
}
|
||||
|
||||
for i, spec := range f.spec.Params {
|
||||
val := posArgs[i]
|
||||
|
||||
if val.IsMarked() && !spec.AllowMarked {
|
||||
// During type checking we just unmark values and discard their
|
||||
// marks, under the assumption that during actual execution of
|
||||
// the function we'll do similarly and then re-apply the marks
|
||||
// afterwards. Note that this does mean that a function that
|
||||
// inspects values (rather than just types) in its Type
|
||||
// implementation can potentially fail to take into account marks,
|
||||
// unless it specifically opts in to seeing them.
|
||||
unmarked, _ := val.Unmark()
|
||||
newArgs := make([]cty.Value, len(args))
|
||||
copy(newArgs, args)
|
||||
newArgs[i] = unmarked
|
||||
args = newArgs
|
||||
}
|
||||
|
||||
if val.IsNull() && !spec.AllowNull {
|
||||
return cty.Type{}, NewArgErrorf(i, "argument must not be null")
|
||||
}
|
||||
|
||||
// AllowUnknown is ignored for type-checking, since we expect to be
|
||||
// able to type check with unknown values. We *do* still need to deal
|
||||
// with DynamicPseudoType here though, since the Type function might
|
||||
// not be ready to deal with that.
|
||||
|
||||
if val.Type() == cty.DynamicPseudoType {
|
||||
if !spec.AllowDynamicType {
|
||||
return cty.DynamicPseudoType, nil
|
||||
}
|
||||
} else if errs := val.Type().TestConformance(spec.Type); errs != nil {
|
||||
// For now we'll just return the first error in the set, since
|
||||
// we don't have a good way to return the whole list here.
|
||||
// Would be good to do something better at some point...
|
||||
return cty.Type{}, NewArgError(i, errs[0])
|
||||
}
|
||||
}
|
||||
|
||||
if varArgs != nil {
|
||||
spec := f.spec.VarParam
|
||||
for i, val := range varArgs {
|
||||
realI := i + len(posArgs)
|
||||
|
||||
if val.IsMarked() && !spec.AllowMarked {
|
||||
// See the similar block in the loop above for what's going on here.
|
||||
unmarked, _ := val.Unmark()
|
||||
newArgs := make([]cty.Value, len(args))
|
||||
copy(newArgs, args)
|
||||
newArgs[realI] = unmarked
|
||||
args = newArgs
|
||||
}
|
||||
|
||||
if val.IsNull() && !spec.AllowNull {
|
||||
return cty.Type{}, NewArgErrorf(realI, "argument must not be null")
|
||||
}
|
||||
|
||||
if val.Type() == cty.DynamicPseudoType {
|
||||
if !spec.AllowDynamicType {
|
||||
return cty.DynamicPseudoType, nil
|
||||
}
|
||||
} else if errs := val.Type().TestConformance(spec.Type); errs != nil {
|
||||
// For now we'll just return the first error in the set, since
|
||||
// we don't have a good way to return the whole list here.
|
||||
// Would be good to do something better at some point...
|
||||
return cty.Type{}, NewArgError(i, errs[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Intercept any panics from the function and return them as normal errors,
|
||||
// so a calling language runtime doesn't need to deal with panics.
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ty = cty.NilType
|
||||
err = errorForPanic(r)
|
||||
}
|
||||
}()
|
||||
|
||||
return f.spec.Type(args)
|
||||
}
|
||||
|
||||
// Call actually calls the function with the given arguments, which must
|
||||
// conform to the function's parameter specification or an error will be
|
||||
// returned.
|
||||
func (f Function) Call(args []cty.Value) (val cty.Value, err error) {
|
||||
expectedType, err := f.ReturnTypeForValues(args)
|
||||
if err != nil {
|
||||
return cty.NilVal, err
|
||||
}
|
||||
|
||||
// Type checking already dealt with most situations relating to our
|
||||
// parameter specification, but we still need to deal with unknown
|
||||
// values and marked values.
|
||||
posArgs := args[:len(f.spec.Params)]
|
||||
varArgs := args[len(f.spec.Params):]
|
||||
var resultMarks []cty.ValueMarks
|
||||
|
||||
for i, spec := range f.spec.Params {
|
||||
val := posArgs[i]
|
||||
|
||||
if !val.IsKnown() && !spec.AllowUnknown {
|
||||
return cty.UnknownVal(expectedType), nil
|
||||
}
|
||||
|
||||
if val.IsMarked() && !spec.AllowMarked {
|
||||
unwrappedVal, marks := val.Unmark()
|
||||
// In order to avoid additional overhead on applications that
|
||||
// are not using marked values, we copy the given args only
|
||||
// if we encounter a marked value we need to unmark. However,
|
||||
// as a consequence we end up doing redundant copying if multiple
|
||||
// marked values need to be unwrapped. That seems okay because
|
||||
// argument lists are generally small.
|
||||
newArgs := make([]cty.Value, len(args))
|
||||
copy(newArgs, args)
|
||||
newArgs[i] = unwrappedVal
|
||||
resultMarks = append(resultMarks, marks)
|
||||
args = newArgs
|
||||
}
|
||||
}
|
||||
|
||||
if f.spec.VarParam != nil {
|
||||
spec := f.spec.VarParam
|
||||
for i, val := range varArgs {
|
||||
if !val.IsKnown() && !spec.AllowUnknown {
|
||||
return cty.UnknownVal(expectedType), nil
|
||||
}
|
||||
if val.IsMarked() && !spec.AllowMarked {
|
||||
unwrappedVal, marks := val.Unmark()
|
||||
newArgs := make([]cty.Value, len(args))
|
||||
copy(newArgs, args)
|
||||
newArgs[len(posArgs)+i] = unwrappedVal
|
||||
resultMarks = append(resultMarks, marks)
|
||||
args = newArgs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var retVal cty.Value
|
||||
{
|
||||
// Intercept any panics from the function and return them as normal errors,
|
||||
// so a calling language runtime doesn't need to deal with panics.
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
val = cty.NilVal
|
||||
err = errorForPanic(r)
|
||||
}
|
||||
}()
|
||||
|
||||
retVal, err = f.spec.Impl(args, expectedType)
|
||||
if err != nil {
|
||||
return cty.NilVal, err
|
||||
}
|
||||
if len(resultMarks) > 0 {
|
||||
retVal = retVal.WithMarks(resultMarks...)
|
||||
}
|
||||
}
|
||||
|
||||
// Returned value must conform to what the Type function expected, to
|
||||
// protect callers from having to deal with inconsistencies.
|
||||
if errs := retVal.Type().TestConformance(expectedType); errs != nil {
|
||||
panic(fmt.Errorf(
|
||||
"returned value %#v does not conform to expected return type %#v: %s",
|
||||
retVal, expectedType, errs[0],
|
||||
))
|
||||
}
|
||||
|
||||
return retVal, nil
|
||||
}
|
||||
|
||||
// ProxyFunc the type returned by the method Function.Proxy.
|
||||
type ProxyFunc func(args ...cty.Value) (cty.Value, error)
|
||||
|
||||
// Proxy returns a function that can be called with cty.Value arguments
|
||||
// to run the function. This is provided as a convenience for when using
|
||||
// a function directly within Go code.
|
||||
func (f Function) Proxy() ProxyFunc {
|
||||
return func(args ...cty.Value) (cty.Value, error) {
|
||||
return f.Call(args)
|
||||
}
|
||||
}
|
||||
|
||||
// Params returns information about the function's fixed positional parameters.
|
||||
// This does not include information about any variadic arguments accepted;
|
||||
// for that, call VarParam.
|
||||
func (f Function) Params() []Parameter {
|
||||
new := make([]Parameter, len(f.spec.Params))
|
||||
copy(new, f.spec.Params)
|
||||
return new
|
||||
}
|
||||
|
||||
// VarParam returns information about the variadic arguments the function
|
||||
// expects, or nil if the function is not variadic.
|
||||
func (f Function) VarParam() *Parameter {
|
||||
if f.spec.VarParam == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ret := *f.spec.VarParam
|
||||
return &ret
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package function
|
||||
|
||||
import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// Unpredictable wraps a given function such that it retains the same arguments
|
||||
// and type checking behavior but will return an unknown value when called.
|
||||
//
|
||||
// It is recommended that most functions be "pure", which is to say that they
|
||||
// will always produce the same value given particular input. However,
|
||||
// sometimes it is necessary to offer functions whose behavior depends on
|
||||
// some external state, such as reading a file or determining the current time.
|
||||
// In such cases, an unpredictable wrapper might be used to stand in for
|
||||
// the function during some sort of prior "checking" phase in order to delay
|
||||
// the actual effect until later.
|
||||
//
|
||||
// While Unpredictable can support a function that isn't pure in its
|
||||
// implementation, it still expects a function to be pure in its type checking
|
||||
// behavior, except for the special case of returning cty.DynamicPseudoType
|
||||
// if it is not yet able to predict its return value based on current argument
|
||||
// information.
|
||||
func Unpredictable(f Function) Function {
|
||||
newSpec := *f.spec // shallow copy
|
||||
newSpec.Impl = unpredictableImpl
|
||||
return New(&newSpec)
|
||||
}
|
||||
|
||||
func unpredictableImpl(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
||||
return cty.UnknownVal(retType), nil
|
||||
}
|
@ -0,0 +1,204 @@
|
||||
package cty
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/zclconf/go-cty/cty/set"
|
||||
)
|
||||
|
||||
// GobEncode is an implementation of the gob.GobEncoder interface, which
|
||||
// allows Values to be included in structures encoded with encoding/gob.
|
||||
//
|
||||
// Currently it is not possible to represent values of capsule types in gob,
|
||||
// because the types themselves cannot be represented.
|
||||
func (val Value) GobEncode() ([]byte, error) {
|
||||
if val.IsMarked() {
|
||||
return nil, errors.New("value is marked")
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
enc := gob.NewEncoder(buf)
|
||||
|
||||
gv := gobValue{
|
||||
Version: 0,
|
||||
Ty: val.ty,
|
||||
V: val.v,
|
||||
}
|
||||
|
||||
err := enc.Encode(gv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error encoding cty.Value: %s", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// GobDecode is an implementation of the gob.GobDecoder interface, which
|
||||
// inverts the operation performed by GobEncode. See the documentation of
|
||||
// GobEncode for considerations when using cty.Value instances with gob.
|
||||
func (val *Value) GobDecode(buf []byte) error {
|
||||
r := bytes.NewReader(buf)
|
||||
dec := gob.NewDecoder(r)
|
||||
|
||||
var gv gobValue
|
||||
err := dec.Decode(&gv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding cty.Value: %s", err)
|
||||
}
|
||||
if gv.Version != 0 {
|
||||
return fmt.Errorf("unsupported cty.Value encoding version %d; only 0 is supported", gv.Version)
|
||||
}
|
||||
|
||||
// Because big.Float.GobEncode is implemented with a pointer reciever,
|
||||
// gob encoding of an interface{} containing a *big.Float value does not
|
||||
// round-trip correctly, emerging instead as a non-pointer big.Float.
|
||||
// The rest of cty expects all number values to be represented by
|
||||
// *big.Float, so we'll fix that up here.
|
||||
gv.V = gobDecodeFixNumberPtr(gv.V, gv.Ty)
|
||||
|
||||
val.ty = gv.Ty
|
||||
val.v = gv.V
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GobEncode is an implementation of the gob.GobEncoder interface, which
|
||||
// allows Types to be included in structures encoded with encoding/gob.
|
||||
//
|
||||
// Currently it is not possible to represent capsule types in gob.
|
||||
func (t Type) GobEncode() ([]byte, error) {
|
||||
buf := &bytes.Buffer{}
|
||||
enc := gob.NewEncoder(buf)
|
||||
|
||||
gt := gobType{
|
||||
Version: 0,
|
||||
Impl: t.typeImpl,
|
||||
}
|
||||
|
||||
err := enc.Encode(gt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error encoding cty.Type: %s", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// GobDecode is an implementatino of the gob.GobDecoder interface, which
|
||||
// reverses the encoding performed by GobEncode to allow types to be recovered
|
||||
// from gob buffers.
|
||||
func (t *Type) GobDecode(buf []byte) error {
|
||||
r := bytes.NewReader(buf)
|
||||
dec := gob.NewDecoder(r)
|
||||
|
||||
var gt gobType
|
||||
err := dec.Decode(>)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding cty.Type: %s", err)
|
||||
}
|
||||
if gt.Version != 0 {
|
||||
return fmt.Errorf("unsupported cty.Type encoding version %d; only 0 is supported", gt.Version)
|
||||
}
|
||||
|
||||
t.typeImpl = gt.Impl
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Capsule types cannot currently be gob-encoded, because they rely on pointer
|
||||
// equality and we have no way to recover the original pointer on decode.
|
||||
func (t *capsuleType) GobEncode() ([]byte, error) {
|
||||
return nil, fmt.Errorf("cannot gob-encode capsule type %q", t.FriendlyName(friendlyTypeName))
|
||||
}
|
||||
|
||||
func (t *capsuleType) GobDecode() ([]byte, error) {
|
||||
return nil, fmt.Errorf("cannot gob-decode capsule type %q", t.FriendlyName(friendlyTypeName))
|
||||
}
|
||||
|
||||
type gobValue struct {
|
||||
Version int
|
||||
Ty Type
|
||||
V interface{}
|
||||
}
|
||||
|
||||
type gobType struct {
|
||||
Version int
|
||||
Impl typeImpl
|
||||
}
|
||||
|
||||
type gobCapsuleTypeImpl struct {
|
||||
}
|
||||
|
||||
// goDecodeFixNumberPtr fixes an unfortunate quirk of round-tripping cty.Number
|
||||
// values through gob: the big.Float.GobEncode method is implemented on a
|
||||
// pointer receiver, and so it loses the "pointer-ness" of the value on
|
||||
// encode, causing the values to emerge the other end as big.Float rather than
|
||||
// *big.Float as we expect elsewhere in cty.
|
||||
//
|
||||
// The implementation of gobDecodeFixNumberPtr mutates the given raw value
|
||||
// during its work, and may either return the same value mutated or a new
|
||||
// value. Callers must no longer use whatever value they pass as "raw" after
|
||||
// this function is called.
|
||||
func gobDecodeFixNumberPtr(raw interface{}, ty Type) interface{} {
|
||||
// Unfortunately we need to work recursively here because number values
|
||||
// might be embedded in structural or collection type values.
|
||||
|
||||
switch {
|
||||
case ty.Equals(Number):
|
||||
if bf, ok := raw.(big.Float); ok {
|
||||
return &bf // wrap in pointer
|
||||
}
|
||||
case ty.IsMapType() && ty.ElementType().Equals(Number):
|
||||
if m, ok := raw.(map[string]interface{}); ok {
|
||||
for k, v := range m {
|
||||
m[k] = gobDecodeFixNumberPtr(v, ty.ElementType())
|
||||
}
|
||||
}
|
||||
case ty.IsListType() && ty.ElementType().Equals(Number):
|
||||
if s, ok := raw.([]interface{}); ok {
|
||||
for i, v := range s {
|
||||
s[i] = gobDecodeFixNumberPtr(v, ty.ElementType())
|
||||
}
|
||||
}
|
||||
case ty.IsSetType() && ty.ElementType().Equals(Number):
|
||||
if s, ok := raw.(set.Set); ok {
|
||||
newS := set.NewSet(s.Rules())
|
||||
for it := s.Iterator(); it.Next(); {
|
||||
newV := gobDecodeFixNumberPtr(it.Value(), ty.ElementType())
|
||||
newS.Add(newV)
|
||||
}
|
||||
return newS
|
||||
}
|
||||
case ty.IsObjectType():
|
||||
if m, ok := raw.(map[string]interface{}); ok {
|
||||
for k, v := range m {
|
||||
aty := ty.AttributeType(k)
|
||||
m[k] = gobDecodeFixNumberPtr(v, aty)
|
||||
}
|
||||
}
|
||||
case ty.IsTupleType():
|
||||
if s, ok := raw.([]interface{}); ok {
|
||||
for i, v := range s {
|
||||
ety := ty.TupleElementType(i)
|
||||
s[i] = gobDecodeFixNumberPtr(v, ety)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return raw
|
||||
}
|
||||
|
||||
// gobDecodeFixNumberPtrVal is a helper wrapper around gobDecodeFixNumberPtr
|
||||
// that works with already-constructed values. This is primarily for testing,
|
||||
// to fix up intentionally-invalid number values for the parts of the test
|
||||
// code that need them to be valid, such as calling GoString on them.
|
||||
func gobDecodeFixNumberPtrVal(v Value) Value {
|
||||
raw := gobDecodeFixNumberPtr(v.v, v.ty)
|
||||
return Value{
|
||||
v: raw,
|
||||
ty: v.ty,
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue