codenoble / cache-crispies

Speedy Rails JSON serialization with built-in caching
MIT License
156 stars 16 forks source link

Add map option #37

Closed sander-deryckere closed 2 years ago

sander-deryckere commented 3 years ago

This PR adds a map option to the serialize calls.

It is similar to the through option, with the difference that the through option uses the name of the sub-call, while the map option uses the name of the parent call.

In the given example, there can be an address having all discrete fields (streetname, city, ...), and some methods for formatting it (full address, just city and state, ...). Then you can map the address to one of the formatters for serialization, while it keeps the address key.

adamcrown commented 3 years ago

Hi @sander-deryckere . Sorry I missed this for so long. I didn't realize I was getting so far behind.

Correct me if I'm wrong, but you could I believe what you're looking to do can already be achieved with

serialize :address, through: :address, from: :full_format

Also, I'm not a fan of the term map here because it makes me think that you would get some sort of array in return.

sander-deryckere commented 3 years ago

@adamcrown , no worries, thanks for your reply.

You can indeed use through, combined with from, but that syntax repeats the serializing name, and looks quite confusing to me. On top of that, the map suggestion I made enables you to combine multiple values for serialization.

Say you have some metrics, modelled as values with units and a formatting display_value function. Then you can map the models to the display_value function to get the formatted value. Something like

serialize :cpu_temp, :cpu_usage, :ram_usage, :disk_usage, map: :display_value

With the existing options, you end up with a serializer that's a lot more verbose since you can't pass the same through option to all serialized values:

serialize :cpu_temp, through: :cup_temp, from: :display_value
serialize :ram_usage, through: :ram_usage, from: :display_value
serialize :disk_usage, through: :disk_usage, from: :display_value

I don't think map is a strange name for this, but maybe it's because I'm from a math background. A map is the abbreviation of a mapping, and used as a synonym to a function. When used as a verb, it means "apply the function to these values", which is what the code is doing.

But if you don't like the name map, what about format? Every example I seem to come up with involves formatting a model into some string: formatting that address, formatting metrics, ...

Flixt commented 3 years ago

I'm with @adamcrown here. map is a rather misleading name, from a rubyist and from a functional programming perspective one would expect this thing to iterate over a collection and transform it.

The repetition

serialize :cpu_temp, through: :cup_temp, from: :display_value
serialize :ram_usage, through: :ram_usage, from: :display_value
serialize :disk_usage, through: :disk_usage, from: :display_value

probably this case can be put into its own serializer and then be merged. Example:

class DisplayValueSerializer < CacheCrispies::Base
  serialize :cpu_temp, :cpu_usage, :ram_usage
end

class MyModelSerializer < CacheCrispies::Base
  serialize :another_attribute
  merge :display_value, with: DisplayValueSerializer
end

display_value = OpenStruct.new(cpu_temp: '100', cpu_usage: 50, ram_usage: 25)
my_model = OpenStruct.new(another_attribute: 'test', display_value: display_value)

MyModelSerializer.new(my_model).as_json
# => {:another_attribute=>"test", :cpu_temp=>"100", :cpu_usage=>50, :ram_usage=>25}
sander-deryckere commented 3 years ago

I'm sorry if my example wasn't clear, but I meant it more like this, where I have a model with metrics (consisting of a value and a unit, and with extra operations)

class MetricSerializer < CacheCrispies::Base
  serialize :display_value
end

class SystemMetricsSerializer < CacheCrispies::Base
  merge :cpu_temp, :cpu_usage, :ram_usage, with: MetricSerializer
end

cpu_temp = OpenStruct.new(value: 75, unit: "°C", display_value: "75 °C")
cpu_usage = OpenStruct.new(value: 15, unit: "%", display_value: "15 %")
ram_usage = OpenStruct.new(value: 80, unit: "%", display_value: "80 %")
# This isn't modeled as an OpenStruct obviously, but rather in a model as offered by UnitWise

metrics = OpenStruct.new(cpu_temp: cpu_temp, cpu_usage: cpu_usage, ram_usage: ram_usage)

SystemMetricsSerializer.new(metrics).as_json
# => {cpu_temp: "75 °C", cpu_usage: "15 %", ram_usage: "80 %"}

Sadly, this isn't how merge works. Instead of the json, I just get an error message saying merge only accepts one argument.

So, unless I missed something, merge doesn't with the given model.

What I want is a function that applies to all arguments given to the serialize call. Passing a list of arguments to a function is almost like passing a collection (it's just a splatted collection), hence why I thought the map option would be a fitting name. But as said before, I'm not fixed on the name, I just would want some easy way to apply a common formatting function to a list of values.

Another option I'm thinking about is having a raw serializer. That serializer shouldn't wrap itself in a sub-hash, but rather just return a value to be used as serialisation.

# Inherit from a different kind of base class
class MetricSerializer < CacheCrispies::Raw
  # Serialize returns the complete object or value to be serialized without it having to be wrapped
  def serialize(model)
    model.display_value
  end
end

class SystemMetricsSerializer < CacheCrispies::Base
  serialize :cpu_temp, :cpu_usage, :ram_usage, with: MetricSerializer
end

cpu_temp = OpenStruct.new(value: 75, unit: "°C", display_value: "75 °C")
cpu_usage = OpenStruct.new(value: 15, unit: "%", display_value: "15 %")
ram_usage = OpenStruct.new(value: 80, unit: "%", display_value: "80 %")
# This isn't modeled as an OpenStruct obviously, but rather in a model as offered by UnitWise

metrics = OpenStruct.new(cpu_temp: cpu_temp, cpu_usage: cpu_usage, ram_usage: ram_usage)

SystemMetricsSerializer.new(metrics).as_json
# => {cpu_temp: "75 °C", cpu_usage: "15 %", ram_usage: "80 %"}

But I don't think something like that exists either. And it follows a more functional programming style, while cache-crispies is more declarative in nature.

sander-deryckere commented 2 years ago

Any opinions on the name format, or on implementing a raw serializer?