module Graphiti class SchemaDiff def initialize(old, new) @old = JSON.parse(old.to_json).deep_symbolize_keys @new = JSON.parse(new.to_json).deep_symbolize_keys @errors = [] end def compare compare_each if @old != @new @errors end private def compare_each compare_resources compare_endpoints compare_types end def compare_resources @old[:resources].each_with_index do |r, index| new_resource = @new[:resources].find { |n| n[:name] == r[:name] } next if new_resource.try(:[], :remote) || r.try(:[], :remote) compare_resource(r, new_resource) do compare_attributes(r, new_resource) compare_defaults(r, new_resource) compare_extra_attributes(r, new_resource) compare_sorts(r, new_resource) compare_filters(r, new_resource) compare_filter_group(r, new_resource) compare_stats(r, new_resource) compare_relationships(r, new_resource) end end end def compare_resource(old_resource, new_resource) unless new_resource @errors << "#{old_resource[:name]} was removed." return end if old_resource[:type] != new_resource[:type] @errors << "#{old_resource[:name]} changed type from #{old_resource[:type].inspect} to #{new_resource[:type].inspect}." end yield end def compare_attributes(old_resource, new_resource) old_resource[:attributes].each_pair do |name, old_att| unless (new_att = new_resource[:attributes][name]) @errors << "#{old_resource[:name]}: attribute #{name.inspect} was removed." next end compare_attribute(old_resource[:name], name, old_att, new_att) end end def compare_defaults(old_resource, new_resource) if new_resource[:default_sort] && !old_resource[:default_sort] @errors << "#{old_resource[:name]}: default sort added." end if old_resource[:default_sort] && !new_resource[:default_sort] @errors << "#{old_resource[:name]}: default sort removed." end if new_resource[:default_sort] && old_resource[:default_sort] if new_resource[:default_sort] != old_resource[:default_sort] @errors << "#{old_resource[:name]}: default sort changed from #{old_resource[:default_sort].inspect} to #{new_resource[:default_sort].inspect}." end end if new_resource[:default_page_size] && !old_resource[:default_page_size] @errors << "#{old_resource[:name]}: default page size added." end if old_resource[:default_page_size] && !new_resource[:default_page_size] @errors << "#{old_resource[:name]}: default page size removed." end if old_resource[:default_page_size] && new_resource[:default_page_size] if old_resource[:default_page_size] != new_resource[:default_page_size] @errors << "#{old_resource[:name]}: default page size changed from #{old_resource[:default_page_size]} to #{new_resource[:default_page_size]}." end end end def compare_relationships(old_resource, new_resource) old_resource[:relationships].each_pair do |name, old_rel| unless (new_rel = new_resource[:relationships][name]) @errors << "#{old_resource[:name]}: relationship #{name.inspect} was removed." next end if new_rel[:single] && !old_rel[:single] @errors << "#{old_resource[:name]}: relationship #{name.inspect} became single: true." next end if new_rel[:resource] != old_rel[:resource] @errors << "#{old_resource[:name]}: relationship #{name.inspect} changed resource from #{old_rel[:resource]} to #{new_rel[:resource]}." end if new_rel[:type] != old_rel[:type] @errors << "#{old_resource[:name]}: relationship #{name.inspect} changed type from #{old_rel[:type].inspect} to #{new_rel[:type].inspect}." end end end def compare_extra_attributes(old_resource, new_resource) old_resource[:extra_attributes].each_pair do |name, old_att| unless (new_att = new_resource[:extra_attributes][name]) @errors << "#{old_resource[:name]}: extra attribute #{name.inspect} was removed." next end compare_attribute(old_resource[:name], name, old_att, new_att, extra: true) end end def compare_sorts(old_resource, new_resource) old_resource[:sorts].each_pair do |name, old_sort| unless (new_sort = new_resource[:sorts][name]) @errors << "#{old_resource[:name]}: sort #{name.inspect} was removed." next end if new_sort[:guard] && !old_sort[:guard] @errors << "#{old_resource[:name]}: sort #{name.inspect} became guarded." end if new_sort[:only] && !old_sort[:only] @errors << "#{old_resource[:name]}: sort #{name.inspect} now limited to only #{new_sort[:only].to_sym.inspect}." end if new_sort[:only] && old_sort[:only] if new_sort[:only] != old_sort[:only] @errors << "#{old_resource[:name]}: sort #{name.inspect} was limited to only #{old_sort[:only].to_sym.inspect}, now limited to only #{new_sort[:only].to_sym.inspect}." end end end end def compare_filters(old_resource, new_resource) old_resource[:filters].each_pair do |name, old_filter| unless (new_filter = new_resource[:filters][name]) @errors << "#{old_resource[:name]}: filter #{name.inspect} was removed." next end if new_filter[:type] != old_filter[:type] @errors << "#{old_resource[:name]}: filter #{name.inspect} changed type from #{old_filter[:type].inspect} to #{new_filter[:type].inspect}." next end if new_filter[:single] && !old_filter[:single] @errors << "#{old_resource[:name]}: filter #{name.inspect} became singular." end if new_filter[:dependencies] && !old_filter[:dependencies] @errors << "#{old_resource[:name]}: filter #{name.inspect} added dependencies #{new_filter[:dependencies].inspect}." end if new_filter[:dependencies] && old_filter[:dependencies] if new_filter[:dependencies] != old_filter[:dependencies] @errors << "#{old_resource[:name]}: filter #{name.inspect} changed dependencies from #{old_filter[:dependencies].inspect} to #{new_filter[:dependencies].inspect}." end end if new_filter[:allow] != old_filter[:allow] new = new_filter[:allow] || [] old = old_filter[:allow] || [] diff = new - old if diff.length > 0 @errors << "#{old_resource[:name]}: filter #{name.inspect} allowlist went from #{old.inspect} to #{new.inspect}." end end if new_filter[:deny] != old_filter[:deny] new = new_filter[:deny] || [] old = old_filter[:deny] || [] diff = new - old if diff.length > 0 @errors << "#{old_resource[:name]}: filter #{name.inspect} denylist went from #{old.inspect} to #{new.inspect}." end end if (diff = old_filter[:operators] - new_filter[:operators]).length > 0 diff.each do |op| @errors << "#{old_resource[:name]}: filter #{name.inspect} removed operator #{op.inspect}." end end if new_filter[:required] && !old_filter[:required] @errors << "#{old_resource[:name]}: filter #{name.inspect} went from optional to required." end if new_filter[:guard] && !old_filter[:guard] @errors << "#{old_resource[:name]}: filter #{name.inspect} went from unguarded to guarded." end end end def compare_filter_group(old_resource, new_resource) if new_resource[:filter_group] if old_resource[:filter_group] new_names = new_resource[:filter_group][:names] old_names = old_resource[:filter_group][:names] diff = new_names - old_names if !diff.empty? && new_resource[:filter_group][:required] == "all" @errors << "#{old_resource[:name]}: all required filter group #{old_names.map(&:to_sym).inspect} added #{"member".pluralize(diff.length)} #{diff.map(&:to_sym).inspect}." end old_required = old_resource[:filter_group][:required] new_required = new_resource[:filter_group][:required] if old_required == "any" && new_required == "all" @errors << "#{old_resource[:name]}: filter group #{old_names.map(&:to_sym).inspect} moved from required: :any to required: :all" end else @errors << "#{old_resource[:name]}: filter group #{new_resource[:filter_group][:names].map(&:to_sym).inspect} was added." end end end def compare_stats(old_resource, new_resource) return unless old_resource.key?(:stats) old_resource[:stats].each_pair do |name, old_calculations| new_calculations = new_resource[:stats][name] if new_calculations old_calculations.each do |calc| unless new_calculations.include?(calc) @errors << "#{old_resource[:name]}: calculation #{calc.to_sym.inspect} was removed from stat #{name.inspect}." end end else @errors << "#{old_resource[:name]}: stat #{name.inspect} was removed." end end end def compare_endpoints @old[:endpoints].each_pair do |path, old_endpoint| unless (new_endpoint = @new[:endpoints][path]) @errors << "Endpoint \"#{path}\" was removed." next end old_endpoint[:actions].each_pair do |name, old_action| unless (new_action = new_endpoint[:actions][name]) @errors << "Endpoint \"#{path}\" removed action #{name.inspect}." next end if new_action[:sideload_allowlist] && !old_action[:sideload_allowlist] @errors << "Endpoint \"#{path}\" added sideload allowlist." end if new_action[:sideload_allowlist] if new_action[:sideload_allowlist] != old_action[:sideload_allowlist] removal = Util::Hash.include_removed? \ new_action[:sideload_allowlist], old_action[:sideload_allowlist] if removal @errors << "Endpoint \"#{path}\" had incompatible sideload allowlist. Was #{old_action[:sideload_allowlist].inspect}, now #{new_action[:sideload_allowlist].inspect}." end end end end end end def compare_types @old[:types].each_pair do |name, old_type| unless (new_type = @new[:types][name]) @errors << "Type #{name.inspect} was removed." next end if new_type[:kind] != old_type[:kind] @errors << "Type #{name.inspect} changed kind from #{old_type[:kind].inspect} to #{new_type[:kind].inspect}." end end end def compare_attribute(resource_name, att_name, old_att, new_att, extra: false) prefix = extra ? "extra attribute" : "attribute" if old_att[:type] != new_att[:type] @errors << "#{resource_name}: #{prefix} #{att_name.inspect} changed type from #{old_att[:type].inspect} to #{new_att[:type].inspect}." end [:readable, :writable].each do |flag| if [true, "guarded"].include?(old_att[flag]) && new_att[flag] == false @errors << "#{resource_name}: #{prefix} #{att_name.inspect} changed flag #{flag.inspect} from #{old_att[flag].inspect} to #{new_att[flag].inspect}." end if new_att[flag] == "guarded" && old_att[flag] == true @errors << "#{resource_name}: #{prefix} #{att_name.inspect} changed flag #{flag.inspect} from #{old_att[flag].inspect} to #{new_att[flag].inspect}." end end end end end