basic_url 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7f10e56f551ec196327c0a2976c49e36a68270ac1bfebee851cd1780916f61d7
4
+ data.tar.gz: 26c1b5b16d7ced8dffc962fbaab4f332a3cb958a7f57dd1197e2a98d06e1cb8e
5
+ SHA512:
6
+ metadata.gz: faa9b663146901b711854535d862218f3d3fa75b08d10c2b0bc0a4d54c38b731aa917ad9f558555df1ac9fa1f3604df45a7a057fcbb3c7f1bb85d59835bc5ed3
7
+ data.tar.gz: 0f292a5a51c9ffb75cb451417e590fd6a339c350127058b046f9c6038b25fb49855b4f87cfa686504bb9d3987faef9fb8f4fb7a24fc3f2a65b3ee030296e84f0
data/.rubocop.yml ADDED
@@ -0,0 +1,68 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.2
3
+ NewCops: enable
4
+ SuggestExtensions: false
5
+
6
+ Layout/LineLength:
7
+ Max: 180
8
+
9
+ # Just turn off the metrics cops. Long lines and methods are fine.
10
+ # This project prefers large, logical blocks to "papering the wall with post-its"
11
+ Metrics:
12
+ Enabled: false
13
+
14
+ # 'exc' is just as good, if not better, as 'e'
15
+ Naming/RescuedExceptionsVariableName:
16
+ Enabled: false
17
+
18
+ # 123_456 doesn't add value IMO.
19
+ NumericLiterals:
20
+ Enabled: false
21
+
22
+ # Favor explicit over implicit code: don't complain of "redundant returns"
23
+ RedundantReturn:
24
+ Enabled: false
25
+
26
+ Style/ArgumentsForwarding:
27
+ UseAnonymousForwarding: false
28
+
29
+ # A "This class manages the App Config" comment on App::Config has no value.
30
+ Style/Documentation:
31
+ Enabled: false
32
+
33
+ # This cop can result in incompatible code
34
+ Style/EmptyLiteral:
35
+ Enabled: false
36
+
37
+ # Ruby 3.0 upgrade cop
38
+ # Literally recommends "magic" code.
39
+ Style/FrozenStringLiteralComment:
40
+ Enabled: false
41
+
42
+ Style/HashSyntax:
43
+ # Explicit over implicit code
44
+ EnforcedShorthandSyntax: never
45
+
46
+ # value == 0 is fine, value.zero? is syntactic nonsense.
47
+ Style/NumericPredicate:
48
+ Enabled: false
49
+
50
+ # Not adding parenthesis can introduce bugs in complex conditions
51
+ Style/ParenthesesAroundCondition:
52
+ Enabled: false
53
+
54
+ # Not adding parenthesis can introduce bugs in complex conditions
55
+ Style/RedundantParentheses:
56
+ Enabled: false
57
+
58
+ # Do not remove explicit range ends
59
+ Style/SlicingWithRange:
60
+ Enabled: false
61
+
62
+ # Allow $? instead of $CHILD_STATUS (which isn't native)
63
+ Style/SpecialGlobalVars:
64
+ Enabled: false
65
+
66
+ # Enforce trailing commas on multiline hashes. This reduces developer error.
67
+ Style/TrailingCommaInHashLiteral:
68
+ EnforcedStyleForMultiline: comma
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.2.3
data/.versions ADDED
@@ -0,0 +1,6 @@
1
+ # DO NOT EDIT. This file is automatically generated
2
+ # This project is versioned with Semantic Versioning: https://semver.org/
3
+ # Use ./cicd/manage_version.sh to increment the major or minor versions (Supports -h for help).
4
+ # This file contains MAJOR.MINOR, PATCH is incremented by the build system to prevent overwriting tags.
5
+ 0.1.0 6ce56b44c6ba4e20f4b234a29ce9615d3e805ab0
6
+ 0.1.1 6c32353c8e1a8162c700b53a82582d020e1dda8b
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ BasicUrl Changelog
2
+ ==================
3
+
4
+ 0.1.0
5
+ -----
6
+
7
+ Initial Release
data/LICENSE.md ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
data/Makefile ADDED
@@ -0,0 +1,14 @@
1
+ all: test lint
2
+
3
+ build: test lint
4
+ ./cicd/build_gem.sh
5
+
6
+ lint:
7
+ bundle exec rubocop
8
+
9
+ push:
10
+ ./cicd/push_gem.sh
11
+
12
+ test:
13
+ bundle exec rspec
14
+
data/README.md ADDED
@@ -0,0 +1,72 @@
1
+ BasicUrl
2
+ ========
3
+
4
+ A simple Ruby module that provides basic URL operations.
5
+
6
+ This implementation supports:
7
+
8
+ - Object oriented operations (`obj.join(component)` instead of `MODULE.join(obj, component)`)
9
+ - Easy URL encoding
10
+ - Joining relative paths (`obj('a/b').join('c')` returns `a/b/c` not `a/c`)
11
+
12
+ This gem *does not try to be RFC compliant when joining URLs.*
13
+
14
+ Given that the majority of other implementations all do some degree of shenanigans when joining URLs it's assumed that `a/b/c` + `d` = `a/d` is buried in a spec somewhere.
15
+ This library does not do that, it does `a/b/c` + `d` = `a/b/c/d`, which is what is needed in 99% of cases.
16
+ If you need full RFC compliance this gem is not suitable.
17
+
18
+
19
+ ## Usage
20
+
21
+ ```ruby
22
+ require 'BasicURL'
23
+
24
+ base_url = BasicUrl.parse('192.0.2.64/api/v1', default_protocol: 'https')
25
+
26
+ puts(base_url.path)
27
+ # api/v1
28
+
29
+ # Basic joining, returns a new object
30
+ # join! is also available to update the current object
31
+ controller = base_url.join('some/controller')
32
+ puts controller.path
33
+ # api/v1/some/controller
34
+
35
+ # By default absolute paths replace
36
+ controller = base_url.join('/some/controller')
37
+ puts controller.path
38
+ # some/controller
39
+
40
+ # But this is selectable
41
+ controller = base_url.join('/some/controller', replace_when_absolute: false)
42
+ puts controller.path
43
+ # api/v1/some/controller
44
+
45
+ # To get a URL string call .to_s
46
+ puts controller.to_s
47
+ # https://192.0.2.64/api/v1/some/controller
48
+
49
+ # Path components are URL encoded by default
50
+ puts BasicUrl.new(protocol: 'http', host: 'foo.local', path: '/api/v0/some path/with spaces', params: { key: 'value:1', key2: 'Value!', key3: ['and', 'arrays']}).to_s
51
+ # http://foo.local/api/v0/some+path/with+spaces?key=value%3A1&key2=Value%21&key3[]=and&key3[]=arrays
52
+
53
+ # Query parameters can also be added with params=. This modifies the object
54
+ controller = base_url.join('some/controller')
55
+
56
+ controller.params = { key1: 'val1' }
57
+ controller.params[:key2] = 'val2'
58
+ puts controller.to_s
59
+ # https://192.0.2.64/api/v1/some/controller?key1=val1&key2=val2
60
+ ```
61
+
62
+ ## Contributing
63
+
64
+ All support is through Github issues.
65
+ Questions, bug reports and pull requests are accepted at https://github.com/TJNII/basic_url.
66
+
67
+ This is an opinionated project, the author was finally annoyed enough by using Pathname to join URL components to publish this.
68
+ This project will not support the `a + b + c + d = d` behavior of other implementations, if you require that perhaps Adressable::URI would be better.
69
+
70
+ For the quickest response please add a test case.
71
+
72
+ Hope this helped you, have a good day.
data/basic_url.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = 'basic_url'
3
+ spec.version = `./cicd/current_gem_version.sh`
4
+ spec.authors = ['Tom Noonan II']
5
+ spec.email = ['tom@tjnii.com']
6
+ spec.licenses = ['Apache-2.0']
7
+
8
+ spec.summary = 'A Basic URL object that supports common URL operations'
9
+ spec.description = 'Implements a simple URL object supporting object oriented paradigms, basic as-you-expect path joins, and native URLencoding.'
10
+ spec.homepage = 'https://github.com/TJNII/basic_url'
11
+ spec.required_ruby_version = '>= 3.2.0'
12
+
13
+ # spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
14
+
15
+ spec.metadata['homepage_uri'] = 'https://github.com/TJNII/basic_url'
16
+ spec.metadata['source_code_uri'] = 'https://github.com/TJNII/basic_url'
17
+ spec.metadata['changelog_uri'] = 'https://github.com/TJNII/basic_url/blob/master/CHANGELOG.md'
18
+ spec.metadata['rubygems_mfa_required'] = 'true'
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(__dir__) do
23
+ `git ls-files -z`.split("\x0").reject do |f|
24
+ (File.expand_path(f) == __FILE__) ||
25
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile])
26
+ end
27
+ end
28
+ spec.bindir = 'exe'
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ['lib']
31
+ end
@@ -0,0 +1,44 @@
1
+ #!/bin/bash
2
+ # Common version handling for ci/cd scripts
3
+ # https://semver.org/
4
+
5
+ set -euo pipefail
6
+
7
+ SCRIPT_DIR=$(dirname "$0")
8
+ SOURCE_DIR=$(dirname "${SCRIPT_DIR}")
9
+
10
+ if [ -n "$(git status -s "${SOURCE_DIR}")" ]; then
11
+ SEMVER_PREFIXED_PRERELEASE="-dirty"
12
+ REPO_CLEAN=false
13
+ else
14
+ SEMVER_PREFIXED_PRERELEASE=""
15
+ REPO_CLEAN=true
16
+ fi
17
+
18
+ PRIMARY_VERSION_FILE="${SOURCE_DIR}/.versions"
19
+ if [ ! -f "${PRIMARY_VERSION_FILE}" ]; then
20
+ echo >&2 "ERROR: Version file ${PRIMARY_VERSION_FILE} not found"
21
+ echo "FAULT"
22
+ exit 1
23
+ fi
24
+
25
+ LAST_VERSION=$(grep "^[0-9]\+\.[0-9]\+\.[0-9]\+[[:space:]]\+[0-9a-f]\+$" "${PRIMARY_VERSION_FILE}" | tail -n 1)
26
+
27
+ SEMVER_VERSION_MAJOR=$(echo "$LAST_VERSION" | awk '{print $1}' | cut -f 1 -d .)
28
+ SEMVER_VERSION_MINOR=$(echo "$LAST_VERSION" | awk '{print $1}' | cut -f 2 -d .)
29
+ SEMVER_VERSION_PATCH=$(echo "$LAST_VERSION" | awk '{print $1}' | cut -f 3 -d .)
30
+ VERSION_LAST_SHA=$(echo "$LAST_VERSION" | awk '{print $2}')
31
+
32
+ # Patch is the count of commits since the last Version file line sha
33
+ # ~1 is because git omits the trailing newline when outputting to a pipe, making it imposible to tell 0 commits from 1 with wc alone,
34
+ # And to make sure something is output to avoid a pipefail
35
+ # The grep ensures each line has a newline.
36
+ SEMVER_NEW_VERSION_PATCH=$(git log --pretty=format:"%H" "${VERSION_LAST_SHA}"~1.. "${SOURCE_DIR}" | wc -l)
37
+
38
+ if [ $SEMVER_NEW_VERSION_PATCH -lt $SEMVER_VERSION_PATCH -a $REPO_CLEAN == "true" ]; then
39
+ echo >&2 "ERROR: Git patch version $SEMVER_NEW_VERSION_PATCH less than released patch version $SEMVER_VERSION_PATCH"
40
+ echo ERROR
41
+ exit 1
42
+ fi
43
+
44
+ CUR_SEMVER="${SEMVER_VERSION_MAJOR}.${SEMVER_VERSION_MINOR}.${SEMVER_VERSION_PATCH}"
data/cicd/build_gem.sh ADDED
@@ -0,0 +1,11 @@
1
+ #!/bin/bash
2
+ # Pulled out of the Makefile because Make's variable handling is screwball.
3
+
4
+ set -euo pipefail
5
+
6
+ ./cicd/manage_version.sh -r
7
+
8
+ out_version=$(./cicd/current_gem_version.sh)
9
+ out_file="out/basic_url.$(./cicd/current_gem_version.sh).gem"
10
+
11
+ gem build -o "${out_file}"
@@ -0,0 +1,9 @@
1
+ #!/bin/bash
2
+
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR=$(dirname "$0")
6
+ source "${SCRIPT_DIR}/_version_common.sh"
7
+
8
+ echo "$CUR_SEMVER"
9
+ exit 0
@@ -0,0 +1,99 @@
1
+ #!/bin/bash
2
+ # This is helper utility for bumping project release versions.
3
+ # Intended to be run interactively as part of PR generation.
4
+
5
+ set -euo pipefail
6
+
7
+ SCRIPT_DIR=$(dirname "$0")
8
+ source "${SCRIPT_DIR}/_version_common.sh"
9
+
10
+ function show_help {
11
+ echo "$0: Manage project version"
12
+ echo " -c: Show if repo is clean"
13
+ echo " -m: Increment minor"
14
+ echo " -M: Increment major"
15
+ echo " -r: Cut release version"
16
+ echo " -s: Show current full version"
17
+ echo ""
18
+ echo "Patch, pre-release, and build are automatic."
19
+ echo "Major/minor bumps are written to ${PRIMARY_VERSION_FILE} which needs to be comitted into the repo."
20
+ exit 0
21
+ }
22
+
23
+ ACTION="default"
24
+
25
+ while getopts ":chMmrs" arg; do
26
+ case $arg in
27
+ h)
28
+ show_help
29
+ ;;
30
+ c)
31
+ echo "${REPO_CLEAN}"
32
+ exit 0
33
+ ;;
34
+ s)
35
+ echo "${CUR_SEMVER}"
36
+ exit 0
37
+ ;;
38
+ M)
39
+ ACTION="inc_major"
40
+ ;;
41
+ m)
42
+ ACTION="inc_minor"
43
+ ;;
44
+ r)
45
+ ACTION="release"
46
+ ;;
47
+ *)
48
+ echo "INTERNAL ERROR: option fallthrough"
49
+ exit 1
50
+ ;;
51
+ esac
52
+ done
53
+
54
+ if [ $ACTION == default ]; then
55
+ show_help
56
+ fi
57
+
58
+ if [ ${REPO_CLEAN} != true ]; then
59
+ echo "ERROR: Repo git state is dirty"
60
+ echo "Version automation is git sha based, cannot version uncomitted files."
61
+ echo "Clean git state and retry."
62
+ exit 1
63
+ fi
64
+
65
+ case $ACTION in
66
+ inc_major)
67
+ NEW_SEMVER_VERSION_MAJOR=$((${SEMVER_VERSION_MAJOR} + 1))
68
+ NEW_SEMVER_VERSION_MINOR=0
69
+ ;;
70
+ inc_minor)
71
+ NEW_SEMVER_VERSION_MAJOR=${SEMVER_VERSION_MAJOR}
72
+ NEW_SEMVER_VERSION_MINOR=$((${SEMVER_VERSION_MINOR} + 1))
73
+ ;;
74
+ release)
75
+ NEW_SEMVER_VERSION_MAJOR=${SEMVER_VERSION_MAJOR}
76
+ NEW_SEMVER_VERSION_MINOR=${SEMVER_VERSION_MINOR}
77
+ ;;
78
+ *)
79
+ echo "INTERNAL ERROR: Action fallthrough"
80
+ exit 1
81
+ ;;
82
+ esac
83
+
84
+ SOURCE_GIT_SHA=$(git log --pretty=format:"%H" -1 "${SOURCE_DIR}")
85
+ NEW_SEMVER="${NEW_SEMVER_VERSION_MAJOR}.${NEW_SEMVER_VERSION_MINOR}.${SEMVER_NEW_VERSION_PATCH}"
86
+
87
+ if [ "${NEW_SEMVER}" == "${CUR_SEMVER}" ]; then
88
+ if [ "${ACTION}" == "RELEASE" ]; then
89
+ echo "No change: Version ${CUR_SEMVER}"
90
+ exit 0
91
+ else
92
+ echo "ERROR: Version ${CUR_SEMVER} failed to increment"
93
+ exit 1
94
+ fi
95
+ fi
96
+
97
+ echo "Incrementing version from ${CUR_SEMVER} to ${NEW_SEMVER} on commit ${SOURCE_GIT_SHA}"
98
+ echo -e "${NEW_SEMVER}\t${SOURCE_GIT_SHA}" >> "${PRIMARY_VERSION_FILE}"
99
+ exit 0
data/cicd/push_gem.sh ADDED
@@ -0,0 +1,10 @@
1
+ #!/bin/bash
2
+ # Pulled out of the Makefile because Make's variable handling is screwball.
3
+
4
+ set -euo pipefail
5
+
6
+ out_version=$(./cicd/current_gem_version.sh)
7
+ out_file="out/basic_url.$(./cicd/current_gem_version.sh).gem"
8
+
9
+ gem push --otp "${out_file}"
10
+ git tag "${out_version}"
@@ -0,0 +1,27 @@
1
+ # Licensed under the Apache 2 License
2
+ # (C)2024 Tom Noonan II
3
+
4
+ class BasicUrl
5
+ module Errors
6
+ class BasicUrlError < StandardError
7
+ end
8
+
9
+ class InternalError < StandardError
10
+ end
11
+
12
+ #
13
+ # Input Validation Errors
14
+ #
15
+ class ValidationFault < StandardError
16
+ end
17
+
18
+ class ComponentTypeError < ValidationFault
19
+ end
20
+
21
+ class InvalidComponent < ValidationFault
22
+ end
23
+
24
+ class InvalidURL < ValidationFault
25
+ end
26
+ end
27
+ end
data/lib/basic_url.rb ADDED
@@ -0,0 +1,270 @@
1
+ # Licensed under the Apache 2 License
2
+ # (C)2024 Tom Noonan II
3
+
4
+ require 'uri'
5
+ require_relative 'basic_url/errors'
6
+
7
+ class BasicUrl
8
+ def self.urldecode_component(input)
9
+ return nil if input.nil?
10
+
11
+ return URI.decode_www_form_component(input)
12
+ end
13
+
14
+ def self.parse(input, **kwargs)
15
+ input = input.downcase
16
+
17
+ # URI.parse does all kinds of oddball grouping if it doesn't have a protocol
18
+ # Parse the protocol out ourself, injecting a placeholder if needed
19
+ proto_match = input.match(%r{^(?<proto>[a-z0-9]+)://})
20
+ if proto_match
21
+ proto = proto_match[:proto]
22
+ else
23
+ proto = nil
24
+ input = "DUMMY://#{input}"
25
+ end
26
+
27
+ begin
28
+ uri_obj = URI.parse(input)
29
+ rescue StandardError => exc
30
+ raise(Errors::InvalidURL, "Failed to parse URL #{input}: #{exc.class}: #{exc}")
31
+ end
32
+
33
+ params_hash = {}
34
+
35
+ if uri_obj.query
36
+ begin
37
+ params_array = uri_obj.query.split('&').map { |qc| qc.split('=') }
38
+ params_array.each.with_index do |pair, index|
39
+ raise(Errors::InvalidURL, "Parameter #{pair} at index #{index + 1} failed to parse as a key-value pair") if pair.length != 2
40
+
41
+ value = urldecode_component(pair[1])
42
+
43
+ if pair[0][-2..-1] == '[]'
44
+ key = urldecode_component(pair[0][0..-3])
45
+ params_hash[key] ||= []
46
+ params_hash[key].push(value)
47
+ else
48
+ key = urldecode_component(pair[0])
49
+ params_hash[key] = value
50
+ end
51
+ end
52
+ rescue StandardError => exc
53
+ raise(Errors::InvalidURL, "Failed to parse URL #{input} query parameters: #{exc.class}: #{exc}")
54
+ end
55
+ end
56
+
57
+ components = {
58
+ protocol: proto,
59
+ host: uri_obj.host,
60
+ port: uri_obj.port,
61
+ path: uri_obj.path,
62
+ params: params_hash,
63
+ fragment: urldecode_component(uri_obj.fragment),
64
+ user: urldecode_component(uri_obj.user),
65
+ password: urldecode_component(uri_obj.password),
66
+ }.reject { |_, v| v.to_s.empty? }
67
+
68
+ return new(**kwargs.merge(components))
69
+ end
70
+
71
+ attr_reader :default_protocol, :fragment, :host, :params, :password, :path_components, :port, :protocol, :user
72
+
73
+ %i[protocol host port path_components params fragment user password].each do |component|
74
+ define_method("#{component}=") do |value|
75
+ _validate_component(component: component, value: value)
76
+ instance_variable_set("@#{component}", value)
77
+ end
78
+ end
79
+
80
+ def initialize(**kwargs)
81
+ @path_components = []
82
+ @default_protocol = kwargs.delete(:default_protocol)
83
+
84
+ raise(ArgumentError, ':path and :path_components are exclusive') if kwargs.key(:path) && kwargs.key(:path_components)
85
+
86
+ defaults = {
87
+ protocol: @default_protocol,
88
+ host: nil,
89
+ port: _default_port_for_protocol(protocol: @default_protocol),
90
+ params: {},
91
+ fragment: nil,
92
+ user: nil,
93
+ password: nil,
94
+ }
95
+
96
+ defaults.merge(kwargs.compact).sort.each do |component, value|
97
+ send("#{component}=", value)
98
+ end
99
+ end
100
+
101
+ def join(value, replace_when_absolute: true)
102
+ _validate_component(component: :path, value: value)
103
+ new_components = _path_to_a(value: value)
104
+
105
+ rv = dup
106
+
107
+ rv.path_components = if replace_when_absolute && value[0] == '/'
108
+ new_components
109
+ else
110
+ @path_components + new_components
111
+ end
112
+
113
+ return rv
114
+ end
115
+
116
+ def join!(value, replace_when_absolute: true)
117
+ _validate_component(component: :path, value: value)
118
+ new_components = _path_to_a(value: value)
119
+
120
+ if replace_when_absolute && value[0] == '/'
121
+ @path_components = new_components
122
+ else
123
+ @path_components += new_components
124
+ end
125
+
126
+ return path
127
+ end
128
+
129
+ def path=(value)
130
+ if value.nil?
131
+ @path_components = []
132
+ else
133
+ _validate_component(component: :path, value: value)
134
+ @path_components = _path_to_a(value: value)
135
+ end
136
+ end
137
+
138
+ def path
139
+ return nil if @path_components.empty?
140
+
141
+ return @path_components.join('/')
142
+ end
143
+
144
+ def path_encoded
145
+ return nil if @path_components.empty?
146
+
147
+ return @path_components.map { |c| urlencode_component(c) }.join('/')
148
+ end
149
+
150
+ def to_s
151
+ %i[protocol host].each do |required_component|
152
+ raise(Errors::InvalidURL, "Missing #{required_component}") unless send(required_component)
153
+ end
154
+
155
+ ret_val = "#{protocol}://"
156
+ if user || password
157
+ ret_val += urlencode_component(user) if user
158
+ ret_val += ":#{urlencode_component(password)}" if password
159
+ ret_val += '@'
160
+ end
161
+
162
+ ret_val += host
163
+ ret_val += ":#{port}" if port && port != _default_port_for_protocol(protocol: protocol)
164
+ ret_val += "/#{path_encoded}" unless @path_components.empty?
165
+ ret_val += _query_string unless params.empty?
166
+ ret_val += "##{urlencode_component(fragment)}" if fragment
167
+
168
+ return ret_val
169
+ end
170
+
171
+ def urlencode_component(value)
172
+ return URI.encode_www_form_component(value.to_s)
173
+ end
174
+
175
+ private
176
+
177
+ def _default_port_for_protocol(protocol:)
178
+ return nil if protocol.nil?
179
+
180
+ rv = URI.parse("#{protocol}://").port
181
+ return rv
182
+ end
183
+
184
+ def _query_string
185
+ encoded_components = []
186
+
187
+ params.each_pair do |key, value|
188
+ encoded_key = urlencode_component(key)
189
+ if value.is_a? Array
190
+ value.map { |v| urlencode_component(v) }.each do |encoded_value|
191
+ encoded_components.push("#{encoded_key}[]=#{encoded_value}")
192
+ end
193
+ else
194
+ encoded_value = urlencode_component(value)
195
+ encoded_components.push("#{encoded_key}=#{encoded_value}")
196
+ end
197
+ end
198
+
199
+ return "?#{encoded_components.join('&')}"
200
+ end
201
+
202
+ def _path_to_a(value:)
203
+ value.split('/').reject(&:empty?).map { |c| self.class.urldecode_component(c) }
204
+ end
205
+
206
+ def _validate_component(**kwargs)
207
+ if kwargs.fetch(:value).nil?
208
+ raise(Errors::InvalidComponent, "nil #{kwargs.fetch(:component)} not allowed") if %i[params].include?(kwargs.fetch(:component))
209
+
210
+ return
211
+ end
212
+
213
+ _validate_component_type(**kwargs)
214
+ _validate_component_characters(**kwargs)
215
+ end
216
+
217
+ def _validate_component_characters(component:, value:)
218
+ case component
219
+ when :default_protocol, :protocol
220
+ # Basic URL building symbols
221
+ disallowed_chars = %w[@ : / ? & #]
222
+ when :host
223
+ disallowed_chars = if value[0] == '[' && value[-1] == ']'
224
+ # IPv6
225
+ # This should technically be more strict, but this is a minimal sanity check
226
+ %w[@ / ? & #]
227
+ else
228
+ %w[@ : / ? & #]
229
+ end
230
+ when :path
231
+ disallowed_chars = %w[? #]
232
+ when :path_components
233
+ value.each do |comp|
234
+ _validate_component_characters(component: :path, value: comp)
235
+ end
236
+
237
+ return
238
+ when :port, :params, :fragment, :user, :password
239
+ # port: integer: Nothing to do
240
+ # params: hash, urlencoded
241
+ # user: urlencoded
242
+ # password: urlencoded
243
+ return
244
+ else
245
+ raise(Errors::InternalError, "Unknown component #{component}")
246
+ end
247
+
248
+ disallowed_chars.each do |dchar|
249
+ raise(Errors::InvalidComponent, "#{component} contains disallowed character #{dchar}") if value.include?(dchar)
250
+ end
251
+ end
252
+
253
+ def _validate_component_type(component:, value:)
254
+ types = {
255
+ default_protocol: String,
256
+ protocol: String,
257
+ host: String,
258
+ port: Integer,
259
+ path: String,
260
+ path_components: Array,
261
+ params: Hash,
262
+ fragment: String,
263
+ user: String,
264
+ password: String,
265
+ }
266
+
267
+ raise(Errors::InternalError, "Unknown component #{component}") unless types.key?(component)
268
+ raise(Errors::ComponentTypeError, "#{component} must be a #{types[component]}, got a #{value.class} (#{value})") unless value.is_a?(types[component])
269
+ end
270
+ end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: basic_url
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Tom Noonan II
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-05-11 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Implements a simple URL object supporting object oriented paradigms,
14
+ basic as-you-expect path joins, and native URLencoding.
15
+ email:
16
+ - tom@tjnii.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - ".rubocop.yml"
22
+ - ".ruby-version"
23
+ - ".versions"
24
+ - CHANGELOG.md
25
+ - LICENSE.md
26
+ - Makefile
27
+ - README.md
28
+ - basic_url.gemspec
29
+ - cicd/_version_common.sh
30
+ - cicd/build_gem.sh
31
+ - cicd/current_gem_version.sh
32
+ - cicd/manage_version.sh
33
+ - cicd/push_gem.sh
34
+ - lib/basic_url.rb
35
+ - lib/basic_url/errors.rb
36
+ homepage: https://github.com/TJNII/basic_url
37
+ licenses:
38
+ - Apache-2.0
39
+ metadata:
40
+ homepage_uri: https://github.com/TJNII/basic_url
41
+ source_code_uri: https://github.com/TJNII/basic_url
42
+ changelog_uri: https://github.com/TJNII/basic_url/blob/master/CHANGELOG.md
43
+ rubygems_mfa_required: 'true'
44
+ post_install_message:
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 3.2.0
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ requirements: []
59
+ rubygems_version: 3.5.6
60
+ signing_key:
61
+ specification_version: 4
62
+ summary: A Basic URL object that supports common URL operations
63
+ test_files: []