Skip to content

Commit

Permalink
feat: add datasource url_components
Browse files Browse the repository at this point in the history
  • Loading branch information
gmeligio committed Apr 9, 2024
1 parent 04bb6bc commit 606e09a
Show file tree
Hide file tree
Showing 12 changed files with 495 additions and 29 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,8 @@ In order to run the full suite of Acceptance tests, run `make testacc`.
```shell
make testacc
```

### TODO

1. Migrate to GitHub action [ghaction-terraform-provider-release](https://github.com/hashicorp/ghaction-terraform-provider-release)
1. Refer in documentation: [What is a URL](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Web_mechanics/What_is_a_URL)
36 changes: 36 additions & 0 deletions docs/data-sources/components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "url_components Data Source - url"
subcategory: ""
description: |-
Parses URL components from a URL string.
---

# url_components (Data Source)

Parses URL components from a URL string.



<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `url` (String) The URL to parse

### Read-Only

- `authority` (String) The concatenation of the username, password, host, and port. It's separated from the scheme by :// . For example: user1:[email protected]:3000 for http://user1:[email protected]:3000
- `credentials` (String) The concatenation of the username and password. For example: user1:123 for https://user1:[email protected]
- `fragment` (String) The URL component after the search. For example: section for https://example.com/path/to/resource?key=value#section
- `hash` (String) The concatenation of a # with the fragment. For example: #section for https://example.com/path/to/resource?key=value#section
- `host` (String) The domain part of the authority. For example: example.com for https://example.com
- `password` (String) The second component of the URL credentials. For example: 123 for https://user1:[email protected]
- `path` (String) The URL component after the authority. For example: /path/to/resource for https://example.com/path/to/resource
- `port` (String) The last component of the URL authority. For example: 443 for https://example.com:443
- `protocol` (String) The concatenation of the scheme and the port. For example: http:, https:, ftp:, sftp:, file:, etc.
- `query` (String) The URL component of the search starting at the ? and before the fragment. For example: key=value for https://example.com/path/to/resource?key=value#section
- `scheme` (String) The protocol used to access the domain. For example: http, https, ftp, sftp, file, etc.
- `search` (String) The URL component after the path. For example: ?key=value for https://example.com/path/to/resource?key=value
- `username` (String) The first component of the URL credentials. For example: user1 for https://user1:[email protected]
17 changes: 17 additions & 0 deletions examples/data_sources/url_components/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
terraform {
required_providers {
url = {
source = "registry.terraform.io/gmeligio/url"
}
}
}

provider "url" {}

data "url_components" "example" {
url = "https://abc:[email protected]:45/path/to/somewhere?foo=bar&baz=qux#231"
}

output "components" {
value = data.url_components.example
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ require (
github.com/hashicorp/terraform-plugin-docs v0.18.0
github.com/hashicorp/terraform-plugin-framework v1.7.0
github.com/hashicorp/terraform-plugin-go v0.22.1
github.com/hashicorp/terraform-plugin-log v0.9.0
github.com/hashicorp/terraform-plugin-testing v1.7.0
golang.org/x/net v0.21.0
)
Expand Down Expand Up @@ -41,6 +40,7 @@ require (
github.com/hashicorp/logutils v1.0.0 // indirect
github.com/hashicorp/terraform-exec v0.20.0 // indirect
github.com/hashicorp/terraform-json v0.21.0 // indirect
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 // indirect
github.com/hashicorp/terraform-registry-address v0.2.3 // indirect
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
Expand Down
170 changes: 170 additions & 0 deletions internal/provider/components.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package provider

import (
"context"

"net/url"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
)

// componentsDataSourceModel describes the data source data model.
// References used.
// https://registry.terraform.io/modules/matti/urlparse/external/latest
// https://registry.terraform.io/providers/northwood-labs/corefunc/latest/docs/data-sources/url_parse
type componentsDataSourceModel struct {
Url types.String `tfsdk:"url"`
Authority types.String `tfsdk:"authority"`
Protocol types.String `tfsdk:"protocol"`
Scheme types.String `tfsdk:"scheme"`
Credentials types.String `tfsdk:"credentials"`
Username types.String `tfsdk:"username"`
Password types.String `tfsdk:"password"`
Host types.String `tfsdk:"host"`
Port types.String `tfsdk:"port"`
Path types.String `tfsdk:"path"`
Search types.String `tfsdk:"search"`
Query types.String `tfsdk:"query"`
Hash types.String `tfsdk:"hash"`
Fragment types.String `tfsdk:"fragment"`
}

func (d componentsDataSourceModel) validate(_ context.Context) diag.Diagnostics {
var diags diag.Diagnostics

// if d.Host.IsUnknown() || d.Host.IsNull() {
// return diags
// }

// host := d.Host.ValueString()

// eTLD, icann := publicsuffix.PublicSuffix(host)

// manager := findManager(icann, eTLD)

// if manager == "None" {
// diags.AddAttributeError(
// path.Root("host"),
// "Invalid Attribute Configuration",
// "Expected host to have as a manager either ICANN or Private.",
// )
// }

return diags
}

func (d *componentsDataSourceModel) update(_ context.Context) diag.Diagnostics {
var diags diag.Diagnostics

// host := d.Host.ValueString()

// eTLD, icann := publicsuffix.PublicSuffix(host)
// d.TLD = types.StringValue(eTLD)

// sld, err := extractSld(host, eTLD)
// if err != nil {
// diags.AddAttributeError(
// path.Root("sld"),
// "Invalid Attribute Configuration",
// err.Error(),
// )
// }
// d.SLD = types.StringValue(sld)

// domain := sld + "." + eTLD
// d.Domain = types.StringValue(domain)

// manager := findManager(icann, eTLD)
// d.Manager = types.StringValue(manager)

// subdomain := extractSubdomain(host, domain)
// d.Subdomain = types.StringValue(subdomain)

rawURL := d.Url.ValueString()

parsed, err := url.Parse(rawURL)
if err != nil {
diags.AddError(
"Invalid URL",
err.Error(),
)
return diags
}

authority := renderAuthority(parsed)
d.Authority = types.StringValue(authority)

scheme := parsed.Scheme
d.Scheme = types.StringValue(scheme)

protocol := scheme + ":"
d.Protocol = types.StringValue(protocol)

credentials := parsed.User.String()
d.Credentials = types.StringValue(credentials)

username := parsed.User.Username()
d.Username = types.StringValue(username)

password, _ := parsed.User.Password()
d.Password = types.StringValue(password)

host := parsed.Hostname()
d.Host = types.StringValue(host)

port := parsed.Port()
d.Port = types.StringValue(port)

path := parsed.Path
d.Path = types.StringValue(path)

search := renderSearch(parsed)
d.Search = types.StringValue(search)

query := parsed.RawQuery
d.Query = types.StringValue(query)

fragment := parsed.Fragment
d.Fragment = types.StringValue(fragment)

hash := renderHash(parsed)
d.Hash = types.StringValue(hash)

return diags
}

func renderHash(parsed *url.URL) string {
fragment := parsed.Fragment
if fragment == "" {
return ""
}

return "#" + fragment
}

func renderSearch(parsed *url.URL) string {
query := parsed.RawQuery
if query == "" {
return ""
}

return "?" + query
}

func renderAuthority(parsed *url.URL) string {
credentials := parsed.User.String()
port := parsed.Port()

var credentialsComponent string
if credentials != "" {
credentialsComponent = credentials + "@"
}

var portComponent string
if port != "" {
portComponent = ":" + port
}

return credentialsComponent + parsed.Hostname() + portComponent
}
119 changes: 119 additions & 0 deletions internal/provider/components_data_source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package provider

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
)

// Ensure provider defined types fully satisfy framework interfaces.
var _ datasource.DataSource = &componentsDataSource{}

func NewComponentsDataSource() datasource.DataSource {
return &componentsDataSource{}
}

// componentsDataSource defines the data source implementation.
type componentsDataSource struct {
}

func (c *componentsDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_components"
}

func (c *componentsDataSource) ValidateConfig(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) {
var data componentsDataSourceModel

diags := req.Config.Get(ctx, &data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

diags = data.validate(ctx)
resp.Diagnostics.Append(diags...)
}

func (c *componentsDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Parses URL components from a URL string.",

Attributes: map[string]schema.Attribute{
"url": schema.StringAttribute{
MarkdownDescription: "The URL to parse",
Required: true,
},
"authority": schema.StringAttribute{
MarkdownDescription: "The concatenation of the username, password, host, and port. It's separated from the scheme by :// . For example: user1:[email protected]:3000 for http://user1:[email protected]:3000",
Computed: true,
},
"scheme": schema.StringAttribute{
MarkdownDescription: "The protocol used to access the domain. For example: http, https, ftp, sftp, file, etc.",
Computed: true,
},
"protocol": schema.StringAttribute{
MarkdownDescription: "The concatenation of the scheme and the port. For example: http:, https:, ftp:, sftp:, file:, etc.",
Computed: true,
},
"credentials": schema.StringAttribute{
MarkdownDescription: "The concatenation of the username and password. For example: user1:123 for https://user1:[email protected]",
Computed: true,
},
"username": schema.StringAttribute{
MarkdownDescription: "The first component of the URL credentials. For example: user1 for https://user1:[email protected]",
Computed: true,
},
"password": schema.StringAttribute{
MarkdownDescription: "The second component of the URL credentials. For example: 123 for https://user1:[email protected]",
Computed: true,
},
"host": schema.StringAttribute{
MarkdownDescription: "The domain part of the authority. For example: example.com for https://example.com",
Computed: true,
},
"port": schema.StringAttribute{
MarkdownDescription: "The last component of the URL authority. For example: 443 for https://example.com:443",
Computed: true,
},
"path": schema.StringAttribute{
MarkdownDescription: "The URL component after the authority. For example: /path/to/resource for https://example.com/path/to/resource",
Computed: true,
},
"search": schema.StringAttribute{
MarkdownDescription: "The URL component after the path. For example: ?key=value for https://example.com/path/to/resource?key=value",
Computed: true,
},
"query": schema.StringAttribute{
MarkdownDescription: "The URL component of the search starting at the ? and before the fragment. For example: key=value for https://example.com/path/to/resource?key=value#section",
Computed: true,
},
"fragment": schema.StringAttribute{
MarkdownDescription: "The URL component after the search. For example: section for https://example.com/path/to/resource?key=value#section",
Computed: true,
},
"hash": schema.StringAttribute{
MarkdownDescription: "The concatenation of a # with the fragment. For example: #section for https://example.com/path/to/resource?key=value#section",
Computed: true,
},
},
}
}

func (c *componentsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data componentsDataSourceModel

diags := req.Config.Get(ctx, &data)

resp.Diagnostics.Append(diags...)

if resp.Diagnostics.HasError() {
return
}

diags = data.update(ctx)
resp.Diagnostics.Append(diags...)

diags = resp.State.Set(ctx, &data)
resp.Diagnostics.Append(diags...)
}
Loading

0 comments on commit 606e09a

Please sign in to comment.