You don't have to sacrifice that simplicity, actually. (And I insist on that simplicity being a wrong type, it'll bite users of your library basically right away, when they try to use it for anything apart from storage/serialisation)
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.
I haven’t looked at the Shale source code, but I suspect that it would not be hard to add `mapped_class` support the way you’ve described it, so that the business objects are not themselves mapper instances. At a guess, the `from_xml` probably does something like (vastly over simplified):
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.
An added advantage of this approach is that it allows clean integration w/any other mapper or library. E.g. you could define a mapping to your Sequel or ActiveRecord models, and in one go you have the ability to roundtrip between JSON/XML etc. and the ORM.
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:
Agreed that this is a big advantage. I’ve switched to having a separate set of serialization objects with straightforward copy constructors or mapping functions and let the serialization library do the job against those. I used to hand roll the serialization, but this is admittedly user.
I'm glad you like it. One clarification - Nokogiri is not required by default, you have to explicitly require "shale/adapter/nokogiri" to use it. If you don't Shale will use REXML which comes from Ruby's standard library.
Rexml has been gemified. Shale's gemspec doesn't require a specific version of rexml and rexml<3.2.5 is vulnerable to CVE-2021-28965. I just checked Ubuntu 20.04 LTS and got Ruby 2.7 with rexml 3.2.3 by default so this seems like a realistic concern and it would be safer if shale required a minimum rexml version.
I have a mixed feelings about this, standard library's vulnerabilities are part of Ruby's vulnerabilities, so you would update your Ruby version anyway. But you're right specifing version explicitly would prevent this.
I think one of the motivations for splitting the stdlib into gems was for exactly for this kind of scenario: some users might not be able to update their Ruby immediately. The ruby-lang advisory explicitly recommends bumping the REXML version.
I have definitely been in situations where I couldn't update the ruby version in a timely manner, but have been able to bump a gem version (like in this example)
Hi, I released Shale, a Ruby gem that allows you to parse JSON, YAML and XML and convert it into Ruby data structures, as well as serialize your Ruby data model to JSON, YAML or XML.
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
Hey this is a very cool project! When you were developing it, I'm curious if you took any special security precautions in your design of this project, seeing how XML/JSON/YAML serialization and de-serialization are the topic of many high profile CVEs, particularly in the Ruby community?
Shale uses Ruby's standard library parsers out of the box, so if you keep your Ruby up to date with security updates you will be good. Also others in this thread suggested to set minimal version on dependencies, so I'll probably do that in the future version.
This seems like programmer error. Don't put restricted fields into types you're deserializing off the wire. It's like accepting user input and directly inserting it into a database without any validation.
If you don't define attributes explicitly on the model, Shale will ignore them.
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)
Documentation site was based on https://vuepress.vuejs.org/ but it evolved so much I dropped Vue all together and wen't with plain HTML instead. I must have left that meta tag from the early days.
Regarding Vue I use it daily at my job, great library :)