パーサーとしてのTrafaret。 JSONスキーマの実装

イントロ


言語の開発には、コンパイラが記述されている場合、そのようなステップがあります。
trafaretライブラリのクールさを証明するために、私も同じことをすることにしました
再帰的にどこへ行くか。


出力を返すJson Schemaパーサーをステンシルに書きましょう
この説明に従って文書をチェックするための既製のステンシル。


つまり、Trafaret型のオブジェクト(正しいJSONスキーマドキュメントを供給する場合)
出力はTrafaret型のオブジェクトを返します。これにドキュメントをフィードできます
説明に対応。


これを検証ライブラリでどのように行うことができますか? 検証ライブラリはそうではありませんが、ユニバーサルトランスフォーマーを使用するのは簡単です。 Trafaretはデータパーサー、より正確には組み合わせパーサーです。 parsecやfuncparserlibなど、聞いたことのあるパーサーのみが文字列を解析し、Trafaretは手元にあるすべてのものと、タレント作成者がエンコードしなければならないものを解析します。


Jsonスキーマは、人々に最も近いのはおそらくこれである文書の束で通常通りに記述されます-http://json-schema.org/latest/json-schema-validation.html
ここでは、ドキュメントの正確さの基準を説明できる多くのキーワードの説明がありますが、ここではたった1箇所での$ refの実装に見事な激怒があります。


パート1


基本バージョンのjsonスキームには、非常に単純な実装があります-最大値(数値の最大値)、 pattern (文字列をチェックするために使用される通常の行)、 items (子スキームまたは配列要素をチェックするためのスキームの配列)などのすべてのキーワード。
したがって、これらのキーワードはすべて個別に適用する必要があります。 上限に達したため、上限に準拠しているかどうかこの番号をすぐに確認します。 つまり、次のような回路を使用できます。


 { "type": "number", "maximum": 45, } 

それをコンポーネントに分解します。すべてのチェックに合格する必要があるチェックのリストです。


 validations = [] for key, value in schema: if key == 'type': if value == 'number': validations.append(is_a_number) 

なんてこった、そして私もスキームをチェックします。 おそらく、いくつかの例で終わります。パーサーの作成を始めましょう。 Json Schemaは、辞書、オブジェクト、マップ、中括弧{}内の短いキーと値です。 だから辞書をチェックしてみてください:


 import trafaret as t #     trafaret     json_schema_type = t.Enum('null', 'boolean', 'object', 'array', 'number', 'integer', 'string') json_schema_keys = t.Dict( t.Key('type', optional=True, trafaret=json_schema_type), t.Key('maximum', optional=True, trafaret=t.Int()), ) 

スクロールで読者を飽きさせないために、2、3のキーワードのみを取りました。 検証は機能しますが、最終的にはスキームをチェックするだけでなく、バ​​リデーターを取得する必要があります。 そして、これは、多くの場合と異なり、trafaretが2回行うこととまったく同じですが、少し考えが必要です。


操作&があり、2つのステンシルを取り、2番目のステンシルがなければ最初の出力に適用します
検証エラー、つまり次のように:


 check_password = ( t.String() & (lambda value: value if value == 'secret' else t.DataError('Wrong password')) ) 

入力が文字列check_password(123)ではない場合、出力は値が文字列ではないというメッセージをすぐに受信し、文字列「secret」への準拠をチェックしません。
ステンシルで同等のpython値をチェックするには、Atomがあります。


そして、次のようなタイプを記述することも可能です。


 json_schema_type = ( t.Atom('null') & t.Null() | t.Atom('boolean') & t.Bool() ) 

しかし、これは私たちが望むものではありません。 ステンシルを返却し、すぐに適用したくない
明らかに誤ったオプション-文字列 'null'は絶対にNoneではありません。


ステンシルでもあるヘルパーを作成し、指定されたステンシルを返します。


 def just(trafaret): """Returns trafaret and ignoring values""" def create(value): return trafaret return create 

該当するもの:


 json_schema_type = ( t.Atom('null') & just(t.Null()) | t.Atom('boolean') & just(t.Bool()) | t.Atom('object') & just(t.Type(dict)) | t.Atom('array') & just(t.Type(list)) | t.Atom('number') & just(t.Float()) | t.Atom('integer') & just(t.Int()) | t.Atom('string') & just(t.String()) ) 

json_schema_type('null')を呼び出すと、 t.Null()インスタンスが返されます。 パーサーが出現し始めました
最終結果。


難易度の最初のレベルが渡され、 typeを実装しました。 喜びがあれば、 enumconstmultipleOfmaximumなども同じように行います。


2番目の難易度に進みます。


jsonスキームのほとんどすべてのキーワードは独立していますが、それらの一部は依然として互いに依存しています。 これらは、配列とオブジェクトのキーワードです。 additionalItemsは、 items記述されていない配列要素をチェックするための子スキーマです。 つまり、たとえば、 "items": [{"type":"string"}, {"type":"bool"}]は最初の2つの要素をチェックしますが、チェック対象のドキュメントに3つ以上ある場合は、すでにチェックする必要がありますadditionalItems (指定されている場合)、または
これ自体が間違いです。


