Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hashie::Extensions::Persistable #262

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

maxlinc
Copy link
Contributor

@maxlinc maxlinc commented Jan 9, 2015

I didn't see #185 until just now, so this might be a duplicate of that, although this takes a fairly different approach (extension vs class). But that's why I'm opening this PR now to get some early feedback, rather than implementing the other features on my mind (autoload/autosave).

Hashie already has Hashie::Mash#load, but it doesn't have Hashie::Mash#save, and there's also no way to load or save a Dash or other Hashie classes. This adds a Persistable mixin so any hash-like class can be loaded (if it has a compatible initializer) or saved.

Usage:

class MyHash < ::Hash
  include Hashie::Extensions::MergeInitializer
  include Hashie::Extensions::Persistable
end

hash1 = MyHash.load('my_data.yaml')
hash1['foo'] #=> 'bar'
hash1['magic_number'] = 42
hash1.save # still remembers 'my_data.yaml'

hash2 = MyHash.new({
  'foo' => 'bar',
  'favorite_color' => 'red'
})
hash2.save # raises exception - don't know where to save to...
                   # or it could save to a tempfile and return the path...
hash2.save('other_data.yaml') # now it works

I had planned on implementing autosave, but I want some feedback before going any further.

Also, right now it's only persisting YAML, but I think it wouldn't be hard to modify to support JSON and "YamlErb" (which is what's currently used by Hashie::Mash). You just need to swap out the adapter class and/or options... in theory you could even use Moneta, though I think the fact that Moneta doesn't support #keys, #each or #to_hash would be problematic.

Any thoughts before I go further down this path?

module Hashie
module Extensions
# Marker module to indicate has a single-argument hash constructor.
module HashInitializer
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I created Hashie::Extensions::HashInitializer that's just a marker module, because I didn't have a way to tell what classes could be initialized from a Hash. That's the behavior for Mash and Dash, but that's not how the initializer works (by default) for ::Hash or Hashie::Hash. It is the behavior if you mixin Hashie::Extensions::MergeInitializer, though. I'm open to other ideas, though, perhaps a .from_hash method instead of a marker module?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is OK.

@dblock
Copy link
Member

dblock commented Jan 9, 2015

I like the Mixin approach very much and this is definitely on the right track.

I'd like the implementation to be future-proof (unlike Mash which is more garbage in, garbage out :), you should be able to mix Persistable::SerializedBinary, Persistable::YAML, Persistable::JSON, etc. Classes like Mash should use one of those as their default load and save.

Let me know what you think, happy to review more code!

@maxlinc
Copy link
Contributor Author

maxlinc commented Jan 12, 2015

Rather than include Peristable::JSON I'd thought about doing include Persistable[:json] or include Persistable, :json.

This might look a little odd at first, and slightly trickier to implement (but should be possible - modularity uses this pattern) but I think it will make it easier to extend. The advantages of specifying the format as an argument to a module rather than as a module constant is:

  • You could create a simple mechanism for plugins to register the formats they support.
  • This would make it easy to add support for additional formats, for example with a hashie-moneta gem that registers the moneta serializers with hashie so you'll get support for things like :bson, :marshall, :msgpack or :ox. (See note below)
  • This would work just as easily with media types, so you might use 'application/json' rather than :json.
  • If you call it with a format that isn't registered, hashie can raise an error like "Cannot persist: there is no serializer registered for application/json"

I think you get the idea. The caller requests persistance to a format, and plugins provide formats. But the caller doesn't need to know if :json is serialized by oj or yajl or multi_json or moneta.

Note: I'm not sure any of moneta's current serializers would be very hashie friendly, because a "key/value store" API is subtly different from a hash API (moneta allows random access but not iteration over keys) but the basic idea applies. Any gem with a collection of classes to serialize/deserialize a hash to a format could easily be registered as a plugin.

This approach might be a slightly less exotic API with the same benefits:

# Default persistance settings in class

class MyConfigFile < Dash
  include Persistable
  persist_to :yaml, 'config.yaml'
  property :foo
  property :bar
end

config = MyConfigFile.load
# modify config...
config.save

# Alternately, on an object:

hash = { foo: 'bar' }
hash.extend Persistable
# Explicitly set the peristance target
hash.persist_to :json, 'data.json'
hash.save
# Or implicit set it as an argument
# hash.save :json, 'data.json'

@dblock
Copy link
Member

dblock commented Jan 13, 2015

I am OK with using symbols, however try to keep things short and simple to begin with. Up to you. Looking forward to it!

@gregory
Copy link
Contributor

gregory commented Feb 2, 2015

IMHO, API should look like this:

  h = {foo: 'bar'}
  Hashie::Persistable.persist(h, adapter: :json, target: 'foo.json')

  class Foo < Dash
    include Hashie::Persistable.new(persist_method: :commit)
  end

  Foo.new.commit(adapter: :json, target: 'foo.json')

@michaelherold
Copy link
Member

Bump @maxlinc - Just wanted to check if you've worked on the changes you mentioned back in January. I like the idea of a persistable framework and between the work you've outlined and something along the lines of Gregory's suggested syntax, I think it could be a great feature.

michaelherold added a commit to michaelherold/hashie that referenced this pull request Feb 1, 2016
Building on @maxlinc's start, this implements a Persistable extension
that you can mix into any Hash to gain access to file persistence.
Currently, there are two adapters: JSON and YAML.

The persistence framework is easily expandable via adapters that can be
registered and hotloaded into a Persistable module. The module defaults
to a JSON adapter with a `#persist` method when mixed into a derived
Hash class, but can be customized as follows:

```ruby
class YamlPersistedHash < Hash
  include Hashie::Extensions::Persistence.new(
    adapter: :yaml,
    persist_method: :save
  )
end

test = YamlPersistedHash['test' => 'value']  #=> {'test' => 'value'}
test.save('filename.yaml')
```

To create a new adapter, you only need to register a class, module, or
object that responds to an `#adapter` method. The `#adapter` method
needs to respond with an object with a `#write(target, data)` method
that knows how to write the data for persistence. For two examples, see
the `Hashie::Extensions::Persistable::Json` and
`Hashie::Extensions::Persistable::Yaml` modules.

Closes hashie#262 by finishing and expanding the implementation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants