#!/usr/bin/env ruby require 'set' require 'yaml' # clear_parameters clears the parameters from the spec that are in the # parameters set. It expects to clear the parameters, and if not found, # it will raise an error, to ensure that we're updating the parameter # modifications as the original spec changes. # To clear by name, use the name as a string. # To clear by name and in, use an array of [name, in]. def clear_parameters(spec, path, method, parameters) found = Set.new parameters = Set.new(parameters) spec['paths'][path][method]['parameters'].each do |parameter| if parameters.include?(parameter['name']) found.add(parameter['name']) parameters.delete(parameter['name']) elsif parameters.include?([parameter['name'], parameter['in']]) found.add([parameter['name'], parameter['in']]) parameters.delete([parameter['name'], parameter['in']]) end end raise "Parameter#{parameters.length > 1 ? 's' : ''} not found: #{parameters.to_a.sort.map(&:inspect).join(', ')}" unless parameters.empty? spec['paths'][path][method]['parameters'].reject! do |parameter| found.include?(parameter['name']) || found.include?([parameter['name'], parameter['in']]) end end def process(spec) spec['paths'].each do |path, methods| methods.each do |method, details| if details['parameters'] details['parameters'].each do |parameter| if parameter['in'] == 'query' && parameter['required'] == false # in: query implies required: false, so we can remove it parameter.delete('required') end end # in: path but not in path, so we should remove it details['parameters'].reject! { |p| p['in'] == 'path' && !path.include?("{#{p['name']}}") } # in: query, but also in: path, so we should remove query path_parameters = details['parameters'].select { |p| p['in'] == 'path' }.map { |p| p['name'] } details['parameters'].reject! { |p| p['in'] == 'query' && path_parameters.include?(p['name']) } end end end # Seemingly unused parameters clear_parameters(spec, '/api2/categories/', 'get', %w[id long_name short_name url]) clear_parameters(spec, '/api2/categories/{bare_id}/', 'get', %w[id long_name short_name url]) clear_parameters(spec, '/api2/user-profiles/', 'get', %w[ask_when_reaffirm_question_modal date_joined default_community_visibility default_mp_visibility email first_name formerly_known_as id is_staff is_superuser last_name last_visited level levelTitle permissions purchasable_track_record score show_profile_comments supporter_level supporter_since url username can_change_username]) clear_parameters(spec, '/api2/user-profiles/{id}/', 'get', %w[ask_when_reaffirm_question_modal date_joined default_community_visibility default_mp_visibility email first_name formerly_known_as is_staff is_superuser last_name last_visited level levelTitle permissions purchasable_track_record score show_profile_comments supporter_level supporter_since url username can_change_username]) clear_parameters(spec, '/api2/user-profiles/{id}/', 'put', %w[ask_when_reaffirm_question_modal date_joined default_community_visibility default_mp_visibility email first_name formerly_known_as is_staff is_superuser last_name last_visited level levelTitle permissions purchasable_track_record score show_profile_comments supporter_level supporter_since url username can_change_username]) clear_parameters(spec, '/api2/user-profiles/{id}/', 'patch', %w[ask_when_reaffirm_question_modal date_joined default_community_visibility default_mp_visibility email first_name formerly_known_as is_staff is_superuser last_name last_visited level levelTitle permissions purchasable_track_record score show_profile_comments supporter_level supporter_since url username can_change_username]) clear_parameters(spec, '/api2/users/', 'get', %w[ask_when_reaffirm_question_modal date_joined default_community_visibility default_mp_visibility email first_name formerly_known_as id is_staff is_superuser last_name last_visited level levelTitle permissions purchasable_track_record score show_profile_comments supporter_level supporter_since url username can_change_username]) clear_parameters(spec, '/api2/users/{id}/', 'get', %w[ask_when_reaffirm_question_modal date_joined default_community_visibility default_mp_visibility email first_name formerly_known_as is_staff is_superuser last_name last_visited level levelTitle permissions purchasable_track_record score show_profile_comments supporter_level supporter_since url username can_change_username]) clear_parameters(spec, '/api2/users/{id}/', 'put', %w[ask_when_reaffirm_question_modal date_joined default_community_visibility default_mp_visibility email first_name formerly_known_as is_staff is_superuser last_name last_visited level levelTitle permissions purchasable_track_record score show_profile_comments supporter_level supporter_since url username can_change_username]) clear_parameters(spec, '/api2/users/{id}/', 'patch', %w[ask_when_reaffirm_question_modal date_joined default_community_visibility default_mp_visibility email first_name formerly_known_as is_staff is_superuser last_name last_visited level levelTitle permissions purchasable_track_record score show_profile_comments supporter_level supporter_since url username can_change_username]) clear_parameters(spec, '/api2/users/global-cp-reminder/', 'get', %w[ask_when_reaffirm_question_modal date_joined default_community_visibility default_mp_visibility email first_name formerly_known_as id is_staff is_superuser last_name last_visited level levelTitle permissions purchasable_track_record score show_profile_comments supporter_level supporter_since url username can_change_username]) clear_parameters(spec, '/api2/users/global-cp-reminder/', 'post', %w[ask_when_reaffirm_question_modal date_joined default_community_visibility default_mp_visibility email first_name formerly_known_as id is_staff is_superuser last_name last_visited level levelTitle permissions purchasable_track_record score show_profile_comments supporter_level supporter_since url username can_change_username]) schemas_were_sorted = spec['components']['schemas'].keys == spec['components']['schemas'].keys.sort if spec['paths']['/api2/about-numbers/']['get']['responses']['200'] == { 'description' => 'No response body' } # Add AboutNumbers response schema based on observed response spec['paths']['/api2/about-numbers/']['get']['responses']['200'] = { 'content' => { 'application/json' => { 'schema' => { '$ref' => '#/components/schemas/AboutNumbers' } } }, 'description' => '' } spec['components']['schemas']['AboutNumbers'] = { 'type' => 'object', 'properties' => { 'predictions' => { 'type' => 'integer' }, 'questions' => { 'type' => 'integer' }, 'resolved_questions' => { 'type' => 'integer' }, 'years_of_predictions' => { 'type' => 'integer' } }, 'required' => ['predictions', 'questions', 'resolved_questions', 'years_of_predictions'] } check_reminder_schema = false reminder_schema_lists = [ spec['paths']['/api2/users/global-cp-reminder/']['get']['responses']['200']['content'], spec['paths']['/api2/users/global-cp-reminder/']['post']['requestBody']['content'], spec['paths']['/api2/users/global-cp-reminder/']['post']['responses']['200']['content'] ] reminder_schema_lists.each do |list| list.each do |content_type, content| if content['schema']['$ref'] == '#/components/schemas/User' content['schema']['$ref'] = '#/components/schemas/GlobalCPReminder' check_reminder_schema = true end end end if check_reminder_schema spec['components']['schemas']['GlobalCPReminder'] ||= { 'type' => 'object', 'properties' => { 'enabled' => { 'type' => 'boolean' }, 'delta' => { 'type' => 'integer' } }, 'required' => ['enabled', 'delta'] } end if spec['paths']['/api2/questions/{id}/prediction-history/']['get']['responses']['200'] == { 'description' => 'No response body' } # Add PredictionHistory response schema based on observed response spec['paths']['/api2/questions/{id}/prediction-history/']['get']['responses']['200'] = { 'content' => { 'application/json' => { 'schema' => { '$ref' => '#/components/schemas/PredictionHistory' } } }, 'description' => '' } spec['components']['schemas']['PredictionHistory'] = { 'type' => 'object', 'properties' => { 'question' => { 'type' => 'integer' }, 'community_prediction' => { 'type' => 'array', 'items' => { '$ref' => '#/components/schemas/PredictionHistoryTime' } }, 'metaculus_prediction' => { 'type' => 'array', 'items' => { '$ref' => '#/components/schemas/PredictionHistoryTime' } }, }, 'required' => ['question', 'community_prediction', 'metaculus_prediction'] } spec['components']['schemas']['PredictionHistoryTime'] = { 'type' => 'object', 'properties' => { 't' => { 'type' => 'number', 'format' => 'double' }, 'y' => { 'type' => 'array', 'items' => { 'type' => 'number', 'format' => 'double' } }, 'q1' => { 'type' => 'number', 'format' => 'double' }, 'q2' => { 'type' => 'number', 'format' => 'double' }, 'q3' => { 'type' => 'number', 'format' => 'double' }, 'low' => { 'type' => 'number', 'format' => 'double' }, 'high' => { 'type' => 'number', 'format' => 'double' }, }, 'required' => ['t', 'y', 'q1', 'q2', 'q3', 'low', 'high'] } end # /api2/questions/{id}/predictions/ if spec['paths']['/api2/questions/{id}/predictions/']['get']['responses']['200'] == { 'description' => 'No response body' } # Add [ExtendedPredictionUsername] response schema based on observed response spec['paths']['/api2/questions/{id}/predictions/']['get']['responses']['200'] = { 'content' => { 'application/json' => { 'schema' => { 'type' => 'array', 'items' => { '$ref' => '#/components/schemas/ExtendedPredictionUsername' } } } }, 'description' => '' } end check_predictions_schemas = ['ExtendedPredictionUsername', 'Prediction', 'PredictionUsername'] check_predictions_schemas.each do |schema| if spec['components']['schemas'][schema]['properties']['predictions']['type'] == 'object' spec['components']['schemas'][schema]['properties']['predictions'] = { 'type' => 'array', 'items' => { 'type' => 'object', 'additionalProperties' => {} } } else raise "#{schema} predictions already fixed" end end # Make effected_close_time nullable based on observed responses # I have not been able to check the others: # * Considerations # * PatchedQuestionUpdate # * Question # * QuestionRelated # * QuestionUpdate check_effected_close_time_schemas = ['QuestionUser', 'QuestionUserDetail', 'SubQuestionUserDetail', 'SubQuestionUserList'] check_effected_close_time_schemas.each do |schema| if spec['components']['schemas'][schema]['properties']['effected_close_time']['nullable'] raise "#{schema} effected_close_time already nullable" end spec['components']['schemas'][schema]['properties']['effected_close_time']['nullable'] = true end # These have been passed back missing, so they should not be required, as it # causes a deserialize error remove_required = { 'QuestionUser' => [ 'comment_count_snapshot', 'metaculus_prediction', 'user_community_vis', ] } remove_required.each do |schema, properties| properties.each do |property| spec['components']['schemas'][schema]['required'].delete(property) end end if schemas_were_sorted spec['components']['schemas'] = spec['components']['schemas'].sort.to_h end end # Remove empty parameters (that we caused earlier) spec['paths'].each do |path, methods| methods.each do |method, details| details.delete('parameters') if details['parameters'] == [] end end # Add better enum names # Also re-orders e.g. ValueEnum so 0 (Neutral) is first for Default impl. better_enum_names = { 'UserCommunityVisEnum' => { 0 => 'Depends', -1 => 'False', 1 => 'True' }, 'ValueEnum' => { 0 => 'Neutral', -1 => 'Down', 1 => 'Up' }, 'NotificationTypeEnum' => { 'Q' => 'QuestionResolved', 'M' => 'Mentioned', 'S' => 'ClosingSoon' }, 'QuestionUpdateStatusEnum' => { 'V' => 'Private', 'T' => 'Draft', 'I' => 'Pending' }, 'Status3baEnum' => { 'V' => 'Private', 'T' => 'Draft', 'I' => 'Pending', 'A' => 'Active', 'R' => 'Rejected', 'D' => 'Deleted' } } better_enum_names.each do |schema, enum_names| if spec['components']['schemas'][schema]['x-enum-varnames'] raise "#{schema} enum names already set" end if spec['components']['schemas'][schema]['enum'].to_set != enum_names.keys.to_set raise "#{schema} enum values do not match" end spec['components']['schemas'][schema]['x-enum-varnames'] = enum_names.values spec['components']['schemas'][schema]['enum'] = enum_names.keys end spec['components']['schemas']['UserCommunityVisEnum']['x-enum-varnames'] # Add servers config to aid generated code and docs spec['servers'] = [{ 'url' => 'https://www.metaculus.com' }] spec end # Not safe, don't pwn me Metaculus spec = open('Metaculus API (1.0).yaml', 'r') { |f| YAML.load(f) } open('Metaculus API (1.0) Modified.yaml', 'w') { |f| f.write(YAML.dump(process(spec))) }