2番目のケースはadditionalPropertiesです。 プロパティとpatternPropertiesはjsonスキームのオブジェクトをチェックするために使用され、最初の2つで説明されていないすべての場合、 additionalProperties使用されます。


これは、原則として、スクリーン構築で長年解決されたトピックであり、特別なキーが使用されますが、100%の人々がスクリーン構築に従事しているわけではないため、もう少し詳しく説明します。


ステンシルの辞書を確認するために、あまり標準的ではないアプローチが使用されました。 本質的に
キーは、ステンシル内の辞書、特にKey 、およびDict自体をチェックするために使用されます
これは、特定のオブジェクトのすべてのキーの実行結果を収集するバインディングです。


mypyでのキータイプは次のようになります。


 KeyT = Callable[ #      __call__ [Mapping], #      Mapping (.. dict ) Sequence[ #  ,       Tuple[ #          str, #         –         Union[Any, t.DataError], #   -     DataError Sequence[str] #             ] ] ] 

3回注意深く見て、括弧の前の最後の行に注意を払います。キーは、プルした辞書のすべてのキーを報告します。 つまり、キーは多数のキーを引き出し、任意の数のキーを返すことができます。 ディクショナリのどのキーがプルされたかを知るにはキーは、ディクショナリに追加または追加の要素があるかどうかを調べるために必要です。


その結果、キーはすぐに多数のキーを取得し、一度にチェックできます。 通常、キー自体が非常に独立している場合、 passwordpassword_confirmationをどうするかを決定しました。 しかし、私たちの場合、タスクは2つのキーを比較して平等にするよりもややトリッキーであり、固有の柔軟性によりこれを行うことができません。


subdict


 def subdict(name, *keys, **kw): trafaret = kw.pop('trafaret') # coz py2k def inner(data, context=None): errors = False preserve_output = [] #   ,      touched = set() collect = {} for key in keys: for k, v, names in key(data, context=context): touched.update(names) preserve_output.append((k, v, names)) if isinstance(v, t.DataError): errors = True else: collect[k] = v if errors: for out in preserve_output: yield out elif collect: #              yield name, t.catch(trafaret, **collect), touched return inner 

そして、このようなものはtrafaret_schemaの腸で使用されtrafaret_schema


 subdict( 'array', t.Key('items', optional=True, trafaret=ensure_list(json_schema)), t.Key('additionalItems', optional=True, trafaret=json_schema), trafaret=check_array, ), 

3番目の図、状態が必要です


まあ、状態は状態ではありませんが、誕生から、ステンシルはその魂の中で完全に機能的にきれいでした。 コンベアに乗るすべてのものが隣人に影響を与えることはありません。 そしてそれは素晴らしいことです! 機能主義の善良で決してエリートでない支持者は、数学の観点からこれを何度もかみました。


しかし、jsonスキームの次のレベルでは、メガボス- $ref出会います。 非常に合理的なことで、すでにどこかで定義されている別のスキームを参照できます。 たとえば、スキーマは、 definitionsまたは一般的な別のドキュメントでdefinitionsできます。


そのため、回路を分析するプロセスでは、すべての回路定義とそのアドレスを1か所で収集する必要があります。 次に、ドキュメントで見つかったすべての$ refに定義があることを確認します。 そして、実行の過程で、それはすでに明らかです-$ refのステンシルは、レジストリから目的のステンシルを取得するだけです。


さて、吐くためにレジストリを一度書いてください:


 class Register: def __init__(self): "  " pass 

しかし、その後、ステンシルを完成させる必要があり、標準の引数valueに加えて、チェーンに沿ってcontext=Anyを標準ステンシルに入れることができます。 そして実際に書かれたばかりのRegisterはまさにこのコンテキストです。


このようなものを使用して、 $refステンシルを定義します。


 def ref_field(reference, context=None): register = context #     context  register.reg_reference(reference) #    $ref,       def inner(value, context=None): #    ,     schema = register.get_schema(reference) #        return schema(value, context=context) #      return inner 

もちろん、最も困難なのは、考えられるすべてのサブサーキットへのリンクを収集することです。
以下は、リンクを保持したい子スキーマの例です。


 def deep_schema(key): #   def inner(data, context=None): register = context register.push(key) #   key        #      —      push,     #     try: schema = json_schema(data, context=register) register.save_schema(schema) #   ,        return schema finally: register.pop() return t.Call(inner) 

なぜ


両方の長所-jsonは広く配布され、あらゆる言語でサポートされています。 また、ステンシルは、Pythonでチェックする変換の最適なライブラリです。 より正確には、唯一のもの。 そして最も重要なことは、 formatキーワードに対して、次のようにステンシルをスリップできることです。


 import trafaret as t from trafaret_schema import json_schema, Register my_reg = Register() my_reg.reg_format('any_ip', t.IPv4 | t.IPv6) check_address = json_schema(open('address.rjson').read(), context=register) check_person = json_schema(open('person.json').read(), context=register) 

まとめ




Source: https://habr.com/ru/post/J336282/


All Articles