# The `Datapimp::Sources` module houses the various # types of remote data stores we are reading and converting into # a JSON array of objects that gets cached on our filesystem. # module Datapimp module Sources class Base attr_reader :options, :name attr_accessor :raw, :processed, :format, :scopes, :slug_column, :refreshed_at, :path class << self attr_accessor :required_options end def self.requires *args self.required_options = args end def initialize(name, options={}) @name ||= name @options ||= options @format ||= options.fetch(:format, :json) @path ||= options.fetch(:path) { Pathname(Dir.pwd()) } @slug_column = options.fetch(:slug_column, :_id) ensure_valid_options! end def to_s data.to_json end # defines a scope for the records in this data source # a scope is a named filter, implemented in the form of a block # which is passed each record. if the block returns true, it returns # the record: # # Example: # # data_source(:galleries) do # scope :active, -> {|record| record.state == "active" } # end def scope(*args, block) name = args.first (self.scopes ||= {})[name.to_sym] = block end def has_scope?(scope_name) scope_name && (self.scopes ||= {}).key?(scope_name.to_sym) end # compute properties takes the raw data of each record # and sets additional properties on the records which may # not be persited in the data source def compute_properties self.processed && self.processed.map! do |row| if slug_column && row.respond_to?(slug_column) row.slug = row.send(slug_column).to_s.parameterize end row end processors.each do |processor| original = self.processed.dup modified = [] original.each_with_index do |record, index| previous = original[index - 1] modified.push(processor.call(record, index, previous: previous, set: original)) end self.processed = modified end end def processors &block @processors ||= [] @processors << block if block_given? @processors end # makes sure that the required options for this data source # are passed for any instance of the data source def ensure_valid_options! missing_options = (Array(self.class.required_options) - options.keys.map(&:to_sym)) missing_options.reject! do |key| respond_to?(key) && !send(key).nil? end if missing_options.length > 0 raise 'Error: failure to supply the following options: ' + missing_options.map(&:to_s).join(",") end end def select(&block) data.select(&block) end def refresh fetch process self.refreshed_at = Time.now.to_i self end def refresh_if_stale? refresh! if stale? end # A data source is stale if it has been populated # and the age is greater than the max age we allow. def stale? !need_to_refresh? && (age > max_age) end def fresh_on_server? need_to_refresh? end def max_age max = ENV['MAX_DATA_SOURCE_AGE'] (max && max.to_i) || 120 end # how long since this data source has been refreshed? def age Time.now.to_i - refreshed_at.to_i end def data refresh if need_to_refresh? processed end def refresh! refresh save_to_disk end def need_to_refresh? !(@fetched && @_processed) end def fetch @fetched = true self.raw = [] end def preprocess self.raw.dup end def process @_processed = true self.processed = preprocess # set_id compute_properties self.processed end def refreshed_at return @refreshed_at if @refreshed_at.to_i > 0 if path_to_file.exist? @refreshed_at = File.mtime(path.join(file)).to_i end end def save_to_disk unless path_to_file.dirname.exist? FileUtils.mkdir(path_to_file.dirname) end path_to_file.open('w+') {|fh| fh.write(to_s) } end def persisted? path_to_file && path_to_file.exist? end def file @file ||= name.parameterize if name.respond_to?(:parameterize) @file.gsub!("-","_") @file = "#{@file}.json" unless @file.match(/\.json/i) @file end def path_to_file Pathname(path).join("#{ file }") end def jsonify(value) case value when String, Numeric, NilClass, TrueClass, FalseClass value when Hash Hash[value.map { |k, v| [jsonify(k), jsonify(v)] }] when Array value.map { |v| jsonify(v) } when HappyMapper value.instance_variables.each_with_object({}) do |var_name, memo| key = var_name.to_s.sub(/^@/, '').to_sym val = value.instance_variable_get(var_name) memo[key] = jsonify(val) end else if value.respond_to?(:to_attrs) value.to_attrs elsif value.respond_to?(:as_json) value.as_json else value.to_s end end end end end end Dir[Datapimp.lib.join("datapimp/sources/**/*.rb")].each {|f| require(f) }