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

Provide key transformation #1574

Merged
merged 1 commit into from
Mar 15, 2016
Merged

Provide key transformation #1574

merged 1 commit into from
Mar 15, 2016

Conversation

remear
Copy link
Member

@remear remear commented Mar 10, 2016

Purpose

Provide key case translation on a per-request basis.

render json: posts, each_serializer: PostSerializer, key_transform: :camel_lower

A global config option may be set to configure the key transform. ActiveModelSerializers.config.key_transform = :camel_lower

Changes

  • Adds a key_transform attribute to SerializationContext.
  • Adds a key_transform option to Configuration.
  • Translates keys for documents in the JsonApi adapter.
  • Translates keys for serialized hash in the Json adapter.
  • Provides the following casing options:
    • camel - SpecialAttribute
    • camel_lower - specialAttribute
    • dashed - special-attribute (default as per JSON API recommendation)
    • unaltered - the original, unaltered key
  • Key translation precedence is as follows:
    • SerializationContext - The key_transform is provided as an option via render.
    • Global config - The key_transform is set inActiveModelSerializers.config.key_transform`.
    • Adapter default - The default key_transform specified in the adapter.

@remear
Copy link
Member Author

remear commented Mar 10, 2016

@rails-api/ams @rails-api/commit Thoughts on this approach?

@@ -40,7 +40,7 @@ def test_toplevel_links
stuff: 'value'
}
}
}).serializable_hash
}).serializable_hash(@options)
Copy link
Member

Choose a reason for hiding this comment

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

❓ I don't think this should be here. Not defined anywhere and we haven't been passing anything interesting into the renderer options

Copy link
Member

Choose a reason for hiding this comment

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

there's a bug in this right now #1572

@bf4
Copy link
Member

bf4 commented Mar 10, 2016

Looks good. you up for benchmarking it? maybe https://github.com/bf4/book_code/blob/master/adrpo/code/chp8/assert_performance.rb or something.. can go in test/dummy now.. which I think does need to be renamed to test/benchmark #1576

@remear
Copy link
Member Author

remear commented Mar 10, 2016

I added a dashed case option and make it the default.

Also, note that rubocop wanted a lot of changes that weren't actually within the scope of this PR. I went ahead and fixed them in a separate commit.

@remear
Copy link
Member Author

remear commented Mar 10, 2016

The rubocop failures I see in travis right now look like the inverse of what it wanted locally, rubocop-0.38.0.

@bf4
Copy link
Member

bf4 commented Mar 10, 2016

#1577 I have a PR to fix the JRuby's.. should be green soon

@bf4
Copy link
Member

bf4 commented Mar 10, 2016

Offenses:
lib/active_model/serializer/collection_serializer.rb:17:11: C: Style/GuardClause: Use a guard clause instead of wrapping the code inside a conditional expression. (https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals)
          if serializer_class.nil?
          ^^
lib/active_model/serializable_resource.rb:36:5: C: Style/Alias: Use alias instead of alias_method in a class body. (https://github.com/bbatsov/ruby-style-guide#alias-method)
    alias_method :adapter_instance, :adapter
    ^^^^^^^^^^^^
lib/active_model/serializable_resource.rb:57:5: C: Style/Alias: Use alias instead of alias_method in a class body. (https://github.com/bbatsov/ruby-style-guide#alias-method)
    alias_method :serializer_class, :serializer
    ^^^^^^^^^^^^
lib/active_model_serializers/adapter.rb:4:19: C: Style/MutableConstant: Freeze mutable objects assigned to constants.
    ADAPTER_MAP = {}
                  ^^
test/support/serialization_testing.rb:24:3: C: Style/Alias: Use alias instead of alias_method in a module body. (https://github.com/bbatsov/ruby-style-guide#alias-method)
  alias_method :with_configured_adapter, :with_adapter
  ^^^^^^^^^^^^
156 files inspected, 5 offenses detected
RuboCop failed!

Style/Alias is a recent update to rubocop that was lagging the ruby-style-guide

That guard clause needs an inline rubocop:disable Style/GuardClause iirc

And I know there's a PR outstanding that appends rubocop:disable Style/MutableConstant to the ADAPTER_MAP

@@ -51,6 +51,20 @@ def include_meta(json)
json[meta_key] = meta if meta
json
end

def translate_key_casing!(value, serialization_context)
Copy link
Contributor

Choose a reason for hiding this comment

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

thoughts on writing these transforms in the style of active support inflections? #1566

at least for the sake of examples in comments,

maybe the key transforms could be in their own module?

module WhateverNamespace::KeyTransforms
  module_function
  # transforms keys to UpperCamelCase or PascalCase (this is what through me down this train of thought in the first place, though, this could be because I've been in non-ruby land at work)
  #
  # @example:
  #    "some_key" => "SomeKey",
  # @see: active_support/inflector#camelize
  def camel(hash)
    hash.deep_transform_keys! { |key| key.to_s.camelize.to_sym }
  end
  ...
end

and then this method would become:

return value unless serialization_context
transform = serialization_context.key_case
KeyTransforms.send(transform)

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not necessarily opposed to it. Are there other benefits to this approach over what's in the PR?

Copy link
Contributor

Choose a reason for hiding this comment

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

it would give a place for users to add their own transformations if they wish. Rather than overwriting translate_key_casing, they write their own transform method -- without losing or having to re-implement the 3 you have. I could see this being especially useful, for APIs that have clients that don't all expect the same casing.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fair enough. I just prefer to focus efforts on making features customizable where there's demand, not just because it can be or to support rare edge cases. I can see some cases where it might be desired to add a custom key translator, but how often would someone want to do that with a default set of, camel, camel_lower, and dashed? Is this a use case we want to spend time covering now and supporting in the future? Should we wait on supporting that until someone asks and/or submits a PR? If it's important, let's make a better interface happen.

Copy link
Contributor

Choose a reason for hiding this comment

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

I understand your point - what triggers this thought is partially the ? Pr / issue. Like, if someone wants to render attribute with ? they could keep / remove it as they desire.

But other than that one issue, I haven't heard of any desires for key formats other than what you have here.

@remear
Copy link
Member Author

remear commented Mar 10, 2016

@NullVoxPopuli Implemented. Let me know what you think.

@remear
Copy link
Member Author

remear commented Mar 10, 2016

Also note that now only the JsonApi adapter has the :dashed default key case transform. The Json defaults to :unaltered.

@@ -0,0 +1,43 @@
module ActiveModelSerializers
module KeyTransform
Copy link
Contributor

Choose a reason for hiding this comment

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

it may be benificial to mention the calling method: https://github.com/rails-api/active_model_serializers/pull/1574/files#diff-426cb6b4484fa95a18649ea3cc74474aR62

it may help future devs, idk

Copy link
Member

Choose a reason for hiding this comment

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

perhaps module_function here to allow public calling on the module but private methods on inclusion. I like me some utility function modules

Copy link
Contributor

Choose a reason for hiding this comment

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

It used to be in this file. not sure what happened to it.

@NullVoxPopuli
Copy link
Contributor

can you rebase, so you get the new JRuby modifications from @bf4 ?

@@ -33,7 +33,7 @@ def serialization_scope_name=(scope_name)
def adapter
@adapter ||= ActiveModelSerializers::Adapter.create(serializer_instance, adapter_opts)
end
alias adapter_instance adapter
alias_method :adapter_instance, :adapter
Copy link
Member

Choose a reason for hiding this comment

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

❗ revert, revert!

@bf4
Copy link
Member

bf4 commented Mar 11, 2016

@remear changeset should include config.jsonapi_resource_type = :plural right?

# @see: active_support/inflector#camelize
def camel(hash)
hash.deep_transform_keys! { |key| key.to_s.camelize.to_sym }
end
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 here you might mean to use a link like # @see {https://github.com/rails/rails/blob/v5.0.0.beta3/activesupport/lib/active_support/inflector/methods.rb#L66-L76 ActiveSupport::Inflector.camelize}

Copy link
Contributor

Choose a reason for hiding this comment

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

I didn't know you could use that syntax for an actual link -- legit

@remear
Copy link
Member Author

remear commented Mar 11, 2016

I'm not sure how config.jsonapi_resource_type = :plural relates to these changes. Could you explain further?


def self.method_missing(m, *args, &block)
warn "\"#{m}\" is not a defined key transform. Defaulting to :unaltered."
send(:unaltered, *args, &block)
Copy link
Member

Choose a reason for hiding this comment

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

this is super dangerous to eat a method missing entirely. I think the proper behavior here is to raise. I mean, if I say KeyTransform.micky_mouse it should fail, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

Do you mean you don't want to silently fail with a warning and default behavior? and would rather have it barf?

Copy link
Member Author

Choose a reason for hiding this comment

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

It just seemed nicer to just give back what was supplied and warn if they called an undefined method.

Copy link
Member

Choose a reason for hiding this comment

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

@remear @NullVoxPopuli So, before I asked:

If I say KeyTransform.micky_mouse it should fail, right?

Here's a better example, I think:

KeyTransform.camellower. Person mistyped camel_lower and now instead of a NoMethodError they're getting back :unaltered. That's surprising.

I feel pretty strongly this should fail fast. The warning message is good, but should be a failure message.

Also, since you're defining the method missing on the module, which you are including in Adapter::Base, you are also eating up all method missing calls on the Adapter::Base. Just seems like asking for trouble with only benefit is a silent failure with a warning somewhere in the logs.

Copy link
Contributor

Choose a reason for hiding this comment

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

oooo, that's a good point. that is a bad idea then.

@bf4
Copy link
Member

bf4 commented Mar 11, 2016

@remear config.jsonapi_resource_type = :plural isn't that key casing concern?

@remear remear changed the title Provide key case translation Provide key translation Mar 14, 2016
@remear remear changed the title Provide key translation Provide key transformation Mar 14, 2016

##### Adapter default

Each adapter has a default key transform configured:
Copy link
Contributor

Choose a reason for hiding this comment

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

nice, I was going to ask about that

@NullVoxPopuli
Copy link
Contributor

LGTM 👍

- `:camel` - ExampleKey
- `:camel_lower` - exampleKey
- `:dashed` - example-key
- `:unaltered` - the original, unaltered key
Copy link
Member

Choose a reason for hiding this comment

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

missing default. perhaps intentional, but the others have it so, should mention something

Copy link
Member Author

Choose a reason for hiding this comment

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

It was intentional because it's mentioned elsewhere, but a mention would be good.

Copy link
Member

Choose a reason for hiding this comment

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

ok, I see that the missing default option is nil: use the adapter default

@bf4
Copy link
Member

bf4 commented Mar 15, 2016

@remear 💯 we need to add a controller test. We've gotten in trouble before when not writing those :)

# "some_key" => "SomeKey",
# @see {https://github.com/rails/rails/blob/master/activesupport/lib/active_support/inflector/methods.rb#L66-L76 ActiveSupport::Inflector.camelize}
def camel(hash)
hash.deep_transform_keys! { |key| key.to_s.camelize.to_sym }
Copy link
Member

Choose a reason for hiding this comment

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

at top of file we should add require 'active_support/core_ext/hash/keys' to ensure the extension is loaded.

@remear remear force-pushed the key-casing branch 2 times, most recently from a56b942 to e7438fe Compare March 15, 2016 04:17

##### jsonapi_resource_type

Set the `type` attributes of resources to singular or plural.
Copy link
Member

Choose a reason for hiding this comment

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

Set the type attributes

Using attributes here seems inappropriate. What about something like this:

Sets whether the [type](http://jsonapi.org/format/#document-resource-identifier-objects) of the JSON API resource should be `singularized` or `pluralized` when it is not [explicitly specified by the serializer](https://github.com/rails-api/active_model_serializers/blob/master/docs/general/serializers.md#type):

Copy link
Member Author

Choose a reason for hiding this comment

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

It could use more clarification. I didn't go out of my way to refactor the content here.

This is already in the JSON API section so the copy should read within that context.

Sets whether the [type](http://jsonapi.org/format/#document-resource-identifier-objects) of the resource should be `singularized` or `pluralized` when it is not [explicitly specified by the serializer](https://github.com/rails-api/active_model_serializers/blob/master/docs/general/serializers.md#type)

Copy link
Member

Choose a reason for hiding this comment

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

Sounds good.

remear added a commit that referenced this pull request Mar 15, 2016
@remear remear merged commit 9e99235 into rails-api:master Mar 15, 2016
@rthbound
Copy link

@bf4, @remear
It looks like there's some problems with this... The changes (which result in failing tests) in this branch should highlight what I'm talking about.

Basically, we're transforming keys only... bad_comments where it would appear in keys is properly transformed, however bad_comments in values (e.g. type: "bad_comments") is not transformed.

This also suggests the action controller tests written for this were incomplete.

@remear
Copy link
Member Author

remear commented Mar 28, 2016

It doesn't cover everything yet. For example, this, ideally, would have been transformed from published_at to published-at. Transforming all values is too heavy-handed, so more finesse is needed for these cases. We'll get it fixed up soon.

@dgdosen
Copy link

dgdosen commented Mar 29, 2016

Still - this was greatly needed. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants