-
-
Notifications
You must be signed in to change notification settings - Fork 11k
Serialize CaskStructs in the internal API
#21502
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,126 @@ | ||||||||||||||
| # typed: strict | ||||||||||||||
| # frozen_string_literal: true | ||||||||||||||
|
|
||||||||||||||
| module Homebrew | ||||||||||||||
| module API | ||||||||||||||
| module Cask | ||||||||||||||
| # Methods for generating CaskStruct instances from API data. | ||||||||||||||
| module CaskStructGenerator | ||||||||||||||
| module_function | ||||||||||||||
|
|
||||||||||||||
| # NOTE: this will be used to load installed cask JSON files, | ||||||||||||||
| # so it must never fail with older JSON API versions | ||||||||||||||
| sig { params(hash: T::Hash[String, T.untyped], bottle_tag: Utils::Bottles::Tag, ignore_types: T::Boolean).returns(CaskStruct) } | ||||||||||||||
| def generate_cask_struct_hash(hash, bottle_tag: Utils::Bottles.tag, ignore_types: false) | ||||||||||||||
| hash = Homebrew::API.merge_variations(hash).dup.deep_symbolize_keys.transform_keys(&:to_s) | ||||||||||||||
|
|
||||||||||||||
| hash["conflicts_with_args"] = hash["conflicts_with"] | ||||||||||||||
|
|
||||||||||||||
| hash["container_args"] = hash["container"]&.to_h do |key, value| | ||||||||||||||
| next [key, value.to_sym] if key == :type | ||||||||||||||
|
|
||||||||||||||
| [key, value] | ||||||||||||||
| end | ||||||||||||||
|
|
||||||||||||||
| hash["depends_on_args"] = hash["depends_on"]&.to_h do |key, value| | ||||||||||||||
| # Arch dependencies are encoded like `{ type: :intel, bits: 64 }` | ||||||||||||||
| # but `depends_on arch:` only accepts `:intel` or `:arm64` | ||||||||||||||
| if key == :arch | ||||||||||||||
| next [:arch, :intel] if value.first[:type] == "intel" | ||||||||||||||
|
|
||||||||||||||
| next [:arch, :arm64] | ||||||||||||||
| end | ||||||||||||||
|
|
||||||||||||||
| next [key, value] if key != :macos | ||||||||||||||
|
|
||||||||||||||
| dep_type = value.keys.first | ||||||||||||||
| if dep_type == :== | ||||||||||||||
| version_symbols = value[dep_type].filter_map do |version| | ||||||||||||||
| MacOSVersion::SYMBOLS.key(version) | ||||||||||||||
| end | ||||||||||||||
| next [key, version_symbols.presence] | ||||||||||||||
| end | ||||||||||||||
|
|
||||||||||||||
| version_symbol = value[dep_type].first | ||||||||||||||
| version_symbol = MacOSVersion::SYMBOLS.key(version_symbol) | ||||||||||||||
| version_dep = "#{dep_type} :#{version_symbol}" if version_symbol | ||||||||||||||
| [key, version_dep] | ||||||||||||||
| end&.compact_blank | ||||||||||||||
|
|
||||||||||||||
| if (deprecate_args = hash["deprecate_args"]) | ||||||||||||||
| deprecate_args = deprecate_args.dup | ||||||||||||||
| deprecate_args[:because] = | ||||||||||||||
| DeprecateDisable.to_reason_string_or_symbol(deprecate_args[:because], type: :cask) | ||||||||||||||
| hash["deprecate_args"] = deprecate_args | ||||||||||||||
| end | ||||||||||||||
|
|
||||||||||||||
| if (disable_args = hash["disable_args"]) | ||||||||||||||
| disable_args = disable_args.dup | ||||||||||||||
| disable_args[:because] = DeprecateDisable.to_reason_string_or_symbol(disable_args[:because], type: :cask) | ||||||||||||||
| hash["disable_args"] = disable_args | ||||||||||||||
| end | ||||||||||||||
|
|
||||||||||||||
| hash["names"] = hash["name"] | ||||||||||||||
|
|
||||||||||||||
| hash["raw_artifacts"] = hash.fetch("artifacts", []).map do |artifact| | ||||||||||||||
| key = artifact.keys.first | ||||||||||||||
|
|
||||||||||||||
| # Pass an empty block to artifacts like postflight that can't be loaded from the API, | ||||||||||||||
| # but need to be set to something. | ||||||||||||||
| next [key, [], {}, -> {}] if artifact[key].nil? | ||||||||||||||
|
Comment on lines
+68
to
+70
|
||||||||||||||
| # Pass an empty block to artifacts like postflight that can't be loaded from the API, | |
| # but need to be set to something. | |
| next [key, [], {}, -> {}] if artifact[key].nil? | |
| # For artifacts like postflight that can't be loaded from the API but need a block slot, | |
| # use nil for the block to keep the structure consistent and JSON-serializable. | |
| next [key, [], {}, nil] if artifact[key].nil? |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CaskStructGenerator module is being added without corresponding test coverage. The FormulaStructGenerator has comprehensive tests in test/api/formula/formula_struct_generator_spec.rb, establishing a convention that struct generators should be tested. Consider adding tests similar to those in formula_struct_generator_spec.rb to verify the generation logic, especially for complex transformations like arch dependencies, macos version handling, and artifact processing.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -93,6 +93,65 @@ def caveats(appdir:) | |||||||||||||||||||||||||||||||||||||||||||||
| deep_remove_placeholders(raw_caveats, appdir.to_s) | ||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| sig { params(bottle_tag: ::Utils::Bottles::Tag).returns(T::Hash[String, T.untyped]) } | ||||||||||||||||||||||||||||||||||||||||||||||
| def serialize(bottle_tag: ::Utils::Bottles.tag) | ||||||||||||||||||||||||||||||||||||||||||||||
| hash = self.class.decorator.all_props.filter_map do |prop| | ||||||||||||||||||||||||||||||||||||||||||||||
| next if PREDICATES.any? { |predicate| prop == :"#{predicate}_present" } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| [prop.to_s, send(prop)] | ||||||||||||||||||||||||||||||||||||||||||||||
| end.to_h | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| hash = ::Utils.deep_stringify_symbols(hash) | ||||||||||||||||||||||||||||||||||||||||||||||
| ::Utils.deep_compact_blank(hash) | ||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| sig { params(hash: T::Hash[String, T.untyped]).returns(CaskStruct) } | ||||||||||||||||||||||||||||||||||||||||||||||
| def self.deserialize(hash) | ||||||||||||||||||||||||||||||||||||||||||||||
| hash = ::Utils.deep_unstringify_symbols(hash) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| # Items that don't follow the `hash["foo_present"] = hash["foo_args"].present?` pattern are overridden below | ||||||||||||||||||||||||||||||||||||||||||||||
| PREDICATES.each do |name| | ||||||||||||||||||||||||||||||||||||||||||||||
| hash["#{name}_present"] = hash["#{name}_args"].present? | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+112
to
+114
|
||||||||||||||||||||||||||||||||||||||||||||||
| # Items that don't follow the `hash["foo_present"] = hash["foo_args"].present?` pattern are overridden below | |
| PREDICATES.each do |name| | |
| hash["#{name}_present"] = hash["#{name}_args"].present? | |
| # Compute predicate *_present flags based on their actual backing fields. | |
| PREDICATES.each do |name| | |
| source_value = | |
| case name | |
| when :auto_updates | |
| hash["auto_updates"] | |
| when :caveats | |
| hash["raw_caveats"] | |
| when :conflicts, :conflicts_with | |
| hash["conflicts_with_args"] | |
| when :desc | |
| hash["desc"] | |
| when :homepage | |
| hash["homepage"] | |
| else | |
| hash["#{name}_args"] | |
| end | |
| hash["#{name}_present"] = source_value.present? |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The serialize and deserialize methods are being added without corresponding test coverage. The FormulaStruct has comprehensive tests in test/api/formula_struct_spec.rb, establishing a convention that serialize/deserialize methods should be tested. Consider adding tests similar to those in formula_struct_spec.rb to verify the serialization/deserialization logic, especially for edge cases like artifacts with empty blocks and predicates with various field types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
bottle_tagparameter is accepted by this method but not passed tomerge_variations. This differs from the FormulaStructGenerator implementation which passes it correctly. The bottle_tag parameter should be passed to ensure variations are correctly merged for the target platform.