@@ -29,6 +29,16 @@ def self.from_hash(formula_hash)
2929 :stable ,
3030 ] . freeze
3131
32+ SKIP_SERIALIZATION = [
33+ # Bottle checksums have special serialization done by the serialize_bottle method
34+ :bottle_checksums ,
35+ ] . freeze
36+
37+ SPECS = [ :head , :stable ] . freeze
38+
39+ # :any_skip_relocation is the most common in homebrew/core
40+ DEFAULT_CELLAR = :any_skip_relocation
41+
3242 DependsOnArgs = T . type_alias do
3343 T . any (
3444 # Dependencies
@@ -83,7 +93,7 @@ def self.from_hash(formula_hash)
8393 const :desc , String
8494 const :disable_args , T ::Hash [ Symbol , T . nilable ( T . any ( String , Symbol ) ) ] , default : { }
8595 const :head_dependencies , T ::Array [ DependsOnArgs ] , default : [ ]
86- const :head_url_args , [ String , T ::Hash [ Symbol , T . anything ] ]
96+ const :head_url_args , [ String , T ::Hash [ Symbol , T . anything ] ] , default : [ "" , { } ]
8797 const :head_uses_from_macos , T ::Array [ UsesFromMacOSArgs ] , default : [ ]
8898 const :homepage , String
8999 const :keg_only_args , T ::Array [ T . any ( String , Symbol ) ] , default : [ ]
@@ -101,11 +111,201 @@ def self.from_hash(formula_hash)
101111 const :service_run_kwargs , T ::Hash [ Symbol , Homebrew ::Service ::RunParam ] , default : { }
102112 const :stable_dependencies , T ::Array [ DependsOnArgs ] , default : [ ]
103113 const :stable_checksum , T . nilable ( String )
104- const :stable_url_args , [ String , T ::Hash [ Symbol , T . anything ] ]
114+ const :stable_url_args , [ String , T ::Hash [ Symbol , T . anything ] ] , default : [ "" , { } ]
105115 const :stable_uses_from_macos , T ::Array [ UsesFromMacOSArgs ] , default : [ ]
106116 const :stable_version , String
107117 const :version_scheme , Integer , default : 0
108118 const :versioned_formulae , T ::Array [ String ] , default : [ ]
119+
120+ sig { params ( bottle_tag : ::Utils ::Bottles ::Tag ) . returns ( T . nilable ( T ::Hash [ String , T . untyped ] ) ) }
121+ def serialize_bottle ( bottle_tag : ::Utils ::Bottles . tag )
122+ bottle_collector = ::Utils ::Bottles ::Collector . new
123+ bottle_checksums . each do |bottle_info |
124+ bottle_info = bottle_info . dup
125+ cellar = bottle_info . delete ( :cellar ) || :any
126+ tag = T . must ( bottle_info . keys . first )
127+ checksum = T . cast ( bottle_info . values . first , String )
128+
129+ bottle_collector . add (
130+ ::Utils ::Bottles ::Tag . from_symbol ( tag ) ,
131+ checksum : Checksum . new ( checksum ) ,
132+ cellar :,
133+ )
134+ end
135+ return unless ( bottle_spec = bottle_collector . specification_for ( bottle_tag ) )
136+
137+ tag = ( bottle_spec . tag if bottle_spec . tag != bottle_tag )
138+ cellar = ( bottle_spec . cellar if bottle_spec . cellar != DEFAULT_CELLAR )
139+
140+ {
141+ "bottle_tag" => tag &.to_sym ,
142+ "bottle_cellar" => cellar ,
143+ "bottle_checksum" => bottle_spec . checksum . to_s ,
144+ }
145+ end
146+
147+ sig { params ( bottle_tag : ::Utils ::Bottles ::Tag ) . returns ( T ::Hash [ String , T . untyped ] ) }
148+ def serialize ( bottle_tag : ::Utils ::Bottles . tag )
149+ hash = self . class . decorator . all_props . filter_map do |prop |
150+ next if PREDICATES . any? { |predicate | prop == :"#{ predicate } _present" }
151+ next if SKIP_SERIALIZATION . include? ( prop )
152+
153+ [ prop . to_s , send ( prop ) ]
154+ end . to_h
155+
156+ if ( bottle_hash = serialize_bottle ( bottle_tag :) )
157+ hash = hash . merge ( bottle_hash )
158+ end
159+
160+ hash = self . class . deep_stringify_symbols ( hash )
161+ self . class . deep_compact_blank ( hash )
162+ end
163+
164+ sig { params ( hash : T ::Hash [ String , T . untyped ] , bottle_tag : ::Utils ::Bottles ::Tag ) . returns ( FormulaStruct ) }
165+ def self . deserialize ( hash , bottle_tag : ::Utils ::Bottles . tag )
166+ hash = deep_unstringify_symbols ( hash )
167+
168+ # Items that don't follow the `hash["foo_present"] = hash["foo_args"].present?` pattern are overridden below
169+ PREDICATES . each do |name |
170+ hash [ "#{ name } _present" ] = hash [ "#{ name } _args" ] . present?
171+ end
172+
173+ if ( bottle_checksum = hash [ "bottle_checksum" ] )
174+ tag = hash . fetch ( "bottle_tag" , bottle_tag . to_sym )
175+ cellar = hash . fetch ( "bottle_cellar" , DEFAULT_CELLAR )
176+
177+ hash [ "bottle_present" ] = true
178+ hash [ "bottle_checksums" ] = [ { cellar : cellar , tag => bottle_checksum } ]
179+ else
180+ hash [ "bottle_present" ] = false
181+ end
182+
183+ # *_url_args need to be in [String, Hash] format, but the hash may have been dropped if empty
184+ SPECS . each do |key |
185+ if ( url_args = hash [ "#{ key } _url_args" ] )
186+ hash [ "#{ key } _present" ] = true
187+ hash [ "#{ key } _url_args" ] = format_arg_pair ( url_args , last : { } )
188+ else
189+ hash [ "#{ key } _present" ] = false
190+ end
191+
192+ next unless ( uses_from_macos = hash [ "#{ key } _uses_from_macos" ] )
193+
194+ hash [ "#{ key } _uses_from_macos" ] = uses_from_macos . map do |args |
195+ format_arg_pair ( args , last : { } )
196+ end
197+ end
198+
199+ hash [ "service_args" ] = if ( service_args = hash [ "service_args" ] )
200+ service_args . map { |service_arg | format_arg_pair ( service_arg , last : nil ) }
201+ end
202+
203+ hash [ "conflicts" ] = if ( conflicts = hash [ "conflicts" ] )
204+ conflicts . map { |conflict | format_arg_pair ( conflict , last : { } ) }
205+ end
206+
207+ from_hash ( hash )
208+ end
209+
210+ # Format argument pairs into proper [first, last] format if serialization has removed some elements.
211+ # Pass a default value for last to be used when only one element is present.
212+ #
213+ # format_arg_pair(["foo"], last: {}) # => ["foo", {}]
214+ # format_arg_pair([{ "foo" => :build }], last: {}) # => [{ "foo" => :build }, {}]
215+ # format_arg_pair(["foo", { since: :catalina }], last: {}) # => ["foo", { since: :catalina }]
216+ sig {
217+ type_parameters ( :U , :V )
218+ . params (
219+ args : T . any ( [ T . type_parameter ( :U ) ] , [ T . type_parameter ( :U ) , T . type_parameter ( :V ) ] ) ,
220+ last : T . type_parameter ( :V ) ,
221+ ) . returns ( [ T . type_parameter ( :U ) , T . type_parameter ( :V ) ] )
222+ }
223+ def self . format_arg_pair ( args , last :)
224+ args = case args
225+ in [ elem ]
226+ [ elem , last ]
227+ in [ elem1 , elem2 ]
228+ [ elem1 , elem2 ]
229+ end
230+
231+ # The case above is exhaustive so args will never be nil, but sorbet cannot infer that.
232+ T . must ( args )
233+ end
234+
235+ # Converts a symbol to a string starting with `:`, otherwise returns the input.
236+ #
237+ # stringify_symbol(:example) # => ":example"
238+ # stringify_symbol("example") # => "example"
239+ sig { params ( value : T . any ( String , Symbol ) ) . returns ( T . nilable ( String ) ) }
240+ def self . stringify_symbol ( value )
241+ return ":#{ value } " if value . is_a? ( Symbol )
242+
243+ value
244+ end
245+
246+ sig { params ( obj : T . untyped ) . returns ( T . untyped ) }
247+ def self . deep_stringify_symbols ( obj )
248+ case obj
249+ when String
250+ # Escape leading : or \ to avoid confusion with stringified symbols
251+ # ":foo" -> "\:foo"
252+ # "\foo" -> "\\foo"
253+ if obj . start_with? ( ":" , "\\ " )
254+ "\\ #{ obj } "
255+ else
256+ obj
257+ end
258+ when Symbol
259+ ":#{ obj } "
260+ when Hash
261+ obj . to_h { |k , v | [ deep_stringify_symbols ( k ) , deep_stringify_symbols ( v ) ] }
262+ when Array
263+ obj . map { |v | deep_stringify_symbols ( v ) }
264+ else
265+ obj
266+ end
267+ end
268+
269+ sig { params ( obj : T . untyped ) . returns ( T . untyped ) }
270+ def self . deep_unstringify_symbols ( obj )
271+ case obj
272+ when String
273+ if obj . start_with? ( "\\ " )
274+ obj [ 1 ..]
275+ elsif obj . start_with? ( ":" )
276+ T . must ( obj [ 1 ..] ) . to_sym
277+ else
278+ obj
279+ end
280+ when Hash
281+ obj . to_h { |k , v | [ deep_unstringify_symbols ( k ) , deep_unstringify_symbols ( v ) ] }
282+ when Array
283+ obj . map { |v | deep_unstringify_symbols ( v ) }
284+ else
285+ obj
286+ end
287+ end
288+
289+ sig {
290+ type_parameters ( :U )
291+ . params ( obj : T . all ( T . type_parameter ( :U ) , Object ) )
292+ . returns ( T . nilable ( T . type_parameter ( :U ) ) )
293+ }
294+ def self . deep_compact_blank ( obj )
295+ obj = case obj
296+ when Hash
297+ obj . transform_values { |v | deep_compact_blank ( v ) }
298+ . compact
299+ when Array
300+ obj . filter_map { |v | deep_compact_blank ( v ) }
301+ else
302+ obj
303+ end
304+
305+ return if obj . blank? || ( obj . is_a? ( Numeric ) && obj . zero? )
306+
307+ obj
308+ end
109309 end
110310 end
111311end
0 commit comments