<commit_before>require "terrestrial/serializer"
require "terrestrial/struct_factory"

RSpec.shared_context "object graph setup" do

  before do
    setup_circular_references_avoiding_stack_overflow
  end

  def setup_circular_references_avoiding_stack_overflow
    biscuits_post_comment.commenter = hansel
    cat_biscuits_category.posts = [ biscuits_post ]
  end

  class PlainObject
    def self.with_members(*list, &block)
      Class.new(self).tap { |klass|
        klass.instance_variable_set(:@members, list)
        klass.class_exec(&block) if block
        klass.send(:attr_accessor, *list)
      }
    end

    def self.members
      @members
    end

    def initialize(attrs)
      members.sort == attrs.keys.sort or (
        raise(ArgumentError.new("Expected `#{self.class.members}` got `#{attrs.keys}"))
      )

      members.each { |member| send("#{member}=", attrs.fetch(member)) }
    end

    def members
      self.class.members
    end

    def to_h
      Hash[members.map { |field| [field, send(field)] }]
    end
  end

  User ||= PlainObject.with_members(:id, :first_name, :last_name, :email, :posts)
  Post ||= PlainObject.with_members(:id, :subject, :body, :comments, :categories, :created_at)
  Comment ||= PlainObject.with_members(:id, :commenter, :body)
  Category ||= PlainObject.with_members(:id, :name, :posts)

  let(:factories) {
    {
      users: User.method(:new),
      posts: Post.method(:new),
      comments: Comment.method(:new),
      categories: Category.method(:new),
      categories_to_posts: ->(x){x},
      noop: ->(x){x},
    }
  }

  let(:default_serializer) {
    ->(fields) {
      ->(object) {
        Terrestrial::Serializer.new(fields, object).to_h
      }
    }
  }

  let(:null_serializer) {
    ->(_fields) {
      ->(x){x}
    }
  }

  let(:hansel) {
    factories.fetch(:users).call(
      id: "users/1",
      first_name: "Hansel",
      last_name: "Trickett",
      email: "<EMAIL>",
      posts: [
        biscuits_post,
        sleep_post,
      ],
    )
  }

  let(:biscuits_post) {
    factories.fetch(:posts).call(
      id: "posts/1",
      subject: "Biscuits",
      body: "I like them",
      comments: [
        biscuits_post_comment,
      ],
      categories: [
        cat_biscuits_category,
      ],
      created_at: Time.parse("2015-09-05T15:00:00+01:00"),
    )
  }

  let(:sleep_post) {
    factories.fetch(:posts).call(
      id: "posts/2",
      subject: "Sleeping",
      body: "I do it three times purrr day",
      comments: [],
      categories: [
        chilling_category,
      ],
      created_at: Time.parse("2015-09-02T15:00:00+01:00"),
    )
  }

  let(:biscuits_post_comment) {
    factories.fetch(:comments).call(
      id: "comments/1",
      body: "oh noes",
      commenter: nil,
    )
  }

  let(:cat_biscuits_category) {
    factories.fetch(:categories).call(
      id: "categories/1",
      name: "Cat biscuits",
      posts: [],
    )
  }

  let(:chilling_category) {
    factories.fetch(:categories).call(
      id: "categories/2",
      name: "Chillaxing",
      posts: [],
    )
  }
end
<commit_msg>Better error message when objects are missing attributes<commit_after>require "terrestrial/serializer"
require "terrestrial/struct_factory"

RSpec.shared_context "object graph setup" do

  before do
    setup_circular_references_avoiding_stack_overflow
  end

  def setup_circular_references_avoiding_stack_overflow
    biscuits_post_comment.commenter = hansel
    cat_biscuits_category.posts = [ biscuits_post ]
  end

  class PlainObject
    def self.with_members(*list, &block)
      Class.new(self).tap { |klass|
        klass.instance_variable_set(:@members, list)
        klass.class_exec(&block) if block
        klass.send(:attr_accessor, *list)
      }
    end

    def self.members
      @members
    end

    def initialize(attrs)
      if members.sort != attrs.keys.sort
        missing = (members - attrs.keys).sort
        unexpected = (attrs.keys - members).sort
        raise(ArgumentError.new(
          "#{self.class} initialized with incorrect arguments." \
          "Missing: #{missing}. " \
          "Unexpected: #{unexpected}. " \
          "Received: #{attrs.keys}."
        ))
      end

      members.each { |member| send("#{member}=", attrs.fetch(member)) }
    end

    def members
      self.class.members
    end

    def to_h
      Hash[members.map { |field| [field, send(field)] }]
    end
  end

  User ||= PlainObject.with_members(:id, :first_name, :last_name, :email, :posts)
  Post ||= PlainObject.with_members(:id, :subject, :body, :comments, :categories, :created_at)
  Comment ||= PlainObject.with_members(:id, :commenter, :body)
  Category ||= PlainObject.with_members(:id, :name, :posts)

  let(:factories) {
    {
      users: User.method(:new),
      posts: Post.method(:new),
      comments: Comment.method(:new),
      categories: Category.method(:new),
      categories_to_posts: ->(x){x},
      noop: ->(x){x},
    }
  }

  let(:default_serializer) {
    ->(fields) {
      ->(object) {
        Terrestrial::Serializer.new(fields, object).to_h
      }
    }
  }

  let(:null_serializer) {
    ->(_fields) {
      ->(x){x}
    }
  }

  let(:hansel) {
    factories.fetch(:users).call(
      id: "users/1",
      first_name: "Hansel",
      last_name: "Trickett",
      email: "<EMAIL>",
      posts: [
        biscuits_post,
        sleep_post,
      ],
    )
  }

  let(:biscuits_post) {
    factories.fetch(:posts).call(
      id: "posts/1",
      subject: "Biscuits",
      body: "I like them",
      comments: [
        biscuits_post_comment,
      ],
      categories: [
        cat_biscuits_category,
      ],
      created_at: Time.parse("2015-09-05T15:00:00+01:00"),
    )
  }

  let(:sleep_post) {
    factories.fetch(:posts).call(
      id: "posts/2",
      subject: "Sleeping",
      body: "I do it three times purrr day",
      comments: [],
      categories: [
        chilling_category,
      ],
      created_at: Time.parse("2015-09-02T15:00:00+01:00"),
    )
  }

  let(:biscuits_post_comment) {
    factories.fetch(:comments).call(
      id: "comments/1",
      body: "oh noes",
      commenter: nil,
    )
  }

  let(:cat_biscuits_category) {
    factories.fetch(:categories).call(
      id: "categories/1",
      name: "Cat biscuits",
      posts: [],
    )
  }

  let(:chilling_category) {
    factories.fetch(:categories).call(
      id: "categories/2",
      name: "Chillaxing",
      posts: [],
    )
  }
end

{'fraction_non_alphanumeric': 0.14499460126484653, 'fraction_numerical': 0.012956964368347987, 'mean_word_length': 2.5296679368535657, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 17, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3933312', 'n_tokens_mistral': 2292, 'n_tokens_neox': 2204, 'n_words': 382}