From da48bbf31237b354a8dd94bd7550f4548437cb1c Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Thu, 30 May 2019 18:31:22 -0500 Subject: [PATCH] Add support for partial POST to '/api/v1/auth/preferences' --- src/invidious.cr | 4 +- src/invidious/helpers/macros.cr | 54 ++++---- src/invidious/helpers/patch_mapping.cr | 166 +++++++++++++++++++++++++ 3 files changed, 195 insertions(+), 29 deletions(-) create mode 100644 src/invidious/helpers/patch_mapping.cr diff --git a/src/invidious.cr b/src/invidious.cr index 01b1063a..7ff3fb3d 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -1880,7 +1880,7 @@ post "/data_control" do |env| end if body["preferences"]? - user.preferences = Preferences.from_json(body["preferences"].to_json) + user.preferences = Preferences.from_json(body["preferences"].to_json, user.preferences) PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", user.preferences.to_json, user.email) end when "import_youtube" @@ -4468,7 +4468,7 @@ post "/api/v1/auth/preferences" do |env| user = env.get("user").as(User) begin - preferences = Preferences.from_json(env.request.body || "{}") + preferences = Preferences.from_json(env.request.body || "{}", user.preferences) rescue preferences = user.preferences end diff --git a/src/invidious/helpers/macros.cr b/src/invidious/helpers/macros.cr index fe1fc94e..ddfb9f8e 100644 --- a/src/invidious/helpers/macros.cr +++ b/src/invidious/helpers/macros.cr @@ -1,49 +1,49 @@ macro db_mapping(mapping) - def initialize({{*mapping.keys.map { |id| "@#{id}".id }}}) - end + def initialize({{*mapping.keys.map { |id| "@#{id}".id }}}) + end - def to_a - return [ {{*mapping.keys.map { |id| "@#{id}".id }}} ] - end + def to_a + return [ {{*mapping.keys.map { |id| "@#{id}".id }}} ] + end - def self.to_type_tuple - return { {{*mapping.keys.map { |id| "#{id}" }}} } - end + def self.to_type_tuple + return { {{*mapping.keys.map { |id| "#{id}" }}} } + end - DB.mapping( {{mapping}} ) + DB.mapping( {{mapping}} ) end macro json_mapping(mapping) - def initialize({{*mapping.keys.map { |id| "@#{id}".id }}}) - end + def initialize({{*mapping.keys.map { |id| "@#{id}".id }}}) + end - def to_a - return [ {{*mapping.keys.map { |id| "@#{id}".id }}} ] - end + def to_a + return [ {{*mapping.keys.map { |id| "@#{id}".id }}} ] + end - JSON.mapping( {{mapping}} ) - YAML.mapping( {{mapping}} ) + patched_json_mapping( {{mapping}} ) + YAML.mapping( {{mapping}} ) end macro yaml_mapping(mapping) - def initialize({{*mapping.keys.map { |id| "@#{id}".id }}}) - end + def initialize({{*mapping.keys.map { |id| "@#{id}".id }}}) + end - def to_a - return [ {{*mapping.keys.map { |id| "@#{id}".id }}} ] - end + def to_a + return [ {{*mapping.keys.map { |id| "@#{id}".id }}} ] + end - def to_tuple - return { {{*mapping.keys.map { |id| "@#{id}".id }}} } - end + def to_tuple + return { {{*mapping.keys.map { |id| "@#{id}".id }}} } + end - YAML.mapping({{mapping}}) + YAML.mapping({{mapping}}) end macro templated(filename, template = "template") - render "src/invidious/views/#{{{filename}}}.ecr", "src/invidious/views/#{{{template}}}.ecr" + render "src/invidious/views/#{{{filename}}}.ecr", "src/invidious/views/#{{{template}}}.ecr" end macro rendered(filename) - render "src/invidious/views/#{{{filename}}}.ecr" + render "src/invidious/views/#{{{filename}}}.ecr" end diff --git a/src/invidious/helpers/patch_mapping.cr b/src/invidious/helpers/patch_mapping.cr new file mode 100644 index 00000000..8360caa6 --- /dev/null +++ b/src/invidious/helpers/patch_mapping.cr @@ -0,0 +1,166 @@ +# Overloads https://github.com/crystal-lang/crystal/blob/0.28.0/src/json/from_json.cr#L24 +def Object.from_json(string_or_io, default) : self + parser = JSON::PullParser.new(string_or_io) + new parser, default +end + +# Adds configurable 'default' to +macro patched_json_mapping(_properties_, strict = false) + {% for key, value in _properties_ %} + {% _properties_[key] = {type: value} unless value.is_a?(HashLiteral) || value.is_a?(NamedTupleLiteral) %} + {% end %} + + {% for key, value in _properties_ %} + {% _properties_[key][:key_id] = key.id.gsub(/\?$/, "") %} + {% end %} + + {% for key, value in _properties_ %} + @{{value[:key_id]}} : {{value[:type]}}{{ (value[:nilable] ? "?" : "").id }} + + {% if value[:setter] == nil ? true : value[:setter] %} + def {{value[:key_id]}}=(_{{value[:key_id]}} : {{value[:type]}}{{ (value[:nilable] ? "?" : "").id }}) + @{{value[:key_id]}} = _{{value[:key_id]}} + end + {% end %} + + {% if value[:getter] == nil ? true : value[:getter] %} + def {{key.id}} : {{value[:type]}}{{ (value[:nilable] ? "?" : "").id }} + @{{value[:key_id]}} + end + {% end %} + + {% if value[:presence] %} + @{{value[:key_id]}}_present : Bool = false + + def {{value[:key_id]}}_present? + @{{value[:key_id]}}_present + end + {% end %} + {% end %} + + def initialize(%pull : ::JSON::PullParser, default = nil) + {% for key, value in _properties_ %} + %var{key.id} = nil + %found{key.id} = false + {% end %} + + %location = %pull.location + begin + %pull.read_begin_object + rescue exc : ::JSON::ParseException + raise ::JSON::MappingError.new(exc.message, self.class.to_s, nil, *%location, exc) + end + while %pull.kind != :end_object + %key_location = %pull.location + key = %pull.read_object_key + case key + {% for key, value in _properties_ %} + when {{value[:key] || value[:key_id].stringify}} + %found{key.id} = true + begin + %var{key.id} = + {% if value[:nilable] || value[:default] != nil %} %pull.read_null_or { {% end %} + + {% if value[:root] %} + %pull.on_key!({{value[:root]}}) do + {% end %} + + {% if value[:converter] %} + {{value[:converter]}}.from_json(%pull) + {% elsif value[:type].is_a?(Path) || value[:type].is_a?(Generic) %} + {{value[:type]}}.new(%pull) + {% else %} + ::Union({{value[:type]}}).new(%pull) + {% end %} + + {% if value[:root] %} + end + {% end %} + + {% if value[:nilable] || value[:default] != nil %} } {% end %} + rescue exc : ::JSON::ParseException + raise ::JSON::MappingError.new(exc.message, self.class.to_s, {{value[:key] || value[:key_id].stringify}}, *%key_location, exc) + end + {% end %} + else + {% if strict %} + raise ::JSON::MappingError.new("Unknown JSON attribute: #{key}", self.class.to_s, nil, *%key_location, nil) + {% else %} + %pull.skip + {% end %} + end + end + %pull.read_next + + {% for key, value in _properties_ %} + {% unless value[:nilable] || value[:default] != nil %} + if %var{key.id}.nil? && !%found{key.id} && !::Union({{value[:type]}}).nilable? + raise ::JSON::MappingError.new("Missing JSON attribute: {{(value[:key] || value[:key_id]).id}}", self.class.to_s, nil, *%location, nil) + end + {% end %} + + {% if value[:nilable] %} + {% if value[:default] != nil %} + @{{value[:key_id]}} = %found{key.id} ? %var{key.id} : (default.responds_to?(:{{value[:key_id]}}) ? default.{{value[:key_id]}} : {{value[:default]}}) + {% else %} + @{{value[:key_id]}} = %var{key.id} + {% end %} + {% elsif value[:default] != nil %} + @{{value[:key_id]}} = %var{key.id}.nil? ? (default.responds_to?(:{{value[:key_id]}}) ? default.{{value[:key_id]}} : {{value[:default]}}) : %var{key.id} + {% else %} + @{{value[:key_id]}} = (%var{key.id}).as({{value[:type]}}) + {% end %} + + {% if value[:presence] %} + @{{value[:key_id]}}_present = %found{key.id} + {% end %} + {% end %} + end + + def to_json(json : ::JSON::Builder) + json.object do + {% for key, value in _properties_ %} + _{{value[:key_id]}} = @{{value[:key_id]}} + + {% unless value[:emit_null] %} + unless _{{value[:key_id]}}.nil? + {% end %} + + json.field({{value[:key] || value[:key_id].stringify}}) do + {% if value[:root] %} + {% if value[:emit_null] %} + if _{{value[:key_id]}}.nil? + nil.to_json(json) + else + {% end %} + + json.object do + json.field({{value[:root]}}) do + {% end %} + + {% if value[:converter] %} + if _{{value[:key_id]}} + {{ value[:converter] }}.to_json(_{{value[:key_id]}}, json) + else + nil.to_json(json) + end + {% else %} + _{{value[:key_id]}}.to_json(json) + {% end %} + + {% if value[:root] %} + {% if value[:emit_null] %} + end + {% end %} + end + end + {% end %} + end + + {% unless value[:emit_null] %} + end + {% end %} + {% end %} + end + end +end