Show HN: Shale – a Ruby object mapper and serializer for JSON, YAML and XML
shalerb.org
shalerb.org
Features:
- convert JSON, XML or YAML into Ruby data model
- serialize data model to JSON, XML or YAML
- generate JSON and XML Schema from Ruby models
- compile JSON Schema into Ruby models (compiling XML Schema is a work in progress)
A quick example so you can get a feel of it:
require 'shale'
class Address < Shale::Mapper
attribute :street, Shale::Type::String
attribute :city, Shale::Type::String
end
class Person < Shale::Mapper
attribute :first_name, Shale::Type::String
attribute :last_name, Shale::Type::String
attribute :address, Address
end
# parse data and convert it into Ruby data model
person = Person.from_json(<<~JSON) # or .from_xml / .from_yaml
{
"first_name": "John",
"last_name": "Doe",
"address": {
"street": "Oxford Street",
"city": "London"
}
}
JSON
# It will give you
# =>
# #<Person:0xa0a4
# @address=#<Address:0xa0a6
# @city="London",
# @street="Oxford Street",
# @zip="E1 6AN">,
# @age=50,
# @first_name="John",
# @hobbies=["Singing", "Dancing"],
# @last_name="Doe",
# @married=false>
# serialize Ruby data model to JSON
Person.new(
first_name: 'John',
last_name: 'Doe',
address: Address.new(street: 'Oxford Street', city: 'London')
).to_json # or .to_xml / .to_yaml
Source code is available on GitHub: https://github.com/kgiszczak/shale class Person < Shale::Mapper
attribute :first_name, Shale::Type::String
attribute :last_name, Shale::Type::String
attribute :age, Shale::Type::Integer
attribute :married, Shale::Type::Boolean, default: false
attribute :hobbies, Shale::Type::String, collection: true
attribute :address, Address
end
And the JSON used for parsing also should contain those atttributes, like: {
"first_name": "John",
"last_name": "Doe",
"age": 30,
"married": false,
"hobbies": ["Singing", "Dancing"],
"address": {
"street": "Oxford Street",
"city": "London"
}
}<meta name="description" content="Vue-powered Static Site Generator">
Kudos for choosing Vue tho =)
Regarding Vue I use it daily at my job, great library :)
CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes <https://cwe.mitre.org/data/definitions/915.html> (Ruby on Rails Mass assignment bug)
Regarding attributes that you defined but still don't want to be assigned, you should probably filter them before passing them to Shale, or alternatively filter them with Shale before passing them further down the stack (e.g to ActiveRecord)
I think the API Shale provides is pretty sane. I would probably use it in my next Ruby/Rails project. I don't like the fact that Nokogiri is included by default, it would be nice to declare a core type, and then bring in what you need (JSON, XML, YAML) as a different gem. But that's not a deal breaker for me.
I have created my own serializers in the past (SimpleAMS[1]) because I really detested AMS, no offence to AMS contributors, but AMS library should just die. Rails, and way more importantly Ruby, should come up with an "official" serializers/deserializers library that is flexible enough, rock solid and fast. For instance I had done some benchmarking among common serializer libraries [2] and AMS was crazy slow, without providing much flexibility, really (meaning, slowness is not justified). Others were faster, but were supporting only one JSON spec format (like jsonapi-rb). I am wondering where shale stands.
Another thing is that most serialization libraries seem to have ActiveSupport as a main dependency (not shale though) which I think is a bit too much, and actually has a performance hit on the methods it provides.
I really think that Ruby community can do better here ?
[1] https://github.com/vasilakisfil/SimpleAMS
[2] https://vasilakisfil.social/blog/2020/01/20/modern-ruby-seri... (scroll towards the end for benchmarks)
See http://www.ruby-lang.org/en/news/2021/04/05/xml-round-trip-v...
Personally though, I've been seeing almost 10x the amount of alerts for useless "vulnerabilities" like ReDOS in nodejs projects though. Either way, alert fatigue is real.
Safely parsing untrusted XML is an extremely hairy task.
Go can be extremely productive but it's definitely not a great choice if you need to create a web app over a weekend.
RoR, Django etc have ready solutions for things like authorization\authentication, administration tools, oauth... Not to mention that 'framework' assumes some sort of contracts so that all thing build for the framework in question can talk to each other.
Go is a good choice if you need to build a custom solution for your needs. Not if you are looking for a set of building blocks you have to configure for your task.
I really liked Go for what it was, but it wasn't the right fit for my set of problems.
It might have been different if I needed something more low level - though in that case one can use C/C++.
It's a shame Ruby and Rails are not getting all the recognition they deserve.
Have really enjoyed using it recent in my Rails apps.
But you can just give an upgrade path! consider something like this:
class Address
attr_accessor :street, :city
end
class Person
attr_accessor :address
end
class AddressMapper < Shale::Mapper
mapped_class Address
attribute :street, Shale::Type::String
attribute :city, Shale::Type::String
end
class PersonMapper < Shale::Mapper
mapped_class Person
attribute :address, AddressMapper
end
# use like this
PersonMapper.from_xml("...."); PersonMapper.to_xml(person)
and then, for _dead_ simplicity, you can add another method
generate_mapped_class "Person"which will define that PORO class for user for extra DRYness. API is basically the same, no repetition, but amount of rewrite with new requirements is drastically less.
I'm not asking you to rewrite your library, and I probably won't write and release mine, just saying that considering future self isn't that hard. And yeah, it's a bit of a rant about ActiveRecord from user of Rails, since 2006.
def from_xml(xml_string)
new.tap { |o|
parse_xml(xml_string) do |key, value|
o.__send__(:"#{key}=", value)
end
}
end
It would then be possible to change this to: def from_xml(xml_string)
(mapped_class || self).new.tap { |o|
parse_xml(xml_string) do |key, value|
o.__send__(:"#{key}=", value)
end
}
end
This would make it easier to solve a larger problem of needing to serialize the same business object in different ways for different consumers with different levels of detail. It would also permit the construction of mappers for temporary objects that contain the details for more complex serializations that have indirect connections.To the point of rewriting: A halfway point is to drop inheritance in favour of include/extend'ing the models. If that's done cautiously, it allows for co-existing with model objects from libraries that require inheritance. That is, this:
class Person
include Shale::Mapper
end
is preferable to class Person < Shale::Mapper
endI'll probably give it a go to replace my current implementation using nokogiri-happymapper (https://github.com/mvz/happymapper)
*Edit: nice docs site as well - what are you using for it?
Interactive examples are powered by https://opalrb.com/