1# -*- coding: utf-8 -*- #
2# Copyright 2017 Google LLC. All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#    http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16"""Classes to specify concept and resource specs.
17
18Concept specs hold information about concepts. "Concepts" are any entity that
19has multiple attributes, which can be specified via multiple flags on the
20command line. A single concept spec should be created and re-used for the same
21concept everywhere it appears.
22
23Resource specs (currently the only type of concept spec used in gcloud) hold
24information about a Cloud resource. "Resources" are types of concepts that
25correspond to Cloud resources specified by a collection path, such as
26'example.projects.shelves.books'. Their attributes correspond to the parameters
27of their collection path. As with concept specs, a single resource spec
28should be defined and re-used for each collection.
29
30For resources, attributes can be configured by ResourceParameterAttributeConfigs
31using kwargs. In many cases, users should also be able to reuse configs for the
32same attribute across several resources (for example,
33'example.projects.shelves.books.pages' could also use the shelf and project
34attribute configs).
35"""
36
37from __future__ import absolute_import
38from __future__ import division
39from __future__ import unicode_literals
40
41import copy
42import re
43
44from googlecloudsdk.calliope.concepts import deps as deps_lib
45from googlecloudsdk.command_lib.util.apis import registry
46from googlecloudsdk.command_lib.util.apis import yaml_command_schema_util as util
47from googlecloudsdk.core import exceptions
48from googlecloudsdk.core import properties
49from googlecloudsdk.core import resources
50import six
51
52
53IGNORED_FIELDS = {
54    'project': 'project',
55    'projectId': 'project',
56    'projectsId': 'project',
57}
58
59
60class Error(exceptions.Error):
61  """Base class for errors in this module."""
62
63
64class InitializationError(Error):
65  """Raised if a spec fails to initialize."""
66
67
68class ResourceConfigurationError(Error):
69  """Raised if a resource is improperly declared."""
70
71
72class InvalidResourceArgumentLists(Error):
73  """Exception for missing, extra, or out of order arguments."""
74
75  def __init__(self, expected, actual):
76    expected = ['[' + e + ']' if e in IGNORED_FIELDS else e for e in expected]
77    super(InvalidResourceArgumentLists, self).__init__(
78        'Invalid resource arguments: Expected [{}], Found [{}].'.format(
79            ', '.join(expected), ', '.join(actual)))
80
81
82class ConceptSpec(object):
83  """Base class for concept args."""
84
85  @property
86  def attributes(self):
87    """A list of Attribute objects representing the attributes of the concept.
88
89    Must be defined in subclasses.
90    """
91    raise NotImplementedError
92
93  @property
94  def name(self):
95    """The name of the overall concept.
96
97    Must be defined in subclasses.
98    """
99    raise NotImplementedError
100
101  def Initialize(self, deps):
102    """Initializes the concept using information provided by a Deps object.
103
104    Must be defined in subclasses.
105
106    Args:
107      deps: googlecloudsdk.calliope.concepts.deps.Deps object representing the
108        fallthroughs for the concept's attributes.
109
110    Returns:
111      the initialized concept.
112
113    Raises:
114      InitializationError, if the concept cannot be initialized.
115    """
116    raise NotImplementedError
117
118  def Parse(self, attribute_to_args_map, base_fallthroughs_map,
119            parsed_args=None, plural=False, allow_empty=False):
120    """Lazy parsing function for resource.
121
122    Must be overridden in subclasses.
123
124    Args:
125      attribute_to_args_map: {str: str}, A map of attribute names to the names
126        of their associated flags.
127      base_fallthroughs_map: {str: [deps.Fallthrough]} A map of attribute
128        names to non-argument fallthroughs, including command-level
129        fallthroughs.
130      parsed_args: the parsed Namespace.
131      plural: bool, True if multiple resources can be parsed, False otherwise.
132      allow_empty: bool, True if resource parsing is allowed to return no
133        resource, otherwise False.
134
135    Returns:
136      the initialized resource or a list of initialized resources if the
137        resource argument was pluralized.
138    """
139    raise NotImplementedError
140
141  def __eq__(self, other):
142    if not isinstance(other, type(self)):
143      return False
144    return self.name == other.name and self.attributes == other.attributes
145
146  def __hash__(self):
147    return hash(self.name) + hash(self.attributes)
148
149
150class _Attribute(object):
151  """A base class for concept attributes.
152
153  Attributes:
154    name: The name of the attribute. Used primarily to control the arg or flag
155      name corresponding to the attribute. Must be in all lower case.
156    help_text: String describing the attribute's relationship to the concept,
157      used to generate help for an attribute flag.
158    required: True if the attribute is required.
159    fallthroughs: [googlecloudsdk.calliope.concepts.deps_lib.Fallthrough], the
160      list of sources of data, in priority order, that can provide a value for
161      the attribute if not given on the command line. These should only be
162      sources inherent to the attribute, such as associated properties, not
163      command-specific sources.
164    completer: core.cache.completion_cache.Completer, the completer associated
165      with the attribute.
166    value_type: the type to be accepted by the attribute arg. Defaults to str.
167  """
168
169  def __init__(self, name, help_text=None, required=False, fallthroughs=None,
170               completer=None, value_type=None):
171    """Initializes."""
172    # Check for attributes that mix lower- and uppercase. Camel case is not
173    # handled consistently among libraries.
174    if re.search(r'[A-Z]', name) and re.search('r[a-z]', name):
175      raise ValueError(
176          'Invalid attribute name [{}]: Attribute names should be in lower '
177          'snake case (foo_bar) so they can be transformed to flag names.'
178          .format(name))
179    self.name = name
180    self.help_text = help_text
181    self.required = required
182    self.fallthroughs = fallthroughs or []
183    self.completer = completer
184    self.value_type = value_type or six.text_type
185
186  def __eq__(self, other):
187    """Overrides."""
188    if not isinstance(other, type(self)):
189      return False
190    return (self.name == other.name and self.help_text == other.help_text
191            and self.required == other.required
192            and self.completer == other.completer
193            and self.fallthroughs == other.fallthroughs
194            and self.value_type == other.value_type)
195
196  def __hash__(self):
197    return sum(map(hash, [
198        self.name, self.help_text, self.required, self.completer,
199        self.value_type])) + sum(map(hash, self.fallthroughs))
200
201
202class Attribute(_Attribute):
203  """An attribute of a resource.
204
205  Has all attributes of the base class along with resource-specific attributes.
206
207  Attributes:
208    completion_request_params: {str: str}, a dict of field names to params to
209      use as static field values in any request to complete this resource.
210    completion_id_field: str, the ID field of the return value in the
211        response for completion requests.
212  """
213
214  def __init__(self, name, completion_request_params=None,
215               completion_id_field=None, **kwargs):
216    """Initializes."""
217    self.completion_request_params = completion_request_params or {}
218    self.completion_id_field = completion_id_field
219    super(Attribute, self).__init__(name, **kwargs)
220
221  def __eq__(self, other):
222    """Overrides."""
223    return (super(Attribute, self).__eq__(other)
224            and self.completer == other.completer
225            and self.completion_request_params
226            == other.completion_request_params
227            and self.completion_id_field == other.completion_id_field)
228
229  def __hash__(self):
230    return super(Attribute, self).__hash__() + sum(
231        map(hash, [six.text_type(self.completer),
232                   six.text_type(self.completion_request_params),
233                   self.completion_id_field]))
234
235
236class ResourceSpec(ConceptSpec):
237  """Defines a Cloud resource as a set of attributes for argument creation.
238  """
239  # TODO(b/67707644): Enable completers by default when confident enough.
240  disable_auto_complete = True
241
242  # TODO(b/78851830): update the documentation to use this method.
243  @classmethod
244  def FromYaml(cls, yaml_data, api_version=None):
245    """Constructs an instance of ResourceSpec from yaml data.
246
247    Args:
248      yaml_data: dict, the parsed data from a resources.yaml file under
249        command_lib/.
250      api_version: string, overrides the default version in the resource
251        registry if provided.
252
253    Returns:
254      A ResourceSpec object.
255    """
256    if not yaml_data:
257      return None
258    collection = registry.GetAPICollection(
259        yaml_data['collection'], api_version=api_version)
260    attributes = ParseAttributesFromData(
261        yaml_data.get('attributes'), collection.detailed_params)
262    return cls(
263        resource_collection=collection.full_name,
264        resource_name=yaml_data['name'],
265        api_version=collection.api_version,
266        disable_auto_completers=yaml_data.get(
267            'disable_auto_completers', ResourceSpec.disable_auto_complete),
268        plural_name=yaml_data.get('plural_name'),
269        **{attribute.parameter_name: attribute for attribute in attributes})
270
271  def __init__(self, resource_collection, resource_name='resource',
272               api_version=None, disable_auto_completers=disable_auto_complete,
273               plural_name=None, **kwargs):
274    """Initializes a ResourceSpec.
275
276    To use a ResourceSpec, give a collection path such as
277    'cloudiot.projects.locations.registries', and optionally an
278    API version.
279
280    For each parameter in the collection path, an attribute is added to the
281    resource spec. Names can be created by default or overridden in the
282    attribute_configs dict, which maps from the parameter name to a
283    ResourceParameterAttributeConfig object. ResourceParameterAttributeConfigs
284    also contain information about the help text that describes the attribute.
285
286    Attribute naming: By default, attributes are named after their collection
287    path param names, or "name" if they are the "anchor" attribute (the final
288    parameter in the path).
289
290    Args:
291      resource_collection: The collection path of the resource.
292      resource_name: The name of the resource, which will be used in attribute
293        help text. Defaults to 'resource'.
294      api_version: Overrides the default version in the resource
295        registry.
296      disable_auto_completers: bool, whether to add completers automatically
297        where possible.
298      plural_name: str, the pluralized name. Will be pluralized by default rules
299        if not given in cases where the resource is referred to in the plural.
300      **kwargs: Parameter names (such as 'projectsId') from the
301        collection path, mapped to ResourceParameterAttributeConfigs.
302
303    Raises:
304      ResourceConfigurationError: if the resource is given unknown params or the
305        collection has no params.
306    """
307    self._name = resource_name
308    self.plural_name = plural_name
309    self.collection = resource_collection
310    self._resources = resources.REGISTRY.Clone()
311    self._collection_info = self._resources.GetCollectionInfo(
312        resource_collection, api_version=api_version)
313    self.disable_auto_completers = disable_auto_completers
314    collection_params = self._collection_info.GetParams('')
315    self._attributes = []
316    self._param_names_map = {}
317
318    orig_kwargs = list(six.iterkeys(kwargs))
319    # Add attributes.
320    anchor = False
321    for i, param_name in enumerate(collection_params):
322      if i == len(collection_params) - 1:
323        anchor = True
324      attribute_config = kwargs.pop(param_name,
325                                    ResourceParameterAttributeConfig())
326      attribute_name = self._AttributeName(param_name, attribute_config,
327                                           anchor=anchor)
328
329      new_attribute = Attribute(
330          name=attribute_name,
331          help_text=attribute_config.help_text,
332          required=True,
333          fallthroughs=attribute_config.fallthroughs,
334          completer=attribute_config.completer,
335          value_type=attribute_config.value_type,
336          completion_request_params=attribute_config.completion_request_params,
337          completion_id_field=attribute_config.completion_id_field)
338      self._attributes.append(new_attribute)
339      # Keep a map from attribute names to param names. While attribute names
340      # are used for error messaging and arg creation/parsing, resource parsing
341      # during command runtime requires parameter names.
342      self._param_names_map[new_attribute.name] = param_name
343    if not self._attributes:
344      raise ResourceConfigurationError('Resource [{}] has no parameters; no '
345                                       'arguments will be generated'.format(
346                                           self._name))
347    if kwargs:
348      raise ResourceConfigurationError('Resource [{}] was given an attribute '
349                                       'config for unknown attribute(s): '
350                                       'Expected [{}], Found [{}]'
351                                       .format(self._name,
352                                               ', '.join(collection_params),
353                                               ', '.join(orig_kwargs)))
354
355  @property
356  def attributes(self):
357    return self._attributes
358
359  @property
360  def name(self):
361    return self._name
362
363  @property
364  def anchor(self):
365    """The "anchor" attribute of the resource."""
366    # self.attributes cannot be empty; will cause an error on init.
367    return self.attributes[-1]
368
369  def IsAnchor(self, attribute):
370    """Convenience method."""
371    return attribute == self.anchor
372
373  @property
374  def attribute_to_params_map(self):
375    """A map from all attribute names to param names."""
376    return self._param_names_map
377
378  @property
379  def collection_info(self):
380    return self._collection_info
381
382  def _AttributeName(self, param_name, attribute_config, anchor=False):
383    """Chooses attribute name for a param name.
384
385    If attribute_config gives an attribute name, that is used. Otherwise, if the
386    param is an anchor attribute, 'name' is used, or if not, param_name is used.
387
388    Args:
389      param_name: str, the parameter name from the collection.
390      attribute_config: ResourceParameterAttributeConfig, the config for the
391        param_name.
392      anchor: bool, whether the parameter is the "anchor" or the last in the
393        collection path.
394
395    Returns:
396      (str) the attribute name.
397    """
398    if attribute_config.attribute_name:
399      return attribute_config.attribute_name
400    if anchor:
401      return 'name'
402    return param_name.replace('Id', '_id').lower()
403
404  def ParamName(self, attribute_name):
405    """Given an attribute name, gets the param name for resource parsing."""
406    if attribute_name not in self.attribute_to_params_map:
407      raise ValueError(
408          'No param name found for attribute [{}]. Existing attributes are '
409          '[{}]'.format(attribute_name,
410                        ', '.join(sorted(self.attribute_to_params_map.keys()))))
411    return self.attribute_to_params_map[attribute_name]
412
413  def AttributeName(self, param_name):
414    """Given a param name, gets the attribute name."""
415    for attribute_name, p in six.iteritems(self.attribute_to_params_map):
416      if p == param_name:
417        return attribute_name
418
419  def Initialize(self, fallthroughs_map, parsed_args=None):
420    """Initializes a resource given its fallthroughs.
421
422    If the attributes have a property or arg fallthrough but the full
423    resource name is provided to the anchor attribute flag, the information
424    from the resource name is used over the properties and args. This
425    preserves typical resource parsing behavior in existing surfaces.
426
427    Args:
428      fallthroughs_map: {str: [deps_lib._FallthroughBase]}, a dict of finalized
429        fallthroughs for the resource.
430      parsed_args: the argparse namespace.
431
432    Returns:
433      (googlecloudsdk.core.resources.Resource) the fully initialized resource.
434
435    Raises:
436      googlecloudsdk.calliope.concepts.concepts.InitializationError, if the
437        concept can't be initialized.
438    """
439    params = {}
440
441    # Returns a function that can be used to parse each attribute, which will be
442    # used only if the resource parser does not receive a fully qualified
443    # resource name.
444    def LazyGet(name):
445      f = lambda: deps_lib.Get(name, fallthroughs_map, parsed_args=parsed_args)
446      return f
447
448    for attribute in self.attributes:
449      params[self.ParamName(attribute.name)] = LazyGet(attribute.name)
450    self._resources.RegisterApiByName(self._collection_info.api_name,
451                                      self._collection_info.api_version)
452    try:
453      return self._resources.Parse(
454          deps_lib.Get(
455              self.anchor.name, fallthroughs_map, parsed_args=parsed_args),
456          collection=self.collection,
457          params=params)
458    except deps_lib.AttributeNotFoundError as e:
459      raise InitializationError(
460          'The [{}] resource is not properly specified.\n'
461          '{}'.format(self.name, six.text_type(e)))
462    except resources.UserError as e:
463      raise InitializationError(six.text_type(e))
464
465  def Parse(self, attribute_to_args_map, base_fallthroughs_map,
466            parsed_args=None, plural=False, allow_empty=False):
467    """Lazy parsing function for resource.
468
469    Args:
470      attribute_to_args_map: {str: str}, A map of attribute names to the names
471        of their associated flags.
472      base_fallthroughs_map: {str: [deps_lib.Fallthrough]} A map of attribute
473        names to non-argument fallthroughs, including command-level
474        fallthroughs.
475      parsed_args: the parsed Namespace.
476      plural: bool, True if multiple resources can be parsed, False otherwise.
477      allow_empty: bool, True if resource parsing is allowed to return no
478        resource, otherwise False.
479
480    Returns:
481      the initialized resource or a list of initialized resources if the
482        resource argument was pluralized.
483    """
484    if not plural:
485      fallthroughs_map = self.BuildFullFallthroughsMap(
486          attribute_to_args_map, base_fallthroughs_map,
487          with_anchor_fallthroughs=False)
488      try:
489        return self.Initialize(
490            fallthroughs_map, parsed_args=parsed_args)
491      except InitializationError:
492        if allow_empty:
493          return None
494        raise
495
496    results = self._ParseFromPluralValue(attribute_to_args_map,
497                                         base_fallthroughs_map,
498                                         self.anchor,
499                                         parsed_args)
500    if results:
501      return results
502
503    if allow_empty:
504      return []
505    fallthroughs_map = self.BuildFullFallthroughsMap(
506        attribute_to_args_map, base_fallthroughs_map)
507    return self.Initialize(
508        base_fallthroughs_map, parsed_args=parsed_args)
509
510  def _ParseFromPluralValue(self, attribute_to_args_map, base_fallthroughs_map,
511                            plural_attribute, parsed_args):
512    """Helper for parsing a list of results from a plural fallthrough."""
513    attribute_name = plural_attribute.name
514    fallthroughs_map = self.BuildFullFallthroughsMap(
515        attribute_to_args_map, base_fallthroughs_map, plural=True,
516        with_anchor_fallthroughs=False)
517    current_fallthroughs = fallthroughs_map.get(attribute_name, [])
518    # Iterate through the values provided to the argument, creating for
519    # each a separate parsed resource.
520    parsed_resources = []
521    for fallthrough in current_fallthroughs:
522      try:
523        values = fallthrough.GetValue(parsed_args)
524      except deps_lib.FallthroughNotFoundError:
525        continue
526      for value in values:
527        def F(return_value=value):
528          return return_value
529
530        new_fallthrough = deps_lib.Fallthrough(
531            F, fallthrough.hint, active=fallthrough.active)
532        fallthroughs_map[attribute_name] = [new_fallthrough]
533        # Add the anchor fallthroughs for this particular value, so that the
534        # error messages will contain the appropriate hints.
535        self._AddAnchorFallthroughs(plural_attribute, fallthroughs_map)
536        parsed_resources.append(
537            self.Initialize(
538                fallthroughs_map, parsed_args=parsed_args))
539      return parsed_resources
540
541  def BuildFullFallthroughsMap(self, attribute_to_args_map,
542                               base_fallthroughs_map, plural=False,
543                               with_anchor_fallthroughs=True):
544    """Builds map of all fallthroughs including arg names.
545
546    Fallthroughs are a list of objects that, when called, try different ways of
547    getting values for attributes (see googlecloudsdk.calliope.concepts.
548    deps_lib._Fallthrough). This method builds a map from the name of each
549    attribute to its fallthroughs, including the "primary" fallthrough
550    representing its corresponding argument value in parsed_args if any, and any
551    fallthroughs that were configured for the attribute beyond that.
552
553    Args:
554      attribute_to_args_map: {str: str}, A map of attribute names to the names
555        of their associated flags.
556      base_fallthroughs_map: {str: [deps_lib._FallthroughBase]}, A map of
557        attribute names to non-argument fallthroughs, including command-level
558        fallthroughs.
559      plural: bool, True if multiple resources can be parsed, False otherwise.
560      with_anchor_fallthroughs: bool, whether to add fully specified anchor
561        fallthroughs. Used only for getting help text/error messages,
562        and for determining which attributes are specified -- not for parsing.
563
564    Returns:
565      {str: [deps_lib._Fallthrough]}, a map from attribute name to its
566      fallthroughs.
567    """
568    fallthroughs_map = {}
569    for attribute in self.attributes:
570      fallthroughs_map[attribute.name] = (
571          self.GetArgAndBaseFallthroughsForAttribute(attribute_to_args_map,
572                                                     base_fallthroughs_map,
573                                                     attribute,
574                                                     plural=plural))
575    if not with_anchor_fallthroughs:
576      return fallthroughs_map
577    for attribute in self.attributes:
578      if self.IsAnchor(attribute):
579        self._AddAnchorFallthroughs(attribute, fallthroughs_map)
580    return fallthroughs_map
581
582  def GetArgAndBaseFallthroughsForAttribute(self,
583                                            attribute_to_args_map,
584                                            base_fallthroughs_map,
585                                            attribute,
586                                            plural=False):
587    """Gets all fallthroughs for an attribute except anchor-dependent ones."""
588    attribute_name = attribute.name
589    attribute_fallthroughs = []
590    # The only args that should be lists are anchor args for plural
591    # resources.
592    attribute_is_plural = self.IsAnchor(attribute) and plural
593
594    # Start the fallthroughs list with the primary associated arg for the
595    # attribute.
596    arg_name = attribute_to_args_map.get(attribute_name)
597    if arg_name:
598      attribute_fallthroughs.append(
599          deps_lib.ArgFallthrough(arg_name, plural=attribute_is_plural))
600
601    given_fallthroughs = base_fallthroughs_map.get(attribute_name, [])
602    for fallthrough in given_fallthroughs:
603      if attribute_is_plural:
604        fallthrough = copy.deepcopy(fallthrough)
605        fallthrough.plural = attribute_is_plural
606      attribute_fallthroughs.append(fallthrough)
607    return attribute_fallthroughs
608
609  def _GetAttributeAnchorFallthroughs(self, anchor_fallthroughs, attribute):
610    """Helper to get anchor-depednent fallthroughs for a specific attribute."""
611    parameter_name = self.ParamName(attribute.name)
612    anchor_based_fallthroughs = [
613        deps_lib.FullySpecifiedAnchorFallthrough(
614            anchor_fallthrough, self.collection_info, parameter_name)
615        for anchor_fallthrough in anchor_fallthroughs
616    ]
617    return anchor_based_fallthroughs
618
619  def _AddAnchorFallthroughs(self, anchor, fallthroughs_map):
620    """Helper for adding anchor fallthroughs to the fallthroughs map."""
621    anchor_fallthroughs = fallthroughs_map.get(anchor.name, [])
622    for attribute in self.attributes:
623      if attribute == anchor:
624        continue
625      anchor_based_fallthroughs = self._GetAttributeAnchorFallthroughs(
626          anchor_fallthroughs, attribute)
627      fallthroughs_map[attribute.name] = (
628          anchor_based_fallthroughs + fallthroughs_map[attribute.name])
629
630  def __eq__(self, other):
631    return (super(ResourceSpec, self).__eq__(other)
632            and self.disable_auto_completers == other.disable_auto_completers
633            and self.attribute_to_params_map == other.attribute_to_params_map)
634
635  def __hash__(self):
636    return super(ResourceSpec, self).__hash__() + sum(
637        map(hash, [self.disable_auto_completers, self.attribute_to_params_map]))
638
639
640class ResourceParameterAttributeConfig(object):
641  """Configuration used to create attributes from resource parameters."""
642
643  @classmethod
644  def FromData(cls, data):
645    """Constructs an attribute config from data defined in the yaml file.
646
647    Args:
648      data: {}, the dict of data from the YAML file for this single attribute.
649
650    Returns:
651      ResourceParameterAttributeConfig
652    """
653    if not data:
654      return None
655
656    attribute_name = data['attribute_name']
657    parameter_name = data['parameter_name']
658    help_text = data['help']
659    completer = util.Hook.FromData(data, 'completer')
660    completion_id_field = data.get('completion_id_field', None)
661    completion_request_params_list = data.get('completion_request_params', [])
662    completion_request_params = {
663        param.get('fieldName'): param.get('value')
664        for param in completion_request_params_list
665    }
666
667    # Add property fallthroughs.
668    fallthroughs = []
669    prop = properties.FromString(data.get('property', ''))
670    if prop:
671      fallthroughs.append(deps_lib.PropertyFallthrough(prop))
672    default_config = DEFAULT_RESOURCE_ATTRIBUTE_CONFIGS.get(attribute_name)
673    if default_config:
674      fallthroughs += [
675          f for f in default_config.fallthroughs if f not in fallthroughs]
676    # Add fallthroughs from python hooks.
677    fallthrough_data = data.get('fallthroughs', [])
678    fallthroughs_from_hook = [
679        deps_lib.Fallthrough(util.Hook.FromPath(f['hook']), hint=f['hint'])
680        for f in fallthrough_data
681    ]
682    fallthroughs += fallthroughs_from_hook
683    return cls(
684        name=attribute_name,
685        help_text=help_text,
686        fallthroughs=fallthroughs,
687        completer=completer,
688        completion_id_field=completion_id_field,
689        completion_request_params=completion_request_params,
690        parameter_name=parameter_name)
691
692  def __init__(self,
693               name=None,
694               help_text=None,
695               fallthroughs=None,
696               completer=None,
697               completion_request_params=None,
698               completion_id_field=None,
699               value_type=None,
700               parameter_name=None):
701    """Create a resource attribute.
702
703    Args:
704      name: str, the name of the attribute. This controls the naming of flags
705        based on the attribute.
706      help_text: str, generic help text for any flag based on the attribute. One
707        special expansion is available to convert "{resource}" to the name of
708        the resource.
709      fallthroughs: [deps_lib.Fallthrough], A list of fallthroughs to use to
710        resolve the attribute if it is not provided on the command line.
711      completer: core.cache.completion_cache.Completer, the completer
712        associated with the attribute.
713      completion_request_params: {str: value}, a dict of field names to static
714        values to fill in for the completion request.
715      completion_id_field: str, the ID field of the return value in the
716        response for completion commands.
717      value_type: the type to be accepted by the attribute arg. Defaults to str.
718      parameter_name: the API parameter name that this attribute maps to.
719    """
720    self.attribute_name = name
721    self.help_text = help_text
722    self.fallthroughs = fallthroughs or []
723    if completer and (completion_request_params or completion_id_field):
724      raise ValueError('Custom completer and auto-completer should not be '
725                       'specified at the same time')
726    self.completer = completer
727    self.completion_request_params = completion_request_params
728    self.completion_id_field = completion_id_field
729    self.value_type = value_type or six.text_type
730    self.parameter_name = parameter_name
731
732
733def ParseAttributesFromData(attributes_data, expected_param_names):
734  """Parses a list of ResourceParameterAttributeConfig from yaml data.
735
736  Args:
737    attributes_data: dict, the attributes data defined in
738      command_lib/resources.yaml file.
739    expected_param_names: [str], the names of the API parameters that the API
740      method accepts. Example, ['projectsId', 'instancesId'].
741
742  Returns:
743    [ResourceParameterAttributeConfig].
744
745  Raises:
746    InvalidResourceArgumentLists: if the attributes defined in the yaml file
747      don't match the expected fields in the API method.
748  """
749  raw_attributes = [
750      ResourceParameterAttributeConfig.FromData(a) for a in attributes_data
751  ]
752  registered_param_names = [a.parameter_name for a in raw_attributes]
753  final_attributes = []
754
755  # TODO(b/78851830): improve the time complexity here.
756  for expected_name in expected_param_names:
757    if raw_attributes and expected_name == raw_attributes[0].parameter_name:
758      # Attribute matches expected, add it and continue checking.
759      final_attributes.append(raw_attributes.pop(0))
760    elif expected_name in IGNORED_FIELDS:
761      # Attribute doesn't match but is being ignored. Add an auto-generated
762      # attribute as a substitute.
763      # Currently, it would only be the project config.
764      attribute_name = IGNORED_FIELDS[expected_name]
765      ignored_attribute = DEFAULT_RESOURCE_ATTRIBUTE_CONFIGS.get(attribute_name)
766      # Manually add the parameter name, e.g. project, projectId or projectsId.
767      ignored_attribute.parameter_name = expected_name
768      final_attributes.append(ignored_attribute)
769    else:
770      # It doesn't match (or there are no more registered params) and the
771      # field is not being ignored, error.
772      raise InvalidResourceArgumentLists(expected_param_names,
773                                         registered_param_names)
774
775  if raw_attributes:
776    # All expected fields were processed but there are still registered
777    # attribute params remaining, they must be extra.
778    raise InvalidResourceArgumentLists(expected_param_names,
779                                       registered_param_names)
780
781  return final_attributes
782
783
784DEFAULT_PROJECT_ATTRIBUTE_CONFIG = ResourceParameterAttributeConfig(
785    name='project',
786    help_text='Project ID of the Google Cloud Platform project for '
787              'the {resource}.',
788    fallthroughs=[
789        # Typically argument fallthroughs should be configured at the command
790        # level, but the --project flag is currently available in every command.
791        deps_lib.ArgFallthrough('--project'),
792        deps_lib.PropertyFallthrough(properties.VALUES.core.project)
793    ])
794DEFAULT_RESOURCE_ATTRIBUTE_CONFIGS = {
795    'project': DEFAULT_PROJECT_ATTRIBUTE_CONFIG}
796_DEFAULT_CONFIGS = {'project': DEFAULT_PROJECT_ATTRIBUTE_CONFIG}
797