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

Conditional attributes/associations (if/unless) #1403

Merged
merged 6 commits into from
Jan 13, 2016

Conversation

beauby
Copy link
Contributor

@beauby beauby commented Dec 28, 2015

My take at conditional attributes/associations. Previous work has been done in this direction (#1266), but I figured it was easily expressed on top of #1370.
This PR adds support for if/unless options to attribute/association definitions in serializers. Those options take a symbol of a method name on the serializer.

Example:

class UserSerializer < ActiveModel::Serializer
  attributes :name, :description, :date_of_birth
  attribute :private_data, if: :is_current_user?

  def is_current_user?
    object.id == current_user.id
  end
end

Note: if this gets traction, I'll add tests.

Ref #1245, #1266, #1058, #922, #1141

@beauby beauby changed the title Conditional attributes Conditional attributes (if/unless) Dec 28, 2015
@beauby beauby force-pushed the conditional-attributes branch 2 times, most recently from 6ea8dcb to bcd428e Compare December 28, 2015 01:12
@beauby beauby changed the title Conditional attributes (if/unless) Conditional attributes/associations (if/unless) Dec 28, 2015
def include?(serializer)
case condition_type
when :if
serializer.send(condition)
Copy link
Member

Choose a reason for hiding this comment

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

public_send ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just updated it.

@bf4
Copy link
Member

bf4 commented Dec 28, 2015

Looks like a good approach to me.

class Serializer
Attribute = Struct.new(:name, :block, :key, :condition_type, :condition) do
def initialize(name, block, options)
super(name, block)
Copy link
Member

Choose a reason for hiding this comment

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

since the :key, :condition_type, :condition attributes to an initializer lumped as options (which I initially missed), why not just take in options and create our own accessors for key, condition_type, condition?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's indeed a possibility. The rationale was to do the work only once.

Copy link
Member

Choose a reason for hiding this comment

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

By 'work' you mean defining accessors for the options? Ok, I get that. I think I'd rather add an attr_accessor than change the Struct definition if I needed to change the options. It took me a couple of reads of the object before I realized you were treating most of the struct attributes as options. (I'm not new to subclassing the initialize... I just usually use it for setting defaults for unset values, rather than as implicit accessors).

Do you foresee taking advantage of the struct's comparable properties? I'm trying to balance the clever vs. useful in my head.

@bf4
Copy link
Member

bf4 commented Dec 30, 2015

Also ref #1333

@beauby beauby force-pushed the conditional-attributes branch from 755516b to d2887a2 Compare December 30, 2015 15:45
@beauby beauby closed this Dec 30, 2015
@beauby beauby force-pushed the conditional-attributes branch from d2887a2 to 7bc66c5 Compare December 30, 2015 16:04
@beauby beauby reopened this Dec 30, 2015
@beauby beauby force-pushed the conditional-attributes branch 2 times, most recently from dc3e8e3 to 92dd9b6 Compare December 30, 2015 16:52

def condition
options[condition_type]
end
Copy link
Member

Choose a reason for hiding this comment

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

yet another sign there's a common ancestor or module

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed, we could either make a Field class from which Attribute and Reflection would inheritate, or a ConditionalField mixin. Not sure what's best, and naming is obviously up for debate.

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 regular inheritance makes sense in this case (if we include more than just the if/else)

@bf4
Copy link
Member

bf4 commented Dec 30, 2015

Looking good.. let's finish #1370 and add some tests and docs here :)

@bf4
Copy link
Member

bf4 commented Jan 4, 2016

@beauby #1370 is merged, so let's rebase, add tests, and docs!

@beauby beauby force-pushed the conditional-attributes branch from 30b8d33 to 0641f25 Compare January 4, 2016 19:25
@@ -0,0 +1,40 @@
module ActiveModel
class Serializer
Field = Struct.new(:name, :options, :block) do
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Kept it as a struct because tests in test/serializers/association_macros_test.rb made use of the comparability. Wouldn't mind modifying those in a subsequent PR.

@beauby beauby force-pushed the conditional-attributes branch 2 times, most recently from 5bc8eea to 656b232 Compare January 4, 2016 20:06
@beauby
Copy link
Contributor Author

beauby commented Jan 12, 2016

PR ready.

@@ -0,0 +1,56 @@
module ActiveModel
class Serializer
# Holds all the meta-data about a field (i.e. attribute or association) as it was
Copy link
Member

Choose a reason for hiding this comment

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

grammar: s/i.e./e.g.

reason: meaning is to change 'that is' to 'for example'

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I actually meant i.e., because I'm just defining what "field" means.

Copy link
Member

Choose a reason for hiding this comment

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

still think it should be e.g., but since it's intentional on your part and it's super unimportant, ok to merge when green

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed that it's not super important, but as I like grammar as well: a field is defined as "either an attribute or an association".

@bf4
Copy link
Member

bf4 commented Jan 13, 2016

@beauby two small comments, one grammar, one internal api. 💯 LGTM

@bf4 bf4 added the S: LGTM label Jan 13, 2016
@bf4 bf4 deleted the conditional-attributes branch March 1, 2016 09:07
@Mohakjuneja
Copy link

Is the support for if/unless already implemented in the master? I don't want my subjects association to be included when the controller method is index
Here's what I tried-

class Admin::SemesterSerializer < ActiveModel::Serializer

  attributes :_id, :semester, :course_id

  has_many :subjects, serializer: Admin::SubjectSerializer, unless: :index

end

