fhir_packages_manager 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +43 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +157 -0
- data/Rakefile +32 -0
- data/exe/fhir_packages_manager +7 -0
- data/lib/fhir_packages_manager/cli.rb +136 -0
- data/lib/fhir_packages_manager/errors.rb +22 -0
- data/lib/fhir_packages_manager/fetch_result.rb +37 -0
- data/lib/fhir_packages_manager/ignore_list.rb +68 -0
- data/lib/fhir_packages_manager/manager.rb +89 -0
- data/lib/fhir_packages_manager/package.rb +28 -0
- data/lib/fhir_packages_manager/registry.rb +136 -0
- data/lib/fhir_packages_manager/version.rb +6 -0
- data/lib/fhir_packages_manager.rb +16 -0
- data/sig/fhir_packages_manager/cli.rbs +34 -0
- data/sig/fhir_packages_manager/errors.rbs +13 -0
- data/sig/fhir_packages_manager/fetch_result.rbs +19 -0
- data/sig/fhir_packages_manager/ignore_list.rbs +17 -0
- data/sig/fhir_packages_manager/manager.rbs +25 -0
- data/sig/fhir_packages_manager/package.rbs +12 -0
- data/sig/fhir_packages_manager/registry.rbs +31 -0
- data/sig/fhir_packages_manager/version.rbs +3 -0
- data/sig/fhir_packages_manager.rbs +2 -0
- metadata +75 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c3db88fe718417bdd2a0bd82604b48928db13d036ee912094cb8891e5218d967
|
|
4
|
+
data.tar.gz: d84a0f807bcd2f479cd511fb100cfd9c723c6b59fca65c5dc41aa2e6dd829418
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 17d1da3f230507fbeb91cd949a90370561bcdeece97bdf433b169f16b7baadf960cc542bd9931f56b2b6afe93936a1d53865efe8f45e9be969e2f1883dcb5acf
|
|
7
|
+
data.tar.gz: 3b4e62d57b1b034c137869355e2d8454fc92292d3b0b59f4825abc89af0d86013afd76f563de14f95a23a03e28f1fd7783ef8b54994e7cf5137d44068bda562e
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-07-10
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Library**: `FhirPackagesManager::Manager`, `Registry`, `Package`, `IgnoreList`, and
|
|
13
|
+
`FetchResult`, providing:
|
|
14
|
+
- Availability checks and version resolution (including `latest`) against one or more
|
|
15
|
+
npm-style FHIR package registries (e.g. `packages.fhir.org`, `packages.simplifier.net`),
|
|
16
|
+
tried in the order given.
|
|
17
|
+
- A configurable ignore list (loaded from YAML or JSON) that skips a package entirely or
|
|
18
|
+
pins a single ignored version.
|
|
19
|
+
- Tarball download into a destination folder, with `HttpError`/`PackageNotFoundError` for
|
|
20
|
+
failure cases and a `:downloaded`/`:ignored`/`:not_found`/`:error` result per package.
|
|
21
|
+
- **CLI**: `fhir_packages_manager` executable with `check` and `fetch` subcommands
|
|
22
|
+
(`-r/--registry`, `-d/--destination`, `-i/--ignore-file`, repeatable `--registry`).
|
|
23
|
+
- **Docker**: a multi-stage `Dockerfile` that builds and installs the gem from source and
|
|
24
|
+
exposes the CLI as the image's entrypoint, plus a `docker-compose.yml` for running it
|
|
25
|
+
without a local Ruby install.
|
|
26
|
+
- **Tests**: an RSpec suite (HTTP stubbed with WebMock, so it never hits a real registry)
|
|
27
|
+
covering the library, CLI, and every registry edge case (redirects, missing `Location`
|
|
28
|
+
header, unsupported URL schemes, 404s, etc.), with 100% line and branch coverage enforced
|
|
29
|
+
by a SimpleCov minimum-coverage gate.
|
|
30
|
+
- **Code quality tooling**: RuboCop, Reek, Fasterer, Flay, Flog, and Steep (with full RBS
|
|
31
|
+
signatures for every class), all wired into `bundle exec rake`.
|
|
32
|
+
- **CI/CD** (GitHub Actions):
|
|
33
|
+
- `quality.yml` — runs the full test/quality suite on every push to any branch, uploading
|
|
34
|
+
coverage to Codecov.
|
|
35
|
+
- `release.yml` — on a published GitHub Release or manual dispatch, re-runs the test/quality
|
|
36
|
+
gate, then publishes the gem to RubyGems.org (via Trusted Publishing) and the Docker image
|
|
37
|
+
to GitHub Container Registry.
|
|
38
|
+
- `docs.yml` — publishes YARD API documentation to GitHub Pages on every push to `main`.
|
|
39
|
+
- **Documentation**: YARD doc comments across the public API, and a Codecov/docs badge pair
|
|
40
|
+
in the README.
|
|
41
|
+
|
|
42
|
+
[Unreleased]: https://github.com/projkov/fhir_packages_manager/compare/v0.1.0...HEAD
|
|
43
|
+
[0.1.0]: https://github.com/projkov/fhir_packages_manager/releases/tag/v0.1.0
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
"fhir_packages_manager" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
|
|
4
|
+
|
|
5
|
+
* Participants will be tolerant of opposing views.
|
|
6
|
+
* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
|
7
|
+
* When interpreting the words and actions of others, participants should always assume good intentions.
|
|
8
|
+
* Behaviour which can be reasonably considered harassment will not be tolerated.
|
|
9
|
+
|
|
10
|
+
If you have any concerns about behaviour within this project, please contact us at ["prozskov@gmail.com"](mailto:"prozskov@gmail.com").
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pavel Rozhkov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# FhirPackagesManager
|
|
2
|
+
|
|
3
|
+
[](https://codecov.io/gh/projkov/fhir_packages_manager)
|
|
4
|
+
[](https://projkov.github.io/fhir_packages_manager/)
|
|
5
|
+
|
|
6
|
+
Checks the availability of FHIR implementation guide packages against npm-style
|
|
7
|
+
registries (e.g. `packages.fhir.org`, `packages.simplifier.net`), skips any
|
|
8
|
+
packages/versions on a configurable ignore list, and downloads the resulting
|
|
9
|
+
`.tgz` tarballs into a destination folder.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
Install the gem and add to the application's Gemfile by executing:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle add fhir_packages_manager
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gem install fhir_packages_manager
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### Library
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require "fhir_packages_manager"
|
|
31
|
+
|
|
32
|
+
manager = FhirPackagesManager::Manager.new(
|
|
33
|
+
registries: ["https://packages.fhir.org", "https://packages.simplifier.net"],
|
|
34
|
+
destination: "./fhir_packages",
|
|
35
|
+
ignore_list: FhirPackagesManager::IgnoreList.load("fhir_packages_ignore.yml") # optional
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
manager.available?("hl7.fhir.us.core", "6.1.0") # => true/false
|
|
39
|
+
|
|
40
|
+
result = manager.fetch("hl7.fhir.us.core@6.1.0")
|
|
41
|
+
result.status # => :downloaded, :ignored, :not_found, or :error
|
|
42
|
+
result.path # => "./fhir_packages/hl7.fhir.us.core-6.1.0.tgz" when downloaded
|
|
43
|
+
|
|
44
|
+
manager.fetch_all(["hl7.fhir.us.core@6.1.0", "hl7.fhir.r4.core"]) # bare name = latest
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The ignore list file (YAML or JSON) is a flat array where a bare string
|
|
48
|
+
ignores every version of a package, and a hash pins a single version:
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
- hl7.fhir.r4.core
|
|
52
|
+
- name: hl7.fhir.us.core
|
|
53
|
+
version: 3.1.0
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### CLI
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
exe/fhir_packages_manager check hl7.fhir.us.core@6.1.0 -r https://packages.fhir.org
|
|
60
|
+
exe/fhir_packages_manager fetch hl7.fhir.us.core@6.1.0 hl7.fhir.r4.core \
|
|
61
|
+
-r https://packages.fhir.org -r https://packages.simplifier.net \
|
|
62
|
+
-d ./fhir_packages -i fhir_packages_ignore.yml
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Docker
|
|
66
|
+
|
|
67
|
+
The CLI is also published as a container image, so it can be used without installing Ruby
|
|
68
|
+
or the gem locally:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
docker run --rm ghcr.io/projkov/fhir_packages_manager check hl7.fhir.us.core@6.1.0 -r https://packages.fhir.org
|
|
72
|
+
|
|
73
|
+
docker run --rm -v "$(pwd)/fhir_packages:/fhir_packages" ghcr.io/projkov/fhir_packages_manager \
|
|
74
|
+
fetch hl7.fhir.us.core@6.1.0 -r https://packages.fhir.org -d /fhir_packages
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Mount a volume (as above) to get downloaded `.tgz` files back out onto the host.
|
|
78
|
+
|
|
79
|
+
#### Docker Compose
|
|
80
|
+
|
|
81
|
+
A `docker-compose.yml` is included so the image name and volume mount don't need to be
|
|
82
|
+
retyped for every invocation:
|
|
83
|
+
|
|
84
|
+
```yaml
|
|
85
|
+
services:
|
|
86
|
+
fhir_packages_manager:
|
|
87
|
+
image: ghcr.io/projkov/fhir_packages_manager:latest
|
|
88
|
+
build: .
|
|
89
|
+
volumes:
|
|
90
|
+
- ./fhir_packages:/fhir_packages
|
|
91
|
+
# - ./fhir_packages_ignore.yml:/fhir_packages_ignore.yml:ro
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Since the image's `ENTRYPOINT` is the CLI itself, anything passed after the service name
|
|
95
|
+
becomes its arguments:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
docker compose run --rm fhir_packages_manager \
|
|
99
|
+
check hl7.fhir.us.core@6.1.0 -r https://packages.fhir.org
|
|
100
|
+
|
|
101
|
+
docker compose run --rm fhir_packages_manager \
|
|
102
|
+
fetch hl7.fhir.us.core@6.1.0 -r https://packages.fhir.org -d /fhir_packages
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`docker compose run --rm` (rather than `up`) is used because this is a one-shot CLI, not a
|
|
106
|
+
long-running service — `--rm` discards the container after it exits. `docker compose pull`
|
|
107
|
+
fetches the published image from GHCR; `docker compose build` builds it locally from the
|
|
108
|
+
`Dockerfile` instead (e.g. to test an unreleased change). Uncomment the ignore-file volume
|
|
109
|
+
line and add `-i /fhir_packages_ignore.yml` to the command to use an ignore list.
|
|
110
|
+
|
|
111
|
+
## Development
|
|
112
|
+
|
|
113
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
114
|
+
|
|
115
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
116
|
+
|
|
117
|
+
To release a new version: bump `VERSION` in `lib/fhir_packages_manager/version.rb`, add a
|
|
118
|
+
dated entry to `CHANGELOG.md`, and merge to `main`. Then either publish a GitHub Release (tag
|
|
119
|
+
`vX.Y.Z`) or manually run the `Release` workflow from the Actions tab — `.github/workflows/release.yml`
|
|
120
|
+
re-runs the full test/quality gate and, if it passes, publishes the gem to
|
|
121
|
+
[rubygems.org](https://rubygems.org) via Trusted Publishing and pushes the Docker image to
|
|
122
|
+
GHCR. Don't run `bundle exec rake release` locally — it would try to push using local
|
|
123
|
+
credentials instead of through that pipeline.
|
|
124
|
+
|
|
125
|
+
### Tests and code quality
|
|
126
|
+
|
|
127
|
+
`bundle exec rake` runs the full suite: RSpec (with a SimpleCov gate requiring 95%+ line
|
|
128
|
+
coverage), RuboCop, Reek, Fasterer, Flay, Flog, and Steep. Each is also runnable on its own,
|
|
129
|
+
e.g. `bundle exec rspec`, `bundle exec rake rubocop`, `bundle exec rake steep`.
|
|
130
|
+
|
|
131
|
+
RSpec specs stub all HTTP calls with WebMock (see `spec/fhir_packages_manager/registry_spec.rb`
|
|
132
|
+
and `cli_spec.rb`), so the suite never hits a real registry.
|
|
133
|
+
|
|
134
|
+
### Documentation
|
|
135
|
+
|
|
136
|
+
API docs are generated from the code with [YARD](https://yardoc.org) and published to
|
|
137
|
+
[GitHub Pages](https://projkov.github.io/fhir_packages_manager/) by
|
|
138
|
+
`.github/workflows/docs.yml` on every push to `main`. To build and browse them locally:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
bundle exec yard doc # writes HTML to doc/
|
|
142
|
+
bundle exec yard server --reload # serves it at http://localhost:8808, rebuilding on change
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`bundle exec yard stats --list-undoc` lists any undocumented public classes/methods.
|
|
146
|
+
|
|
147
|
+
## Contributing
|
|
148
|
+
|
|
149
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/projkov/fhir_packages_manager. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/projkov/fhir_packages_manager/blob/main/CODE_OF_CONDUCT.md).
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
154
|
+
|
|
155
|
+
## Code of Conduct
|
|
156
|
+
|
|
157
|
+
Everyone interacting in the FhirPackagesManager project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/projkov/fhir_packages_manager/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
|
|
5
|
+
require 'rspec/core/rake_task'
|
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
7
|
+
|
|
8
|
+
require 'rubocop/rake_task'
|
|
9
|
+
RuboCop::RakeTask.new
|
|
10
|
+
|
|
11
|
+
require 'reek/rake/task'
|
|
12
|
+
Reek::Rake::Task.new do |t|
|
|
13
|
+
t.source_files = '{lib,exe}/**/*.rb'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
require 'flay_task'
|
|
17
|
+
FlayTask.new(:flay, 200, %w[lib exe])
|
|
18
|
+
|
|
19
|
+
require 'flog_task'
|
|
20
|
+
FlogTask.new(:flog, 30, %w[lib exe], :max_method)
|
|
21
|
+
|
|
22
|
+
desc 'Check for slow Ruby idioms with fasterer'
|
|
23
|
+
task :fasterer do
|
|
24
|
+
sh 'bundle exec fasterer lib exe'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
desc 'Type-check with Steep'
|
|
28
|
+
task :steep do
|
|
29
|
+
sh 'bundle exec steep check'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
task default: %i[spec rubocop reek fasterer flay flog steep]
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
|
|
5
|
+
module FhirPackagesManager
|
|
6
|
+
# Command-line entry point backing the `fhir_packages_manager` executable.
|
|
7
|
+
# See the "CLI" section of the README for usage examples.
|
|
8
|
+
class CLI
|
|
9
|
+
# @return [Array<String>] the supported subcommands
|
|
10
|
+
COMMANDS = %w[fetch check].freeze
|
|
11
|
+
|
|
12
|
+
# @return [String] usage text shown on --help and on invalid invocations
|
|
13
|
+
BANNER = <<~USAGE
|
|
14
|
+
Usage: fhir_packages_manager COMMAND package[@version] [package[@version] ...] [options]
|
|
15
|
+
|
|
16
|
+
Commands:
|
|
17
|
+
fetch Download packages into the destination folder
|
|
18
|
+
check Report which registry (if any) has each package/version
|
|
19
|
+
|
|
20
|
+
USAGE
|
|
21
|
+
|
|
22
|
+
# Parses argv and runs the requested command (`fetch` or `check`).
|
|
23
|
+
#
|
|
24
|
+
# @param argv [Array<String>] arguments as passed to the executable, e.g. ARGV
|
|
25
|
+
# @return [void]
|
|
26
|
+
def self.run(argv)
|
|
27
|
+
new(argv).run
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param argv [Array<String>] see {.run}
|
|
31
|
+
def initialize(argv)
|
|
32
|
+
@argv = argv.dup
|
|
33
|
+
@options = { destination: './fhir_packages', registries: [] } # : Hash[Symbol, untyped]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [void]
|
|
37
|
+
def run
|
|
38
|
+
parser.parse!(@argv)
|
|
39
|
+
command = @argv.shift
|
|
40
|
+
package_specs = @argv
|
|
41
|
+
|
|
42
|
+
return usage_error(1) if !COMMANDS.include?(command) || package_specs.empty?
|
|
43
|
+
return usage_error('Error: at least one --registry URL is required') if @options[:registries].empty?
|
|
44
|
+
|
|
45
|
+
dispatch(command, package_specs)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def dispatch(command, package_specs)
|
|
51
|
+
case command
|
|
52
|
+
when 'fetch'
|
|
53
|
+
fetch(package_specs)
|
|
54
|
+
when 'check'
|
|
55
|
+
check(package_specs)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def parser
|
|
60
|
+
@parser ||= OptionParser.new do |opts|
|
|
61
|
+
opts.banner = BANNER
|
|
62
|
+
define_options(opts)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def define_options(opts)
|
|
67
|
+
opts.on('-r URL', '--registry URL', 'Registry base URL (repeatable, checked in order given)') do |value|
|
|
68
|
+
@options[:registries] << value
|
|
69
|
+
end
|
|
70
|
+
opts.on('-d DIR', '--destination DIR',
|
|
71
|
+
'Destination folder for downloaded packages (default: ./fhir_packages)') do |value|
|
|
72
|
+
@options[:destination] = value
|
|
73
|
+
end
|
|
74
|
+
opts.on('-i PATH', '--ignore-file PATH', 'YAML/JSON file listing packages/versions to ignore') do |value|
|
|
75
|
+
@options[:ignore_file] = value
|
|
76
|
+
end
|
|
77
|
+
opts.on('-h', '--help', 'Show this help') do
|
|
78
|
+
puts opts
|
|
79
|
+
exit
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def usage_error(message_or_status)
|
|
84
|
+
if message_or_status.is_a?(String)
|
|
85
|
+
warn message_or_status
|
|
86
|
+
else
|
|
87
|
+
warn parser
|
|
88
|
+
end
|
|
89
|
+
exit 1
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def manager
|
|
93
|
+
@manager ||= Manager.new(
|
|
94
|
+
registries: @options[:registries],
|
|
95
|
+
destination: @options[:destination],
|
|
96
|
+
ignore_list: load_ignore_list
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def load_ignore_list
|
|
101
|
+
ignore_file = @options[:ignore_file]
|
|
102
|
+
IgnoreList.load(ignore_file) if ignore_file
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def fetch(package_specs)
|
|
106
|
+
results = manager.fetch_all(package_specs)
|
|
107
|
+
results.each { |result| puts fetch_line(result) }
|
|
108
|
+
exit 1 if results.any? { |result| result.not_found? || result.error? }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def fetch_line(result)
|
|
112
|
+
package = result.package
|
|
113
|
+
case result.status
|
|
114
|
+
when :downloaded then "OK #{package} -> #{result.path} (#{result.registry})"
|
|
115
|
+
when :ignored then "SKIP #{package} (ignored)"
|
|
116
|
+
when :not_found then "MISS #{package} (not found in any registry)"
|
|
117
|
+
when :error then "ERR #{package}: #{result.error}"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def check(package_specs)
|
|
122
|
+
package_specs.each do |spec|
|
|
123
|
+
package = Package.parse(spec)
|
|
124
|
+
found = manager.find_registry(package.name, package.version)
|
|
125
|
+
puts check_line(package, found)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def check_line(package, found)
|
|
130
|
+
return "UNAVAILABLE #{package}" unless found
|
|
131
|
+
|
|
132
|
+
registry, version = found
|
|
133
|
+
"AVAILABLE #{package.name}@#{version} (#{registry.base_url})"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FhirPackagesManager
|
|
4
|
+
# Base class for all errors raised by this gem.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised for any non-2xx/3xx HTTP response from a registry.
|
|
8
|
+
class HttpError < Error
|
|
9
|
+
# @return [Integer, nil] the HTTP status code, or nil for a connection-level failure
|
|
10
|
+
attr_reader :status
|
|
11
|
+
|
|
12
|
+
# @param message [String]
|
|
13
|
+
# @param status [Integer, nil]
|
|
14
|
+
def initialize(message, status)
|
|
15
|
+
super(message)
|
|
16
|
+
@status = status
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Raised when a package or version does not exist on a registry.
|
|
21
|
+
class PackageNotFoundError < Error; end
|
|
22
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FhirPackagesManager
|
|
4
|
+
# Outcome of {Manager#fetch} for a single package.
|
|
5
|
+
#
|
|
6
|
+
# @!attribute package
|
|
7
|
+
# @return [Package] the package that was requested (version resolved, when downloaded)
|
|
8
|
+
# @!attribute status
|
|
9
|
+
# @return [Symbol] one of :downloaded, :ignored, :not_found, :error
|
|
10
|
+
# @!attribute registry
|
|
11
|
+
# @return [String, nil] the base URL of the registry that served the package, if downloaded
|
|
12
|
+
# @!attribute path
|
|
13
|
+
# @return [String, nil] where the tarball was written, if downloaded
|
|
14
|
+
# @!attribute error
|
|
15
|
+
# @return [String, nil] the error message, when status is :error
|
|
16
|
+
class FetchResult < Struct.new(:package, :status, :registry, :path, :error, keyword_init: true)
|
|
17
|
+
# @return [Boolean] true if the package was downloaded successfully
|
|
18
|
+
def downloaded?
|
|
19
|
+
status == :downloaded
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Boolean] true if the package was skipped because it's on the ignore list
|
|
23
|
+
def ignored?
|
|
24
|
+
status == :ignored
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [Boolean] true if no registry had the package/version
|
|
28
|
+
def not_found?
|
|
29
|
+
status == :not_found
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [Boolean] true if downloading raised an HTTP error
|
|
33
|
+
def error?
|
|
34
|
+
status == :error
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module FhirPackagesManager
|
|
7
|
+
# A list of packages (optionally pinned to a version) to skip when fetching.
|
|
8
|
+
#
|
|
9
|
+
# Loaded from a YAML or JSON file containing a flat array, e.g.:
|
|
10
|
+
#
|
|
11
|
+
# - hl7.fhir.r4.core # ignore every version of this package
|
|
12
|
+
# - name: hl7.fhir.us.core
|
|
13
|
+
# version: 3.1.0 # ignore only this one version
|
|
14
|
+
class IgnoreList
|
|
15
|
+
# Loads an ignore list from a YAML or JSON file (JSON iff the extension is .json).
|
|
16
|
+
#
|
|
17
|
+
# @param path [String] path to a YAML or JSON file containing a flat array of entries
|
|
18
|
+
# @return [IgnoreList]
|
|
19
|
+
def self.load(path)
|
|
20
|
+
data = case File.extname(path).downcase
|
|
21
|
+
when '.json'
|
|
22
|
+
JSON.parse(File.read(path))
|
|
23
|
+
else
|
|
24
|
+
YAML.load_file(path)
|
|
25
|
+
end
|
|
26
|
+
new(data || [])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param entries [Array<String, Hash>] bare package names (ignore every version) and/or
|
|
30
|
+
# `{"name" => ..., "version" => ...}` hashes (ignore only that version)
|
|
31
|
+
# @raise [ArgumentError] if an entry is neither a String nor a Hash
|
|
32
|
+
def initialize(entries = [])
|
|
33
|
+
@entries = entries.map { |entry| normalize(entry) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param name [String] the package name
|
|
37
|
+
# @param version [String, nil] the version being requested, if any
|
|
38
|
+
# @return [Boolean] true if this name/version is on the ignore list
|
|
39
|
+
def ignored?(name, version = nil)
|
|
40
|
+
@entries.any? { |entry| matches?(entry, name, version) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def matches?(entry, name, version)
|
|
46
|
+
return false unless entry[:name] == name
|
|
47
|
+
|
|
48
|
+
ignored_version = entry[:version]
|
|
49
|
+
ignored_version.nil? || ignored_version == version
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def normalize(entry)
|
|
53
|
+
case entry
|
|
54
|
+
when String
|
|
55
|
+
{ name: entry, version: nil }
|
|
56
|
+
when Hash
|
|
57
|
+
normalize_hash(entry)
|
|
58
|
+
else
|
|
59
|
+
raise ArgumentError, "Invalid ignore list entry: #{entry.inspect}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def normalize_hash(entry)
|
|
64
|
+
version = entry['version'] || entry[:version]
|
|
65
|
+
{ name: entry['name'] || entry[:name], version: version&.to_s }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
module FhirPackagesManager
|
|
6
|
+
# Orchestrates checking availability across a set of registries, honoring an
|
|
7
|
+
# ignore list, and downloading tarballs into a destination folder.
|
|
8
|
+
class Manager
|
|
9
|
+
# @return [Array<Registry>] registries checked, in the order given to {#initialize}
|
|
10
|
+
attr_reader :registries
|
|
11
|
+
# @return [String] the folder packages are downloaded into
|
|
12
|
+
attr_reader :destination
|
|
13
|
+
# @return [IgnoreList, nil] packages/versions skipped by {#fetch}
|
|
14
|
+
attr_reader :ignore_list
|
|
15
|
+
|
|
16
|
+
# @param registries [Array<String, Registry>] one or more registry base URLs and/or
|
|
17
|
+
# {Registry} instances, checked in the order given
|
|
18
|
+
# @param destination [String] folder downloaded tarballs are written into
|
|
19
|
+
# @param ignore_list [IgnoreList, nil] packages/versions to skip in {#fetch}
|
|
20
|
+
# @raise [ArgumentError] if registries is nil or empty
|
|
21
|
+
def initialize(registries:, destination:, ignore_list: nil)
|
|
22
|
+
raise ArgumentError, 'at least one registry is required' if registries.nil? || registries.empty?
|
|
23
|
+
|
|
24
|
+
@registries = registries.map { |entry| entry.is_a?(Registry) ? entry : Registry.new(entry) }
|
|
25
|
+
@destination = destination.to_s
|
|
26
|
+
@ignore_list = ignore_list
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param name [String] the package name
|
|
30
|
+
# @param version [String, nil] a specific version, or nil/"latest" for the newest
|
|
31
|
+
# @return [Boolean] true if any configured registry has this package/version
|
|
32
|
+
def available?(name, version = nil)
|
|
33
|
+
!find_registry(name, version).nil?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param name [String] the package name
|
|
37
|
+
# @param version [String, nil] a specific version, or nil/"latest" for the newest
|
|
38
|
+
# @return [Array(Registry, String), nil] the first registry that has this package/version,
|
|
39
|
+
# paired with the resolved version string; nil if none of them do
|
|
40
|
+
def find_registry(name, version = nil)
|
|
41
|
+
registries.each do |registry|
|
|
42
|
+
resolved = registry.version?(name, version)
|
|
43
|
+
return [registry, resolved] if resolved
|
|
44
|
+
end
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Fetches a single package. Skips it if it's on the ignore list, otherwise downloads
|
|
49
|
+
# its tarball into {#destination}.
|
|
50
|
+
#
|
|
51
|
+
# @param package [String, Package] a "name@version" string, a bare name (latest), or a Package
|
|
52
|
+
# @return [FetchResult]
|
|
53
|
+
def fetch(package)
|
|
54
|
+
fetch_package(Package.parse(package))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @param packages [Array<String, Package>] see {#fetch}
|
|
58
|
+
# @return [Array<FetchResult>] one result per package, in the same order
|
|
59
|
+
def fetch_all(packages)
|
|
60
|
+
packages.map { |package| fetch(package) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def fetch_package(package)
|
|
66
|
+
return FetchResult.new(package: package, status: :ignored) if ignored?(package)
|
|
67
|
+
|
|
68
|
+
found = find_registry(package.name, package.version)
|
|
69
|
+
return FetchResult.new(package: package, status: :not_found) unless found
|
|
70
|
+
|
|
71
|
+
download_result(package, found)
|
|
72
|
+
rescue HttpError => e
|
|
73
|
+
FetchResult.new(package: package, status: :error, error: e.message)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def ignored?(package)
|
|
77
|
+
!!ignore_list&.ignored?(package.name, package.version)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def download_result(package, found)
|
|
81
|
+
registry, resolved_version = found
|
|
82
|
+
name = package.name
|
|
83
|
+
target = File.join(destination, "#{name}-#{resolved_version}.tgz")
|
|
84
|
+
registry.download(name, resolved_version, target)
|
|
85
|
+
FetchResult.new(package: Package.new(name, resolved_version), status: :downloaded,
|
|
86
|
+
registry: registry.base_url, path: target)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FhirPackagesManager
|
|
4
|
+
# A requested package, e.g. "hl7.fhir.us.core@6.1.0" or "hl7.fhir.us.core"
|
|
5
|
+
# (version nil/"latest" means "resolve to the registry's latest version").
|
|
6
|
+
#
|
|
7
|
+
# @!attribute name
|
|
8
|
+
# @return [String] the FHIR package name, e.g. "hl7.fhir.us.core"
|
|
9
|
+
# @!attribute version
|
|
10
|
+
# @return [String, nil] the requested version, or nil/"latest" for the newest available
|
|
11
|
+
class Package < Struct.new(:name, :version)
|
|
12
|
+
# Parses a "name@version" (or bare "name") spec string into a Package.
|
|
13
|
+
#
|
|
14
|
+
# @param spec [String, Package] a spec string, or an existing Package (returned as-is)
|
|
15
|
+
# @return [Package]
|
|
16
|
+
def self.parse(spec)
|
|
17
|
+
return spec if spec.is_a?(Package)
|
|
18
|
+
|
|
19
|
+
parts = spec.to_s.split('@', 2)
|
|
20
|
+
new(parts[0] || '', parts[1])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [String] "name@version", or just "name" when version is nil
|
|
24
|
+
def to_s
|
|
25
|
+
version ? "#{name}@#{version}" : name
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'fileutils'
|
|
7
|
+
|
|
8
|
+
module FhirPackagesManager
|
|
9
|
+
# Client for a single FHIR package registry (e.g. https://packages.fhir.org
|
|
10
|
+
# or https://packages.simplifier.net). Both implement the same npm-style
|
|
11
|
+
# registry API: `GET /<package>` returns metadata with a "versions" map and
|
|
12
|
+
# "dist-tags", and `GET /<package>/<version>` streams the .tgz tarball.
|
|
13
|
+
class Registry
|
|
14
|
+
# @return [Integer] maximum HTTP redirects followed before raising {HttpError}
|
|
15
|
+
MAX_REDIRECTS = 5
|
|
16
|
+
|
|
17
|
+
# @return [String] the registry's base URL, with any trailing slash stripped
|
|
18
|
+
attr_reader :base_url
|
|
19
|
+
|
|
20
|
+
# @param base_url [String] e.g. "https://packages.fhir.org"
|
|
21
|
+
def initialize(base_url)
|
|
22
|
+
@base_url = base_url.to_s.chomp('/')
|
|
23
|
+
@metadata_cache = {} # : Hash[String, Hash[untyped, untyped]]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Fetches (and caches) the full registry metadata document for a package name.
|
|
27
|
+
#
|
|
28
|
+
# @param name [String] the package name
|
|
29
|
+
# @return [Hash] the parsed npm-style registry metadata ("dist-tags", "versions", etc.)
|
|
30
|
+
# @raise [PackageNotFoundError] if the package doesn't exist on this registry
|
|
31
|
+
# @raise [HttpError] for any other non-2xx/3xx response
|
|
32
|
+
def metadata(name)
|
|
33
|
+
@metadata_cache[name] ||= JSON.parse(get("#{base_url}/#{name}"))
|
|
34
|
+
rescue HttpError => e
|
|
35
|
+
raise PackageNotFoundError, "#{name} not found on #{base_url}" if e.status == 404
|
|
36
|
+
|
|
37
|
+
raise
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @param name [String] the package name
|
|
41
|
+
# @param version [String, nil] a specific version, or nil/"latest" for the newest
|
|
42
|
+
# @return [String, nil] the resolved version string if it exists on this registry,
|
|
43
|
+
# or nil if the package or version doesn't exist (never raises)
|
|
44
|
+
def version?(name, version = nil)
|
|
45
|
+
meta = metadata(name)
|
|
46
|
+
resolved = resolve_version(meta, version)
|
|
47
|
+
resolved && meta['versions']&.key?(resolved) ? resolved : nil
|
|
48
|
+
rescue PackageNotFoundError
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @param name [String] the package name
|
|
53
|
+
# @param version [String] an exact version, as returned by {#version?}
|
|
54
|
+
# @return [String] the tarball's download URL
|
|
55
|
+
# @raise [PackageNotFoundError] if the name/version doesn't exist on this registry
|
|
56
|
+
def tarball_url(name, version)
|
|
57
|
+
meta = metadata(name)
|
|
58
|
+
entry = meta.dig('versions', version)
|
|
59
|
+
raise PackageNotFoundError, "#{name}@#{version} not found on #{base_url}" unless entry
|
|
60
|
+
|
|
61
|
+
entry.dig('dist', 'tarball') || "#{base_url}/#{name}/#{version}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Downloads the package tarball to destination_path, creating parent directories as needed.
|
|
65
|
+
#
|
|
66
|
+
# @param name [String] the package name
|
|
67
|
+
# @param version [String] an exact version, as returned by {#version?}
|
|
68
|
+
# @param destination_path [String] where to write the downloaded .tgz file
|
|
69
|
+
# @return [String] destination_path
|
|
70
|
+
# @raise [PackageNotFoundError] if the name/version doesn't exist on this registry
|
|
71
|
+
# @raise [HttpError] if the download itself fails
|
|
72
|
+
def download(name, version, destination_path)
|
|
73
|
+
FileUtils.mkdir_p(File.dirname(destination_path))
|
|
74
|
+
download_file(tarball_url(name, version), destination_path)
|
|
75
|
+
destination_path
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def resolve_version(meta, version)
|
|
81
|
+
return meta.dig('dist-tags', 'latest') if version.nil? || version == 'latest'
|
|
82
|
+
|
|
83
|
+
version
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def get(url)
|
|
87
|
+
request(url, read_timeout: 30) { |http, uri| http.get(uri.request_uri, 'Accept' => 'application/json') }.body
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def download_file(url, destination_path)
|
|
91
|
+
request(url, read_timeout: 120) do |http, uri|
|
|
92
|
+
http.request_get(uri.request_uri) do |response|
|
|
93
|
+
save_body(response, destination_path) if response.is_a?(Net::HTTPSuccess)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def save_body(response, destination_path)
|
|
99
|
+
File.open(destination_path, 'wb') do |file|
|
|
100
|
+
response.read_body { |chunk| file.write(chunk) }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Issues a GET, yielding (http, uri) to perform it, and follows any
|
|
105
|
+
# redirect response by re-issuing the same block against the new location.
|
|
106
|
+
def request(url, read_timeout:, redirects_left: MAX_REDIRECTS, &)
|
|
107
|
+
raise HttpError.new("Too many redirects for #{url}", nil) if redirects_left <= 0
|
|
108
|
+
|
|
109
|
+
uri = URI(url)
|
|
110
|
+
raise HttpError.new("Unsupported URL scheme: #{url}", nil) unless uri.is_a?(URI::HTTP)
|
|
111
|
+
|
|
112
|
+
host = uri.host or raise HttpError.new("URL has no host: #{url}", nil)
|
|
113
|
+
|
|
114
|
+
response = Net::HTTP.start(host, uri.port, use_ssl: uri.scheme == 'https', open_timeout: 10,
|
|
115
|
+
read_timeout: read_timeout) do |http|
|
|
116
|
+
yield http, uri
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
follow(response, url, read_timeout:, redirects_left:, &)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def follow(response, url, read_timeout:, redirects_left:, &)
|
|
123
|
+
case response
|
|
124
|
+
when Net::HTTPSuccess
|
|
125
|
+
response
|
|
126
|
+
when Net::HTTPRedirection
|
|
127
|
+
location = response['location'] or raise HttpError.new("Redirect from #{url} has no Location header", nil)
|
|
128
|
+
|
|
129
|
+
request(location, read_timeout:, redirects_left: redirects_left - 1, &)
|
|
130
|
+
else
|
|
131
|
+
status = response.code.to_i
|
|
132
|
+
raise HttpError.new("GET #{url} failed: #{status} #{response.message}", status)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'fhir_packages_manager/version'
|
|
4
|
+
require_relative 'fhir_packages_manager/errors'
|
|
5
|
+
require_relative 'fhir_packages_manager/package'
|
|
6
|
+
require_relative 'fhir_packages_manager/fetch_result'
|
|
7
|
+
require_relative 'fhir_packages_manager/registry'
|
|
8
|
+
require_relative 'fhir_packages_manager/ignore_list'
|
|
9
|
+
require_relative 'fhir_packages_manager/manager'
|
|
10
|
+
|
|
11
|
+
# Checks the availability of FHIR implementation guide packages against
|
|
12
|
+
# npm-style registries (e.g. packages.fhir.org, packages.simplifier.net),
|
|
13
|
+
# skips entries on a configurable {IgnoreList}, and downloads the resulting
|
|
14
|
+
# tarballs via {Manager}. See the README for usage examples.
|
|
15
|
+
module FhirPackagesManager
|
|
16
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module FhirPackagesManager
|
|
2
|
+
class CLI
|
|
3
|
+
COMMANDS: Array[String]
|
|
4
|
+
BANNER: String
|
|
5
|
+
|
|
6
|
+
def self.run: (Array[String] argv) -> void
|
|
7
|
+
|
|
8
|
+
def initialize: (Array[String] argv) -> void
|
|
9
|
+
|
|
10
|
+
def run: () -> void
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def dispatch: (String? command, Array[String] package_specs) -> void
|
|
15
|
+
|
|
16
|
+
def parser: () -> OptionParser
|
|
17
|
+
|
|
18
|
+
def define_options: (OptionParser opts) -> void
|
|
19
|
+
|
|
20
|
+
def usage_error: (String | Integer message_or_status) -> bot
|
|
21
|
+
|
|
22
|
+
def manager: () -> Manager
|
|
23
|
+
|
|
24
|
+
def load_ignore_list: () -> IgnoreList?
|
|
25
|
+
|
|
26
|
+
def fetch: (Array[String] package_specs) -> void
|
|
27
|
+
|
|
28
|
+
def fetch_line: (FetchResult result) -> String?
|
|
29
|
+
|
|
30
|
+
def check: (Array[String] package_specs) -> void
|
|
31
|
+
|
|
32
|
+
def check_line: (Package package, [Registry, String]? found) -> String
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module FhirPackagesManager
|
|
2
|
+
class FetchResult < Struct[untyped]
|
|
3
|
+
attr_accessor package: Package
|
|
4
|
+
attr_accessor status: Symbol
|
|
5
|
+
attr_accessor registry: String?
|
|
6
|
+
attr_accessor path: String?
|
|
7
|
+
attr_accessor error: String?
|
|
8
|
+
|
|
9
|
+
def self.new: (package: Package, status: Symbol, ?registry: String?, ?path: String?, ?error: String?) -> FetchResult
|
|
10
|
+
|
|
11
|
+
def downloaded?: () -> bool
|
|
12
|
+
|
|
13
|
+
def ignored?: () -> bool
|
|
14
|
+
|
|
15
|
+
def not_found?: () -> bool
|
|
16
|
+
|
|
17
|
+
def error?: () -> bool
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module FhirPackagesManager
|
|
2
|
+
class IgnoreList
|
|
3
|
+
def self.load: (String path) -> IgnoreList
|
|
4
|
+
|
|
5
|
+
def initialize: (?Array[String | Hash[untyped, untyped]] entries) -> void
|
|
6
|
+
|
|
7
|
+
def ignored?: (String name, ?String? version) -> bool
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def matches?: (Hash[Symbol, String?] entry, String name, String? version) -> bool
|
|
12
|
+
|
|
13
|
+
def normalize: (String | Hash[untyped, untyped] entry) -> Hash[Symbol, String?]
|
|
14
|
+
|
|
15
|
+
def normalize_hash: (Hash[untyped, untyped] entry) -> Hash[Symbol, String?]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module FhirPackagesManager
|
|
2
|
+
class Manager
|
|
3
|
+
attr_reader registries: Array[Registry]
|
|
4
|
+
attr_reader destination: String
|
|
5
|
+
attr_reader ignore_list: IgnoreList?
|
|
6
|
+
|
|
7
|
+
def initialize: (registries: Array[String | Registry], destination: String, ?ignore_list: IgnoreList?) -> void
|
|
8
|
+
|
|
9
|
+
def available?: (String name, ?String? version) -> bool
|
|
10
|
+
|
|
11
|
+
def find_registry: (String name, ?String? version) -> [Registry, String]?
|
|
12
|
+
|
|
13
|
+
def fetch: (String | Package package) -> FetchResult
|
|
14
|
+
|
|
15
|
+
def fetch_all: (Array[String | Package] packages) -> Array[FetchResult]
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def fetch_package: (Package package) -> FetchResult
|
|
20
|
+
|
|
21
|
+
def ignored?: (Package package) -> bool
|
|
22
|
+
|
|
23
|
+
def download_result: (Package package, [Registry, String] found) -> FetchResult
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module FhirPackagesManager
|
|
2
|
+
class Package < Struct[untyped]
|
|
3
|
+
attr_accessor name: String
|
|
4
|
+
attr_accessor version: String?
|
|
5
|
+
|
|
6
|
+
def self.new: (String name, ?String? version) -> Package
|
|
7
|
+
|
|
8
|
+
def self.parse: (Package | String spec) -> Package
|
|
9
|
+
|
|
10
|
+
def to_s: () -> String
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module FhirPackagesManager
|
|
2
|
+
class Registry
|
|
3
|
+
MAX_REDIRECTS: Integer
|
|
4
|
+
|
|
5
|
+
attr_reader base_url: String
|
|
6
|
+
|
|
7
|
+
def initialize: (String base_url) -> void
|
|
8
|
+
|
|
9
|
+
def metadata: (String name) -> Hash[untyped, untyped]
|
|
10
|
+
|
|
11
|
+
def version?: (String name, ?String? version) -> String?
|
|
12
|
+
|
|
13
|
+
def tarball_url: (String name, String version) -> String
|
|
14
|
+
|
|
15
|
+
def download: (String name, String version, String destination_path) -> String
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def resolve_version: (Hash[untyped, untyped] meta, String? version) -> String?
|
|
20
|
+
|
|
21
|
+
def get: (String url) -> String
|
|
22
|
+
|
|
23
|
+
def download_file: (String url, String destination_path) -> void
|
|
24
|
+
|
|
25
|
+
def save_body: (Net::HTTPResponse response, String destination_path) -> void
|
|
26
|
+
|
|
27
|
+
def request: (String url, read_timeout: Integer, ?redirects_left: Integer) { (Net::HTTP, URI::HTTP) -> untyped } -> Net::HTTPResponse
|
|
28
|
+
|
|
29
|
+
def follow: (Net::HTTPResponse response, String url, read_timeout: Integer, redirects_left: Integer) { (Net::HTTP, URI::HTTP) -> untyped } -> Net::HTTPResponse
|
|
30
|
+
end
|
|
31
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: fhir_packages_manager
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Pavel Rozhkov
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-07-10 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Resolves FHIR implementation guide packages against registries such as
|
|
14
|
+
packages.fhir.org and packages.simplifier.net, skips entries on a configurable ignore
|
|
15
|
+
list, and downloads the resulting tarballs into a folder.
|
|
16
|
+
email:
|
|
17
|
+
- prozskov@gmail.com
|
|
18
|
+
- pavel.r@beda.software
|
|
19
|
+
executables:
|
|
20
|
+
- fhir_packages_manager
|
|
21
|
+
extensions: []
|
|
22
|
+
extra_rdoc_files: []
|
|
23
|
+
files:
|
|
24
|
+
- CHANGELOG.md
|
|
25
|
+
- CODE_OF_CONDUCT.md
|
|
26
|
+
- LICENSE.txt
|
|
27
|
+
- README.md
|
|
28
|
+
- Rakefile
|
|
29
|
+
- exe/fhir_packages_manager
|
|
30
|
+
- lib/fhir_packages_manager.rb
|
|
31
|
+
- lib/fhir_packages_manager/cli.rb
|
|
32
|
+
- lib/fhir_packages_manager/errors.rb
|
|
33
|
+
- lib/fhir_packages_manager/fetch_result.rb
|
|
34
|
+
- lib/fhir_packages_manager/ignore_list.rb
|
|
35
|
+
- lib/fhir_packages_manager/manager.rb
|
|
36
|
+
- lib/fhir_packages_manager/package.rb
|
|
37
|
+
- lib/fhir_packages_manager/registry.rb
|
|
38
|
+
- lib/fhir_packages_manager/version.rb
|
|
39
|
+
- sig/fhir_packages_manager.rbs
|
|
40
|
+
- sig/fhir_packages_manager/cli.rbs
|
|
41
|
+
- sig/fhir_packages_manager/errors.rbs
|
|
42
|
+
- sig/fhir_packages_manager/fetch_result.rbs
|
|
43
|
+
- sig/fhir_packages_manager/ignore_list.rbs
|
|
44
|
+
- sig/fhir_packages_manager/manager.rbs
|
|
45
|
+
- sig/fhir_packages_manager/package.rbs
|
|
46
|
+
- sig/fhir_packages_manager/registry.rbs
|
|
47
|
+
- sig/fhir_packages_manager/version.rbs
|
|
48
|
+
homepage: https://github.com/projkov/fhir_packages_manager
|
|
49
|
+
licenses:
|
|
50
|
+
- MIT
|
|
51
|
+
metadata:
|
|
52
|
+
source_code_uri: https://github.com/projkov/fhir_packages_manager
|
|
53
|
+
documentation_uri: https://projkov.github.io/fhir_packages_manager/
|
|
54
|
+
changelog_uri: https://github.com/projkov/fhir_packages_manager/blob/main/CHANGELOG.md
|
|
55
|
+
rubygems_mfa_required: 'true'
|
|
56
|
+
post_install_message:
|
|
57
|
+
rdoc_options: []
|
|
58
|
+
require_paths:
|
|
59
|
+
- lib
|
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
61
|
+
requirements:
|
|
62
|
+
- - ">="
|
|
63
|
+
- !ruby/object:Gem::Version
|
|
64
|
+
version: 3.2.0
|
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '0'
|
|
70
|
+
requirements: []
|
|
71
|
+
rubygems_version: 3.5.22
|
|
72
|
+
signing_key:
|
|
73
|
+
specification_version: 4
|
|
74
|
+
summary: Check availability of and download FHIR packages from npm-style registries.
|
|
75
|
+
test_files: []
|