Greyscale Record
GreyscaleRecord
is a flat-file ORM, designed for users whose data is perfectly static and is stored in a flat format (e.g. yaml files). It is a clone of YamlBSides, but is extended to support multiple backend drivers (YAML files, JSON APIs)
Installation
Add this line to your application's Gemfile:
gem 'greyscale_record'
And then execute:
$ bundle
Or install it yourself as:
$ gem install greyscale_record
Usage
Greyscale Record
acts very mich like active record. You set up your base class like you would an ActiveRecord class:
class Person < GreyscaleRecord::Base
and your #{FIXTURES_PATH}/people.yml
(see Setup for fixtures path setup):
greg:
name: Greg Orlov
url_slug: greg
bio: |
I do stuff
john:
name: John Doe
# ... etc
and you're in business. Your Person
objects will now respond to the present fields as methods. (see Properties for setting defaults)
Note: Greyscale Record
expects your class names to match the fixture names (e.g. Person
will want a people.yml
file)
Your Person
class now responds to
Indexing
-
index( field )
: will add an index on that field, for faster searching
Property definitions
These are completely optional, but if you have a yaml file that's not uniform, and want to have some defaults, you can use
-
property( name, defaul= nil )
: will set a single field. will set defaul value to nil if omitted -
properties( props = {})
: takes a hash; will set many defaults at once
Associations
You can define simple associations that behave very much like ActiveRecord associations. Once you define your association, you will have a method with that name that will do the lookups and cache the results for you.
-
belongs_to association
: the base object has to have the association id- will return a single object or nil
-
has_one
: the assiociation object has the id of the base.- will return a single object or nil
-
has_many
: the association object has the id of the base.- will return an array
Assocaition Options
Associations have some of the standard ActiveRecord options. Namely:
-
class
: specifies the class to find the record in.has_one :special_thing, class: Thing
-
class_name
: specifies the class w.o having to have the class defined. Handy for circular dependenciesclass Person < GreyscaleRecord::Base has_one :nickname, class_name: "Pesudonym" end class Pseudonym < GreyscaleRecord::Base belongs_to :bearer, class_name: "Person" end
-
through
: many to may association helper.class Person < GreyscaleRecord::Base has_many :person_foods has_many :favorite_foods, through: :person_foods, class_name: "Food" end class PersonFood < GreyscaleRecord::Base belongs_to :person belongs_to :food end class Food < GreyscaleRecord::Base end
NOTE: only works for
has_one
andhas_many
-
as
: specifies what the associated object calls the callerclass Person has_many :images, as: :target end class Image belongs_to :target, class: Person end
NOTE: only works for
has_one
andhas_many
-
polymorphic
: specifies a polymorphicbelongs_to
association. Better explanation here, on the ActiveRecord pageclass Person has_many :images, as: :target end class Party has_many :images, as: :target end class Image belongs_to :target, polymorphic: true end
And then the
images.yml
looks something likeimage-1: thing_id: a thing_class: Person # ... image-2: thing_id: a thing_class: PArty #...
Query Methods
-
all
: will give you all of the records in the table -
first
: wil return the first record in the table -
find( id )
: will find a single record with the specified yaml key -
find_by( properties = {} )
: will find all the recored that match all the proerties in the hash
Relation methods
These act as you would expect them to in ActiveRecord. These can be chained and applied to associations
-
where( params )
: will return a relation which can be interacted with as if it were the resulting array -
and( params )
: identical towhere
, but nicer to read
Person.where( url_slug: "greg" ) #=> [<Person>]
Person.where( url_slug: "greg", name: "Greg Orlov" ).and( id: "greg") #=> [<Person>]
Person.where( url_slug: "greg", name: "Greg Orlov" ).where( id: "greg") #=> [<Person>]
# from the code above
Person.first.images.where( some_property: "value" )
Data Sourcing
As mentioned in the summary above, GreyscaleRecord
can be connected to different data sources. The structure that we use is a Driver
.
There is a built in driver to use as an example: Yaml Driver. This is the structure that controls data flow from the original data store,
such as YAML files or an API, and formats it to be consumed by the internal GreyscaleRecord::DataStore::Store
object, which expects the
result set to look roughly like
{ table_name: {
record_id: { attribute: value, ... },
...
},
...
}
GreyscaleRecord
will populate the id field for you from the record keys.
Data Patching
If you have a data set that you want to temporarily augment, you can apply a JSON patch to the data store. This will apply the patch within the context of your current thread until it is removed
patch = ::Hana::Patch.new [ { 'op' => 'add', 'path' => '/people/mike', 'value' => { id: "mike", name: "Mike Uchman" } } ]
# this will stick around indefinitely in this thread
data_store.apply_patch patch
Person.find( 'mike' ).name # => "Mike Uchman"
Person.where( id: 'mike' ).first.name #=> "Mike Uchman"
# let's go back to the original set
data_store.remove_patch
Person.where( id: "mike" ) #=> []
Or, if you are doing something simple and can fir your code in a single block, you can do:
data_store.with_patch patch do
Person.find( 'mike' ).name # => "Mike Uchman"
Person.where( id: 'mike' ).first.name #=> "Mike Uchman"
end
Person.where( id: "mike" ) #=> []
NOTE: The patch interface that Store
expects is that of Hana. You don't have to use it, but it has
to respond to patch.apply( doc )
.
Example
To use the People
class from earlier, a fully fleshed out model would look something like:
class Person < GreyscaleRecord::Base
property :name, ""
properties url_slug: "",
bio: ""
has_many :nicknames
index :name
index :url_slug
end
class Nickname < GreyscaleRecord::Base
belongs_to :person
end
and the YAML files will look like
# in people.yml
mark_twain:
name: Mark Twain
url_slug: mark-twain
#...
# in nicknames.yml
sam_clemmens:
name: Samuel Clemmens
person_id: mark_twain
Setup
The setup is pretty straightforward. Greyscale Record wants a logger and a base dir to look for files in. An example config for a Rails app would look like:
GreyscaleRecord::logger = Rails.logger
# for now this is the only driver
yaml_driver = GreyscaleRecord::Drivers::Yaml.new File.expand_path("./db/fixtures", File.dirname(__FILE__))
GreyscaleRecord::Base.data_store = GreyscaleRecord::DataStore::Engine.new(yaml_driver)
# in development.rb
# eanble reload of the data files to avoid having to restart the server for every change
GreyscaleRecord.live_reload = true
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake test
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/gaorlov/greyscale_record.
License
The gem is available as open source under the terms of the MIT License.