But I get the error undefined method `index' for #<Admin::SemesterSerializer

@NullVoxPopuli
Copy link
Contributor

@Mohakjuneja the serializer doesn't have access to the controller's methods, as the passed in scope defaults to current_user

Have you seen these docs? https://github.com/rails-api/active_model_serializers/blob/master/docs/general/serializers.md#scope

maybe they could help?

@Mohakjuneja
Copy link

I figured it out! I apologize for being impatient and posting the issue too soon. AMS has consumed a lot of my time upgrading from 0.9.3 to the master. Thank you!

@NullVoxPopuli
Copy link
Contributor

what did you do? (for those who may come by this issue in the future with the same problem?)

@Mohakjuneja
Copy link

I added the index: true part to the end of my controller method

respond_with semesters.where(:semester_num.lte => current_user.current_semester).desc(:semester_num), each_serializer: Admin::SemesterSerializer, index: true

And in the serializer, I accessed it using the instance_options[:index] method

@bf4
Copy link
Member

bf4 commented Apr 5, 2016

@Mohakjuneja

AMS has consumed a lot of my time upgrading from 0.9.3 to the master. Thank you!

You would win many friends and much karma if you could help us write an updating guide! ref: #1005

@Mohakjuneja
Copy link

@bf4 I'm starting up my own venture (wish me luck!) but I'll be glad to help you with it! This community has been very supportive :)

@jDeppen
Copy link

jDeppen commented Apr 6, 2016

I was able to determine the view by adding this to my controller
serialization_scope :view_context
and this to my serializer

[:name, :birthday, :address, :phone].each { |attr| attribute attr, if: :show? }
def show?
  view_context.action_name == 'show'
end

@Mohakjuneja
Copy link

It stopped working all of a sudden.
throws error at the if: symbol now on this line

SyntaxError (/home/mojo/Documents/Rails/hopin/app/serializers/api/v1/content_serializer.rb:5: syntax error, unexpected ':'

attribute :total, :subject_name if: :view_uploads?
                                     ^):
  app/serializers/api/v1/content_serializer.rb:5: syntax error, unexpected ':'

What just happened?

@NullVoxPopuli
Copy link
Contributor

You need a comma before your :if :-)

@Mohakjuneja
Copy link

I found the error. But I don't know how to correct it.
It works if I only include one attribute
attribute :total, if: :view_uploads?

It's not working for this syntax anymore
attribute :total, :subject_name ,if: :view_uploads?
Throws this error
ArgumentError (wrong number of arguments (3 for 1..2)):

Does that mean I need to write another line for :subject_name?

@NullVoxPopuli
Copy link
Contributor

attributes can be used for multiple attributes. :-)

@Mohakjuneja
Copy link

attributes :total, :subject_name, if: :view_uploads?
TypeError ({:if=>:view_uploads?} is not a symbol nor a string)

I'm sorry about taking so much of your time but I've tried everything

@NullVoxPopuli
Copy link
Contributor

so maybe you do need to define each attribute on it's own line with it's own if condition, so:

attribute :total, if: :view_uploads?
attribute :subject_name, if: :view_uploads?

@Mohakjuneja
Copy link

I'll do that for now but I don't find that a very effective solution. We may need to take a look at it. Also I don't know why it worked two days ago but doesn't work anymore. Thank you @NullVoxPopuli :)

@bf4
Copy link
Member

bf4 commented Apr 7, 2016

@Mohakjuneja that's what version control is for :) Let me know if I'm summarizing this correctly (ignoring syntax errors), then please open a new issue if we need further discussion.

works

attribute :total, if: :view_uploads?
attribute :subject_name, if: :view_uploads?

desired, doesn't work

attributes :total, :subject_name, if: :view_uploads? 

@Mohakjuneja
Copy link

@bf4 I've been thinking about the current syntax and I'm wondering if we should stick to it.
In cases where you want a certain attribute to be present in multiple cases but not all, this makes more sense.
for instance


attribute :total, if: [:view_uploads?, edit_content]
attribute :subject_name, if: :view_uploads?

What is your opinion about that?

@beauby
Copy link
Contributor Author

beauby commented Apr 9, 2016

I'd much rather allow lambdas as if/unless values than introduce a cumbersome "array of method name symbols" syntax (which wouldn't make it obvious whether it is a conjunction or a disjunction).

lambda2 added a commit to lambda2/active_model_serializers that referenced this pull request May 18, 2016
Adding documentation and short example ([from this pull request](rails-api#1403)) on conditional attributes.
lambda2 added a commit to lambda2/active_model_serializers that referenced this pull request May 19, 2016
Adding documentation and short example ([from this pull request](rails-api#1403)) on conditional attributes.

Adding lambda literal notation and example.

Adding lambda literal notation and example, and fixing typo.

Removing PR reminder

Adding Changelog entry

Moving CHANGELOG entry under master (unreleased)
lambda2 added a commit to lambda2/active_model_serializers that referenced this pull request May 19, 2016
Adding documentation and short example ([from this pull request](rails-api#1403)) on conditional attributes.

Adding lambda literal notation and example.

Adding lambda literal notation and example, and fixing typo.

Removing PR reminder

Adding Changelog entry

Moving CHANGELOG entry under master (unreleased)

Use option instead of parameter
@majedbojan
Copy link

the attributes doesn't work with scope if your shown multiple attributes the correct way put it as arry

%w[attribute1 attribute2 attribute3].each { |att| attribute att, if: :condition? }

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.

8 participants