public static List<PipelineOptionDescriptor> describe(
      Set<Class<? extends PipelineOptions>> ifaces) {
    checkNotNull(ifaces);
    List<PipelineOptionDescriptor> result = new ArrayList<>();
    Set<Method> seenMethods = Sets.newHashSet();

    for (Class<? extends PipelineOptions> iface : ifaces) {
      CACHE.get().validateWellFormed(iface);

      Set<PipelineOptionSpec> properties = PipelineOptionsReflector.getOptionSpecs(iface, false);

      RowSortedTable<Class<?>, String, Method> ifacePropGetterTable =
          TreeBasedTable.create(ClassNameComparator.INSTANCE, Ordering.natural());
      for (PipelineOptionSpec prop : properties) {
        ifacePropGetterTable.put(
            prop.getDefiningInterface(), prop.getName(), prop.getGetterMethod());
      }

      for (Map.Entry<Class<?>, Map<String, Method>> ifaceToPropertyMap :
          ifacePropGetterTable.rowMap().entrySet()) {
        Class<?> currentIface = ifaceToPropertyMap.getKey();
        Map<String, Method> propertyNamesToGetters = ifaceToPropertyMap.getValue();

        List<@KeyFor("propertyNamesToGetters") String> lists =
            Lists.newArrayList(propertyNamesToGetters.keySet());
        lists.sort(String.CASE_INSENSITIVE_ORDER);
        for (String propertyName : lists) {
          Method method = propertyNamesToGetters.get(propertyName);
          if (!seenMethods.add(method)) {
            continue;
          }
          Class<?> returnType = method.getReturnType();
          PipelineOptionType.Enum optionType = PipelineOptionType.Enum.STRING;
          if (JSON_INTEGER_TYPES.contains(returnType)) {
            optionType = PipelineOptionType.Enum.INTEGER;
          } else if (JSON_NUMBER_TYPES.contains(returnType)) {
            optionType = PipelineOptionType.Enum.NUMBER;
          } else if (returnType == boolean.class || returnType == Boolean.class) {
            optionType = PipelineOptionType.Enum.BOOLEAN;
          } else if (List.class.isAssignableFrom(returnType)) {
            optionType = PipelineOptionType.Enum.ARRAY;
          }
          String optionName = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, propertyName);
          Description description = method.getAnnotation(Description.class);
          PipelineOptionDescriptor.Builder builder =
              PipelineOptionDescriptor.newBuilder()
                  .setName(optionName)
                  .setType(optionType)
                  .setGroup(currentIface.getName());
          Optional<String> defaultValue = getDefaultValueFromAnnotation(method);
          if (defaultValue.isPresent()) {
            builder.setDefaultValue(defaultValue.get());
          }
          if (description != null) {
            builder.setDescription(description.value());
          }
          result.add(builder.build());
        }
      }
    }
    return result;
  }
@Test
  public void testDescribe() {
    List<PipelineOptionDescriptor> described =
        PipelineOptionsFactory.describe(
            Sets.newHashSet(PipelineOptions.class, TestDescribeOptions.class));

    Map<String, PipelineOptionDescriptor> mapped = uniqueIndex(described, input -> input.getName());
    assertEquals("no duplicates", described.size(), mapped.size());

    Collection<PipelineOptionDescriptor> filtered =
        Collections2.filter(
            described, input -> input.getGroup().equals(TestDescribeOptions.class.getName()));
    assertEquals(6, filtered.size());
    mapped = uniqueIndex(filtered, input -> input.getName());

    PipelineOptionDescriptor listDesc = mapped.get("list");
    assertThat(listDesc, notNullValue());
    assertThat(listDesc.getDescription(), isEmptyString());
    assertEquals(PipelineOptionType.Enum.ARRAY, listDesc.getType());
    assertThat(listDesc.getDefaultValue(), isEmptyString());

    PipelineOptionDescriptor stringDesc = mapped.get("string");
    assertThat(stringDesc, notNullValue());
    assertThat(stringDesc.getDescription(), isEmptyString());
    assertEquals(PipelineOptionType.Enum.STRING, stringDesc.getType());
    assertThat(stringDesc.getDefaultValue(), isEmptyString());

    PipelineOptionDescriptor integerDesc = mapped.get("integer");
    assertThat(integerDesc, notNullValue());
    assertEquals("integer property", integerDesc.getDescription());
    assertEquals(PipelineOptionType.Enum.INTEGER, integerDesc.getType());
    assertThat(integerDesc.getDefaultValue(), isEmptyString());

    PipelineOptionDescriptor floatDesc = mapped.get("float");
    assertThat(integerDesc, notNullValue());
    assertEquals("float number property", floatDesc.getDescription());
    assertEquals(PipelineOptionType.Enum.NUMBER, floatDesc.getType());
    assertThat(floatDesc.getDefaultValue(), isEmptyString());

    PipelineOptionDescriptor booleanSimpleDesc = mapped.get("boolean_simple");
    assertThat(booleanSimpleDesc, notNullValue());
    assertEquals("simple boolean property", booleanSimpleDesc.getDescription());
    assertEquals(PipelineOptionType.Enum.BOOLEAN, booleanSimpleDesc.getType());
    assertThat(booleanSimpleDesc.getDefaultValue(), equalTo("true"));

    PipelineOptionDescriptor booleanWrapperDesc = mapped.get("boolean_wrapper");
    assertThat(booleanWrapperDesc, notNullValue());
    assertThat(booleanWrapperDesc.getDescription(), isEmptyString());
    assertEquals(PipelineOptionType.Enum.BOOLEAN, booleanWrapperDesc.getType());
    assertThat(booleanWrapperDesc.getDefaultValue(), equalTo("false"));
  }