diff options
| author | Zog | 2018-03-05 13:13:38 +0100 | 
|---|---|---|
| committer | Zog | 2018-03-05 13:13:38 +0100 | 
| commit | eb01c9180419f05ab0ad4a734c082cc889792e75 (patch) | |
| tree | ab03f00dbd576fa4d88ab2d42209575ec70d5d78 | |
| parent | a412f915c885f3bf2962d0b786ff864f1b0e120e (diff) | |
| download | chouette-core-eb01c9180419f05ab0ad4a734c082cc889792e75.tar.bz2 | |
Refs #6068; First steps toward JSON exporter
| -rw-r--r-- | app/models/simple_exporter.rb | 63 | ||||
| -rw-r--r-- | app/models/simple_importer.rb | 13 | ||||
| -rw-r--r-- | app/models/simple_interface.rb | 10 | ||||
| -rw-r--r-- | app/models/simple_json_exporter.rb | 164 | ||||
| -rw-r--r-- | spec/models/simple_exporter_spec.rb | 6 | ||||
| -rw-r--r-- | spec/models/simple_json_exporter_spec.rb | 95 | 
6 files changed, 309 insertions, 42 deletions
| diff --git a/app/models/simple_exporter.rb b/app/models/simple_exporter.rb index fe0aa05b5..adc48533f 100644 --- a/app/models/simple_exporter.rb +++ b/app/models/simple_exporter.rb @@ -1,17 +1,8 @@  class SimpleExporter < SimpleInterface    def export opts={}      configuration.validate! -    @verbose = opts.delete :verbose - -    @resolution_queue = Hash.new{|h,k| h[k] = []} -    @errors = [] -    @messages = [] -    @number_of_lines = 0 -    @padding = 1 -    @current_line = -1 -    @number_of_lines = collection.size -    @padding = [1, Math.log(@number_of_lines, 10).ceil()].max +    init_env opts      @csv = nil      fail_with_error "Unable to write in file: #{self.filepath}" do @@ -52,6 +43,12 @@ class SimpleExporter < SimpleInterface    end    protected +  def init_env opts +    @number_of_lines = collection.size + +    super opts +  end +    def process_collection      self.configuration.before_actions(:all).each do |action| action.call self end      log "Starting export ...", color: :green @@ -75,6 +72,31 @@ class SimpleExporter < SimpleInterface      configuration.item_to_rows_mapping.call(item).map {|row| row.is_a?(ActiveRecord::Base) ? row : CustomRow.new(row) }    end +  def resolve_value item, col +    scoped_item = col.scope.inject(item){|tmp, scope| tmp.send(scope)} +    val = col[:value] +    if val.nil? || val.is_a?(Proc) +      if val.is_a?(Proc) +        val = instance_exec(scoped_item, &val) +      else +        attributes = [col.attribute].flatten +        val = attributes.inject(scoped_item){|tmp, attr| tmp.send(attr)} +      end +    end +    if val.nil? +      push_in_journal({event: :attribute_not_found, message: "Value missing for: #{[col.scope, col.attribute].flatten.join('.')}", kind: :warning}) +      self.status ||= :success_with_warnings +    end + +    if val.nil? && col.required? +      @new_status = colorize("x", :red) +      raise "MISSING VALUE FOR COLUMN #{col.name}" +    end + +    val = encode_string(val) if val.is_a?(String) +    val +  end +    def handle_item item      number_of_lines = @number_of_lines      map_item_to_rows(item).each_with_index do |item, i| @@ -84,27 +106,8 @@ class SimpleExporter < SimpleInterface        row = []        @new_status = nil        self.configuration.columns.each do |col| -        scoped_item = col.scope.inject(item){|tmp, scope| tmp.send(scope)} -        val = col[:value] -        if val.nil? || val.is_a?(Proc) -          if val.is_a?(Proc) -            val = instance_exec(scoped_item, &val) -          else -            attributes = [col.attribute].flatten -            val = attributes.inject(scoped_item){|tmp, attr| tmp.send(attr)} -          end -        end -        if val.nil? -          push_in_journal({event: :attribute_not_found, message: "Value missing for: #{[col.scope, col.attribute].flatten.join('.')}", kind: :warning}) -          self.status ||= :success_with_warnings -        end - -        if val.nil? && col.required? -          @new_status = colorize("x", :red) -          raise "MISSING VALUE FOR COLUMN #{col.name}" -        end +        val = resolve_value item, col          @new_status ||= colorize("✓", :green) -        val = encode_string(val) if val.is_a?(String)          row << val        end        push_in_journal({event: :success, kind: :log}) diff --git a/app/models/simple_importer.rb b/app/models/simple_importer.rb index 6db71797a..e23b3e524 100644 --- a/app/models/simple_importer.rb +++ b/app/models/simple_importer.rb @@ -1,5 +1,4 @@  class SimpleImporter < SimpleInterface -    def resolve col_name, value, &block      val = block.call(value)      return val if val.present? @@ -9,19 +8,15 @@ class SimpleImporter < SimpleInterface    def import opts={}      configuration.validate! -    @verbose = opts.delete :verbose -    @resolution_queue = Hash.new{|h,k| h[k] = []} -    @errors = [] -    @messages = [] -    @number_of_lines = 0 -    @padding = 1 -    @current_line = 0      fail_with_error "File not found: #{self.filepath}" do        @number_of_lines = CSV.read(self.filepath, self.configuration.csv_options).length -      @padding = [1, Math.log(@number_of_lines, 10).ceil()].max      end +    init_env opts + +    @resolution_queue = Hash.new{|h,k| h[k] = []} +      self.configuration.before_actions(:parsing).each do |action| action.call self end      @statuses = "" diff --git a/app/models/simple_interface.rb b/app/models/simple_interface.rb index 5f022719a..07fabd832 100644 --- a/app/models/simple_interface.rb +++ b/app/models/simple_interface.rb @@ -27,6 +27,16 @@ class SimpleInterface < ActiveRecord::Base      self.journal ||= []    end +  def init_env opts +    @verbose = opts.delete :verbose + +    @errors = [] +    @messages = [] +    @padding = 1 +    @current_line = 0 +    @padding = [1, Math.log([@number_of_lines, 1].max, 10).ceil()].max +  end +    def configure      new_config = configuration.duplicate      yield new_config diff --git a/app/models/simple_json_exporter.rb b/app/models/simple_json_exporter.rb new file mode 100644 index 000000000..706307de1 --- /dev/null +++ b/app/models/simple_json_exporter.rb @@ -0,0 +1,164 @@ +class SimpleJsonExporter < SimpleExporter + +  def export opts={} +    configuration.validate! + +    init_env opts + +    if self.configuration.root +      @out = {self.configuration.root => []} +    else +      @out = [] +    end + +    fail_with_error "Unable to write in file: #{self.filepath}" do +      dir = Pathname.new(self.filepath).dirname +      FileUtils.mkdir_p dir +      @file = File.open(self.filepath, 'w', self.configuration.file_options) +    end + +    self.configuration.before_actions(:parsing).each do |action| action.call self end + +    @statuses = "" + +    if ENV["NO_TRANSACTION"] +      process_collection +    else +      ActiveRecord::Base.transaction do +        process_collection +      end +    end +    self.status ||= :success +  rescue SimpleInterface::FailedOperation +    self.status = :failed +  ensure +    if @file +      @file.write @out.to_json +      @file.close +    end +    self.save! +  end + +  protected +  def root +    if self.configuration.root +      @out[self.configuration.root] +    else +      @out +    end +  end + +  def process_collection +    self.configuration.before_actions(:all).each do |action| action.call self end +    log "Starting export ...", color: :green +    log "Export will be written in #{filepath}", color: :green + +    if collection.is_a?(ActiveRecord::Relation) && collection.model.column_names.include?("id") +      ids = collection.pluck :id +      ids.in_groups_of(configuration.batch_size).each do |batch_ids| +        collection.where(id: batch_ids).each do |item| +          handle_item item +        end +      end +    else +      collection.each{|item| handle_item item } +    end +    print_state +  end + +  def resolve_node item, node +    vals = [] +    [item.send(node.attribute)].flatten.each do |node_item| +      item_val = {} +      apply_configuration node_item, node.configuration, item_val +      vals.push item_val +    end +    node.multiple ? vals : vals.first +  end + +  def apply_configuration item, configuration, output +    configuration.columns.each do |col| +      val = resolve_value item, col +      output[col.name] = val +    end + +    configuration.nodes.each do |node| +      val = resolve_node item, node +      output[node.name] = val +    end +  end + +  def handle_item item +    serialized_item = {} +    @current_row = item.attributes +    @current_row = @current_row.slice(*configuration.logged_attributes) if configuration.logged_attributes.present? +    @new_status = nil + +    apply_configuration item, self.configuration, serialized_item + +    @new_status ||= colorize("✓", :green) + +    push_in_journal({event: :success, kind: :log}) +    @statuses += @new_status +    print_state if @current_line % 20 == 0 +    @current_line += 1 +    append_item serialized_item +  end + +  def append_item serialized_item +    root.push serialized_item +  end + +  class Configuration < SimpleExporter::Configuration +    attr_reader :nodes +    attr_accessor :root + +    alias_method :add_field, :add_column + +    def initialize import_name, opts={} +      @nodes = [] +      super import_name, opts +    end + +    def add_node name, opts={} +      @nodes ||= [] +      node = Node.new({name: name.to_s}.update(opts)) +      yield node.configuration +      @nodes.push node +    end + +    def add_nodes name, opts={}, &block +      self.add_node name, opts.update({multiple: true}), &block +    end + +    def file_options +      { +        encoding: self.encoding +      } +    end +  end + +  class NodeConfiguration < Configuration +    def initialize node +      super +    end +  end + +  class Node +    attr_accessor :name, :configuration + +    def initialize opts={} +      @name = opts[:name] +      @options = opts +      @configuration = NodeConfiguration.new self +    end + +    def attribute +      name +    end + +    def multiple +      !!@options[:multiple] +    end +  end +end diff --git a/spec/models/simple_exporter_spec.rb b/spec/models/simple_exporter_spec.rb index 18cdc55e8..75051aeb9 100644 --- a/spec/models/simple_exporter_spec.rb +++ b/spec/models/simple_exporter_spec.rb @@ -19,7 +19,7 @@ RSpec.describe SimpleExporter do          expect{SimpleExporter.find_configuration(:foo)}.to_not raise_error          expect{SimpleExporter.new(configuration_name: :foo, filepath: "").export}.to_not raise_error          expect{SimpleExporter.find_configuration(:bar)}.to raise_error -        expect{SimpleExporter.new(configuration_name: :bar, filepath: "")}.to_not raise_error +        expect{SimpleExporter.new(configuration_name: :bar, filepath: "")}.to raise_error          expect{SimpleExporter.new(configuration_name: :bar, filepath: "").export}.to raise_error          expect{SimpleExporter.create(configuration_name: :foo, filepath: "")}.to change{SimpleExporter.count}.by 1        end @@ -64,7 +64,7 @@ RSpec.describe SimpleExporter do        end        it "should export the given file" do -        expect{exporter.export verbose: true}.to_not raise_error +        expect{exporter.export verbose: false}.to_not raise_error          expect(exporter.status).to eq "success"          expect(File.exists?(filepath)).to be_truthy          csv = CSV.read(filepath, headers: true, col_sep: ";") @@ -102,7 +102,7 @@ RSpec.describe SimpleExporter do        end        it "should export the given file" do -        expect{exporter.export verbose: true}.to_not raise_error +        expect{exporter.export verbose: false}.to_not raise_error          expect(exporter.status).to eq "success"          expect(File.exists?(filepath)).to be_truthy          csv = CSV.read(filepath, headers: true) diff --git a/spec/models/simple_json_exporter_spec.rb b/spec/models/simple_json_exporter_spec.rb new file mode 100644 index 000000000..4b48dc732 --- /dev/null +++ b/spec/models/simple_json_exporter_spec.rb @@ -0,0 +1,95 @@ +RSpec.describe SimpleJsonExporter do +  describe "#define" do +    context "with an incomplete configuration" do +      it "should raise an error" do +        SimpleJsonExporter.define :foo +        expect do +          SimpleJsonExporter.new(configuration_name: :test).export +        end.to raise_error +      end +    end +    context "with a complete configuration" do +      before do +        SimpleJsonExporter.define :foo do |config| +          config.collection = Chouette::StopArea.all +        end +      end + +      it "should define an exporter" do +        expect{SimpleJsonExporter.find_configuration(:foo)}.to_not raise_error +        expect{SimpleJsonExporter.new(configuration_name: :foo, filepath: "").export}.to_not raise_error +        expect{SimpleJsonExporter.find_configuration(:bar)}.to raise_error +        expect{SimpleJsonExporter.new(configuration_name: :bar, filepath: "")}.to raise_error +        expect{SimpleJsonExporter.new(configuration_name: :bar, filepath: "").export}.to raise_error +        expect{SimpleJsonExporter.create(configuration_name: :foo, filepath: "")}.to change{SimpleJsonExporter.count}.by 1 +      end +    end + +    context "when defining the same col twice" do +      it "should raise an error" do +        expect do +          SimpleJsonExporter.define :foo do |config| +            config.collection = Chouette::StopArea.all +            config.add_field :name +            config.add_field :name +          end +        end.to raise_error +      end +    end +  end + +  describe "#export" do +    let(:exporter){ importer = SimpleJsonExporter.new(configuration_name: :test, filepath: filepath) } +    let(:filepath){ Rails.root + "tmp/" + filename } +    let(:filename){ "stop_area.json" } +    # let(:stop_area_referential){ create(:stop_area_referential, objectid_format: :stif_netex) } + +    context "with one row per item" do +      before(:each) do +        @stop_area = create :stop_area +        @stop_1 = create :stop_point, stop_area: @stop_area +        @stop_2 = create :stop_point, stop_area: @stop_area + +        SimpleJsonExporter.define :test do |config| +          config.collection = ->{ Chouette::StopArea.all } +          config.root = "stops" +          config.add_field :name +          config.add_field :lat, attribute: :latitude +          config.add_field :lng, attribute: [:latitude, :to_i, :next] +          config.add_field :type, attribute: :area_type +          config.add_field :street_name, value: "Lil Exporter" +          config.add_node :stop_area_referential do |config| +            config.add_field :id, attribute: :id +            config.add_field :id_other, value: ->(item){ item.id } +          end + +          config.add_nodes :stop_points do |config| +            config.add_field :id +            config.add_node :stop_area do |config| +              config.add_field :id +            end +          end +          config.add_field :forty_two, value: 42 +        end +      end + +      it "should export the given file" do +        expect{exporter.export verbose: false}.to_not raise_error +        expect(exporter.status).to eq "success" +        expect(File.exists?(filepath)).to be_truthy +        json = JSON.parse File.read(filepath) +        row = json["stops"].first +        expect(row["name"]).to eq @stop_area.name +        expect(row["lat"]).to eq @stop_area.latitude.to_s +        expect(row["lng"]).to eq (@stop_area.latitude.to_i + 1) +        expect(row["street_name"]).to eq "Lil Exporter" +        expect(row["stop_area_referential"]["id"]).to eq @stop_area.stop_area_referential_id +        expect(row["stop_area_referential"]["id_other"]).to eq @stop_area.stop_area_referential_id +        expect(row["stop_points"][0]["id"]).to eq @stop_1.id +        expect(row["stop_points"][0]["stop_area"]["id"]).to eq @stop_area.id +        expect(row["stop_points"][1]["id"]).to eq @stop_2.id +        expect(row["forty_two"]).to eq 42 +      end +    end +  end +end | 
